Skip to content

Commit c64268f

Browse files
authored
feat(updater): expose builder, allow setting a custom version checker (#3792)
1 parent f6e32ee commit c64268f

File tree

5 files changed

+178
-69
lines changed

5 files changed

+178
-69
lines changed

.changes/updater-check-api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"tauri": patch
33
---
44

5-
Added `check_for_updates` method to `App` and `AppHandle`.
5+
Added `updater` method to `App` and `AppHandle`, a builder to check for app updates.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
Allow using a custom updater version checker via `App::updater().should_install()`.

core/tauri/src/app.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -398,10 +398,25 @@ macro_rules! shared_app_impl {
398398
impl<R: Runtime> $app {
399399
#[cfg(updater)]
400400
#[cfg_attr(doc_cfg, doc(cfg(feature = "updater")))]
401-
/// Runs the updater to check if there is a new app version.
402-
/// It is the same as triggering the `tauri://update` event.
403-
pub async fn check_for_updates(&self) -> updater::Result<updater::UpdateResponse<R>> {
404-
updater::check(self.app_handle()).await
401+
/// Gets the updater builder to manually check if an update is available.
402+
///
403+
/// # Examples
404+
///
405+
/// ```no_run
406+
/// tauri::Builder::default()
407+
/// .setup(|app| {
408+
/// let handle = app.handle();
409+
/// tauri::async_runtime::spawn(async move {
410+
#[cfg_attr(
411+
any(feature = "updater", feature = "__updater-docs"),
412+
doc = r#" let response = handle.updater().check().await;"#
413+
)]
414+
/// });
415+
/// Ok(())
416+
/// });
417+
/// ```
418+
pub fn updater(&self) -> updater::UpdateBuilder<R> {
419+
updater::builder(self.app_handle())
405420
}
406421

407422
/// Creates a new webview window.

core/tauri/src/updater/core.rs

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use tauri_utils::{platform::current_exe, Env};
2121
use std::io::Seek;
2222
use std::{
2323
collections::HashMap,
24-
env,
24+
env, fmt,
2525
io::{Cursor, Read},
2626
path::{Path, PathBuf},
2727
str::from_utf8,
@@ -198,29 +198,42 @@ impl RemoteRelease {
198198
}
199199
}
200200

201-
#[derive(Debug)]
202-
pub struct UpdateBuilder<'a, R: Runtime> {
201+
pub struct UpdateBuilder<R: Runtime> {
203202
/// Application handle.
204203
pub app: AppHandle<R>,
205204
/// Current version we are running to compare with announced version
206-
pub current_version: &'a str,
205+
pub current_version: String,
207206
/// The URLs to checks updates. We suggest at least one fallback on a different domain.
208207
pub urls: Vec<String>,
209208
/// The platform the updater will check and install the update. Default is from `get_updater_target`
210209
pub target: Option<String>,
211210
/// The current executable path. Default is automatically extracted.
212211
pub executable_path: Option<PathBuf>,
212+
should_install: Option<Box<dyn FnOnce(&str, &str) -> bool + Send>>,
213+
}
214+
215+
impl<R: Runtime> fmt::Debug for UpdateBuilder<R> {
216+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217+
f.debug_struct("UpdateBuilder")
218+
.field("app", &self.app)
219+
.field("current_version", &self.current_version)
220+
.field("urls", &self.urls)
221+
.field("target", &self.target)
222+
.field("executable_path", &self.executable_path)
223+
.finish()
224+
}
213225
}
214226

215227
// Create new updater instance and return an Update
216-
impl<'a, R: Runtime> UpdateBuilder<'a, R> {
228+
impl<R: Runtime> UpdateBuilder<R> {
217229
pub fn new(app: AppHandle<R>) -> Self {
218230
UpdateBuilder {
219231
app,
220232
urls: Vec::new(),
221233
target: None,
222234
executable_path: None,
223-
current_version: env!("CARGO_PKG_VERSION"),
235+
current_version: env!("CARGO_PKG_VERSION").into(),
236+
should_install: None,
224237
}
225238
}
226239

@@ -250,15 +263,14 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
250263

251264
/// Set the current app version, used to compare against the latest available version.
252265
/// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml`
253-
pub fn current_version(mut self, ver: &'a str) -> Self {
254-
self.current_version = ver;
266+
pub fn current_version(mut self, ver: impl Into<String>) -> Self {
267+
self.current_version = ver.into();
255268
self
256269
}
257270

258271
/// Set the target name. Represents the string that is looked up on the updater API or response JSON.
259-
#[allow(dead_code)]
260-
pub fn target(mut self, target: &str) -> Self {
261-
self.target = Some(target.to_owned());
272+
pub fn target(mut self, target: impl Into<String>) -> Self {
273+
self.target.replace(target.into());
262274
self
263275
}
264276

@@ -269,7 +281,12 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
269281
self
270282
}
271283

272-
pub async fn build(self) -> Result<Update<R>> {
284+
pub fn should_install<F: FnOnce(&str, &str) -> bool + Send + 'static>(mut self, f: F) -> Self {
285+
self.should_install.replace(Box::new(f));
286+
self
287+
}
288+
289+
pub async fn build(mut self) -> Result<Update<R>> {
273290
let mut remote_release: Option<RemoteRelease> = None;
274291

275292
// make sure we have at least one url
@@ -279,9 +296,6 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
279296
));
280297
};
281298

282-
// set current version if not set
283-
let current_version = self.current_version;
284-
285299
// If no executable path provided, we use current_exe from tauri_utils
286300
let executable_path = self.executable_path.unwrap_or(current_exe()?);
287301

@@ -324,7 +338,7 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
324338
// The main objective is if the update URL is defined via the Cargo.toml
325339
// the URL will be generated dynamicly
326340
let fixed_link = url
327-
.replace("{{current_version}}", current_version)
341+
.replace("{{current_version}}", &self.current_version)
328342
.replace("{{target}}", &target)
329343
.replace("{{arch}}", arch);
330344

@@ -383,8 +397,11 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
383397
let final_release = remote_release.ok_or(Error::ReleaseNotFound)?;
384398

385399
// did the announced version is greated than our current one?
386-
let should_update =
387-
version::is_greater(current_version, &final_release.version).unwrap_or(false);
400+
let should_update = if let Some(comparator) = self.should_install.take() {
401+
comparator(&self.current_version, &final_release.version)
402+
} else {
403+
version::is_greater(&self.current_version, &final_release.version).unwrap_or(false)
404+
};
388405

389406
// create our new updater
390407
Ok(Update {
@@ -404,7 +421,7 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
404421
}
405422
}
406423

407-
pub fn builder<'a, R: Runtime>(app: AppHandle<R>) -> UpdateBuilder<'a, R> {
424+
pub fn builder<R: Runtime>(app: AppHandle<R>) -> UpdateBuilder<R> {
408425
UpdateBuilder::new(app)
409426
}
410427

core/tauri/src/updater/mod.rs

Lines changed: 117 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
//! .setup(|app| {
102102
//! let handle = app.handle();
103103
//! tauri::async_runtime::spawn(async move {
104-
//! let response = handle.check_for_updates().await;
104+
//! let response = handle.updater().check().await;
105105
//! });
106106
//! Ok(())
107107
//! });
@@ -165,7 +165,7 @@
165165
//! .setup(|app| {
166166
//! let handle = app.handle();
167167
//! tauri::async_runtime::spawn(async move {
168-
//! match handle.check_for_updates().await {
168+
//! match handle.updater().check().await {
169169
//! Ok(update) => {
170170
//! if update.is_update_available() {
171171
//! update.download_and_install().await.unwrap();
@@ -499,6 +499,115 @@ struct UpdateManifest {
499499
body: String,
500500
}
501501

502+
/// An update check builder.
503+
#[derive(Debug)]
504+
pub struct UpdateBuilder<R: Runtime> {
505+
inner: core::UpdateBuilder<R>,
506+
events: bool,
507+
}
508+
509+
impl<R: Runtime> UpdateBuilder<R> {
510+
/// Do not use the event system to emit information or listen to install the update.
511+
pub fn skip_events(mut self) -> Self {
512+
self.events = false;
513+
self
514+
}
515+
516+
/// Set the target name. Represents the string that is looked up on the updater API or response JSON.
517+
pub fn target(mut self, target: impl Into<String>) -> Self {
518+
self.inner = self.inner.target(target);
519+
self
520+
}
521+
522+
/// Sets a closure that is invoked to compare the current version and the latest version returned by the updater server.
523+
/// The first argument is the current version, and the second one is the latest version.
524+
///
525+
/// The closure must return `true` if the update should be installed.
526+
///
527+
/// # Examples
528+
///
529+
/// - Always install the version returned by the server:
530+
///
531+
/// ```no_run
532+
/// tauri::Builder::default()
533+
/// .setup(|app| {
534+
/// tauri::updater::builder(app.handle()).should_install(|_current, _latest| true);
535+
/// Ok(())
536+
/// });
537+
/// ```
538+
pub fn should_install<F: FnOnce(&str, &str) -> bool + Send + 'static>(mut self, f: F) -> Self {
539+
self.inner = self.inner.should_install(f);
540+
self
541+
}
542+
543+
/// Check if an update is available.
544+
///
545+
/// # Examples
546+
///
547+
/// ```no_run
548+
/// tauri::Builder::default()
549+
/// .setup(|app| {
550+
/// let handle = app.handle();
551+
/// tauri::async_runtime::spawn(async move {
552+
/// match tauri::updater::builder(handle).check().await {
553+
/// Ok(update) => {}
554+
/// Err(error) => {}
555+
/// }
556+
/// });
557+
/// Ok(())
558+
/// });
559+
/// ```
560+
pub async fn check(self) -> Result<UpdateResponse<R>> {
561+
let handle = self.inner.app.clone();
562+
let events = self.events;
563+
// check updates
564+
match self.inner.build().await {
565+
Ok(update) => {
566+
if events {
567+
// send notification if we need to update
568+
if update.should_update {
569+
let body = update.body.clone().unwrap_or_else(|| String::from(""));
570+
571+
// Emit `tauri://update-available`
572+
let _ = handle.emit_all(
573+
EVENT_UPDATE_AVAILABLE,
574+
UpdateManifest {
575+
body: body.clone(),
576+
date: update.date.clone(),
577+
version: update.version.clone(),
578+
},
579+
);
580+
let _ = handle.create_proxy().send_event(EventLoopMessage::Updater(
581+
UpdaterEvent::UpdateAvailable {
582+
body,
583+
date: update.date.clone(),
584+
version: update.version.clone(),
585+
},
586+
));
587+
588+
// Listen for `tauri://update-install`
589+
let update_ = update.clone();
590+
handle.once_global(EVENT_INSTALL_UPDATE, move |_msg| {
591+
crate::async_runtime::spawn(async move {
592+
let _ = download_and_install(update_).await;
593+
});
594+
});
595+
} else {
596+
send_status_update(&handle, UpdaterEvent::AlreadyUpToDate);
597+
}
598+
}
599+
Ok(UpdateResponse { update })
600+
}
601+
Err(e) => {
602+
if self.events {
603+
send_status_update(&handle, UpdaterEvent::Error(e.to_string()));
604+
}
605+
Err(e)
606+
}
607+
}
608+
}
609+
}
610+
502611
/// The response of an updater check.
503612
pub struct UpdateResponse<R: Runtime> {
504613
update: core::Update<R>,
@@ -582,7 +691,7 @@ pub(crate) fn listener<R: Runtime>(handle: AppHandle<R>) {
582691
handle.listen_global(EVENT_CHECK_UPDATE, move |_msg| {
583692
let handle_ = handle_.clone();
584693
crate::async_runtime::spawn(async move {
585-
let _ = check(handle_.clone()).await;
694+
let _ = builder(handle_.clone()).check().await;
586695
});
587696
});
588697
}
@@ -617,7 +726,8 @@ pub(crate) async fn download_and_install<R: Runtime>(update: core::Update<R>) ->
617726
update_result
618727
}
619728

620-
pub(crate) async fn check<R: Runtime>(handle: AppHandle<R>) -> Result<UpdateResponse<R>> {
729+
/// Initializes the [`UpdateBuilder`] using the app configuration.
730+
pub fn builder<R: Runtime>(handle: AppHandle<R>) -> UpdateBuilder<R> {
621731
let updater_config = &handle.config().tauri.updater;
622732
let package_info = handle.package_info().clone();
623733

@@ -636,47 +746,9 @@ pub(crate) async fn check<R: Runtime>(handle: AppHandle<R>) -> Result<UpdateResp
636746
if let Some(target) = &handle.updater_settings.target {
637747
builder = builder.target(target);
638748
}
639-
640-
// check updates
641-
match builder.build().await {
642-
Ok(update) => {
643-
// send notification if we need to update
644-
if update.should_update {
645-
let body = update.body.clone().unwrap_or_else(|| String::from(""));
646-
647-
// Emit `tauri://update-available`
648-
let _ = handle.emit_all(
649-
EVENT_UPDATE_AVAILABLE,
650-
UpdateManifest {
651-
body: body.clone(),
652-
date: update.date.clone(),
653-
version: update.version.clone(),
654-
},
655-
);
656-
let _ = handle.create_proxy().send_event(EventLoopMessage::Updater(
657-
UpdaterEvent::UpdateAvailable {
658-
body,
659-
date: update.date.clone(),
660-
version: update.version.clone(),
661-
},
662-
));
663-
664-
// Listen for `tauri://update-install`
665-
let update_ = update.clone();
666-
handle.once_global(EVENT_INSTALL_UPDATE, move |_msg| {
667-
crate::async_runtime::spawn(async move {
668-
let _ = download_and_install(update_).await;
669-
});
670-
});
671-
} else {
672-
send_status_update(&handle, UpdaterEvent::AlreadyUpToDate);
673-
}
674-
Ok(UpdateResponse { update })
675-
}
676-
Err(e) => {
677-
send_status_update(&handle, UpdaterEvent::Error(e.to_string()));
678-
Err(e)
679-
}
749+
UpdateBuilder {
750+
inner: builder,
751+
events: true,
680752
}
681753
}
682754

0 commit comments

Comments
 (0)