From c64268f9274bdb7352da1a53184e487b03437dc2 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Mon, 28 Mar 2022 09:58:35 -0700 Subject: [PATCH] feat(updater): expose builder, allow setting a custom version checker (#3792) --- .changes/updater-check-api.md | 2 +- .changes/updater-custom-version-checker.md | 5 + core/tauri/src/app.rs | 23 ++- core/tauri/src/updater/core.rs | 55 ++++--- core/tauri/src/updater/mod.rs | 162 +++++++++++++++------ 5 files changed, 178 insertions(+), 69 deletions(-) create mode 100644 .changes/updater-custom-version-checker.md diff --git a/.changes/updater-check-api.md b/.changes/updater-check-api.md index cb77ea7c0a0..1077aecb1f2 100644 --- a/.changes/updater-check-api.md +++ b/.changes/updater-check-api.md @@ -2,4 +2,4 @@ "tauri": patch --- -Added `check_for_updates` method to `App` and `AppHandle`. +Added `updater` method to `App` and `AppHandle`, a builder to check for app updates. diff --git a/.changes/updater-custom-version-checker.md b/.changes/updater-custom-version-checker.md new file mode 100644 index 00000000000..ddc68bd10bc --- /dev/null +++ b/.changes/updater-custom-version-checker.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +Allow using a custom updater version checker via `App::updater().should_install()`. diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index acb913dbecc..af5b46839cd 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -398,10 +398,25 @@ macro_rules! shared_app_impl { impl $app { #[cfg(updater)] #[cfg_attr(doc_cfg, doc(cfg(feature = "updater")))] - /// Runs the updater to check if there is a new app version. - /// It is the same as triggering the `tauri://update` event. - pub async fn check_for_updates(&self) -> updater::Result> { - updater::check(self.app_handle()).await + /// Gets the updater builder to manually check if an update is available. + /// + /// # Examples + /// + /// ```no_run + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle(); + /// tauri::async_runtime::spawn(async move { + #[cfg_attr( + any(feature = "updater", feature = "__updater-docs"), + doc = r#" let response = handle.updater().check().await;"# + )] + /// }); + /// Ok(()) + /// }); + /// ``` + pub fn updater(&self) -> updater::UpdateBuilder { + updater::builder(self.app_handle()) } /// Creates a new webview window. diff --git a/core/tauri/src/updater/core.rs b/core/tauri/src/updater/core.rs index 275ab2abf7d..4e87ef3b507 100644 --- a/core/tauri/src/updater/core.rs +++ b/core/tauri/src/updater/core.rs @@ -21,7 +21,7 @@ use tauri_utils::{platform::current_exe, Env}; use std::io::Seek; use std::{ collections::HashMap, - env, + env, fmt, io::{Cursor, Read}, path::{Path, PathBuf}, str::from_utf8, @@ -198,29 +198,42 @@ impl RemoteRelease { } } -#[derive(Debug)] -pub struct UpdateBuilder<'a, R: Runtime> { +pub struct UpdateBuilder { /// Application handle. pub app: AppHandle, /// Current version we are running to compare with announced version - pub current_version: &'a str, + pub current_version: String, /// The URLs to checks updates. We suggest at least one fallback on a different domain. pub urls: Vec, /// The platform the updater will check and install the update. Default is from `get_updater_target` pub target: Option, /// The current executable path. Default is automatically extracted. pub executable_path: Option, + should_install: Option bool + Send>>, +} + +impl fmt::Debug for UpdateBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UpdateBuilder") + .field("app", &self.app) + .field("current_version", &self.current_version) + .field("urls", &self.urls) + .field("target", &self.target) + .field("executable_path", &self.executable_path) + .finish() + } } // Create new updater instance and return an Update -impl<'a, R: Runtime> UpdateBuilder<'a, R> { +impl UpdateBuilder { pub fn new(app: AppHandle) -> Self { UpdateBuilder { app, urls: Vec::new(), target: None, executable_path: None, - current_version: env!("CARGO_PKG_VERSION"), + current_version: env!("CARGO_PKG_VERSION").into(), + should_install: None, } } @@ -250,15 +263,14 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> { /// Set the current app version, used to compare against the latest available version. /// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml` - pub fn current_version(mut self, ver: &'a str) -> Self { - self.current_version = ver; + pub fn current_version(mut self, ver: impl Into) -> Self { + self.current_version = ver.into(); self } /// Set the target name. Represents the string that is looked up on the updater API or response JSON. - #[allow(dead_code)] - pub fn target(mut self, target: &str) -> Self { - self.target = Some(target.to_owned()); + pub fn target(mut self, target: impl Into) -> Self { + self.target.replace(target.into()); self } @@ -269,7 +281,12 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> { self } - pub async fn build(self) -> Result> { + pub fn should_install bool + Send + 'static>(mut self, f: F) -> Self { + self.should_install.replace(Box::new(f)); + self + } + + pub async fn build(mut self) -> Result> { let mut remote_release: Option = None; // make sure we have at least one url @@ -279,9 +296,6 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> { )); }; - // set current version if not set - let current_version = self.current_version; - // If no executable path provided, we use current_exe from tauri_utils let executable_path = self.executable_path.unwrap_or(current_exe()?); @@ -324,7 +338,7 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> { // The main objective is if the update URL is defined via the Cargo.toml // the URL will be generated dynamicly let fixed_link = url - .replace("{{current_version}}", current_version) + .replace("{{current_version}}", &self.current_version) .replace("{{target}}", &target) .replace("{{arch}}", arch); @@ -383,8 +397,11 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> { let final_release = remote_release.ok_or(Error::ReleaseNotFound)?; // did the announced version is greated than our current one? - let should_update = - version::is_greater(current_version, &final_release.version).unwrap_or(false); + let should_update = if let Some(comparator) = self.should_install.take() { + comparator(&self.current_version, &final_release.version) + } else { + version::is_greater(&self.current_version, &final_release.version).unwrap_or(false) + }; // create our new updater Ok(Update { @@ -404,7 +421,7 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> { } } -pub fn builder<'a, R: Runtime>(app: AppHandle) -> UpdateBuilder<'a, R> { +pub fn builder(app: AppHandle) -> UpdateBuilder { UpdateBuilder::new(app) } diff --git a/core/tauri/src/updater/mod.rs b/core/tauri/src/updater/mod.rs index 9feb32f9daa..3c9a1f6f85d 100644 --- a/core/tauri/src/updater/mod.rs +++ b/core/tauri/src/updater/mod.rs @@ -101,7 +101,7 @@ //! .setup(|app| { //! let handle = app.handle(); //! tauri::async_runtime::spawn(async move { -//! let response = handle.check_for_updates().await; +//! let response = handle.updater().check().await; //! }); //! Ok(()) //! }); @@ -165,7 +165,7 @@ //! .setup(|app| { //! let handle = app.handle(); //! tauri::async_runtime::spawn(async move { -//! match handle.check_for_updates().await { +//! match handle.updater().check().await { //! Ok(update) => { //! if update.is_update_available() { //! update.download_and_install().await.unwrap(); @@ -499,6 +499,115 @@ struct UpdateManifest { body: String, } +/// An update check builder. +#[derive(Debug)] +pub struct UpdateBuilder { + inner: core::UpdateBuilder, + events: bool, +} + +impl UpdateBuilder { + /// Do not use the event system to emit information or listen to install the update. + pub fn skip_events(mut self) -> Self { + self.events = false; + self + } + + /// Set the target name. Represents the string that is looked up on the updater API or response JSON. + pub fn target(mut self, target: impl Into) -> Self { + self.inner = self.inner.target(target); + self + } + + /// Sets a closure that is invoked to compare the current version and the latest version returned by the updater server. + /// The first argument is the current version, and the second one is the latest version. + /// + /// The closure must return `true` if the update should be installed. + /// + /// # Examples + /// + /// - Always install the version returned by the server: + /// + /// ```no_run + /// tauri::Builder::default() + /// .setup(|app| { + /// tauri::updater::builder(app.handle()).should_install(|_current, _latest| true); + /// Ok(()) + /// }); + /// ``` + pub fn should_install bool + Send + 'static>(mut self, f: F) -> Self { + self.inner = self.inner.should_install(f); + self + } + + /// Check if an update is available. + /// + /// # Examples + /// + /// ```no_run + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle(); + /// tauri::async_runtime::spawn(async move { + /// match tauri::updater::builder(handle).check().await { + /// Ok(update) => {} + /// Err(error) => {} + /// } + /// }); + /// Ok(()) + /// }); + /// ``` + pub async fn check(self) -> Result> { + let handle = self.inner.app.clone(); + let events = self.events; + // check updates + match self.inner.build().await { + Ok(update) => { + if events { + // send notification if we need to update + if update.should_update { + let body = update.body.clone().unwrap_or_else(|| String::from("")); + + // Emit `tauri://update-available` + let _ = handle.emit_all( + EVENT_UPDATE_AVAILABLE, + UpdateManifest { + body: body.clone(), + date: update.date.clone(), + version: update.version.clone(), + }, + ); + let _ = handle.create_proxy().send_event(EventLoopMessage::Updater( + UpdaterEvent::UpdateAvailable { + body, + date: update.date.clone(), + version: update.version.clone(), + }, + )); + + // Listen for `tauri://update-install` + let update_ = update.clone(); + handle.once_global(EVENT_INSTALL_UPDATE, move |_msg| { + crate::async_runtime::spawn(async move { + let _ = download_and_install(update_).await; + }); + }); + } else { + send_status_update(&handle, UpdaterEvent::AlreadyUpToDate); + } + } + Ok(UpdateResponse { update }) + } + Err(e) => { + if self.events { + send_status_update(&handle, UpdaterEvent::Error(e.to_string())); + } + Err(e) + } + } + } +} + /// The response of an updater check. pub struct UpdateResponse { update: core::Update, @@ -582,7 +691,7 @@ pub(crate) fn listener(handle: AppHandle) { handle.listen_global(EVENT_CHECK_UPDATE, move |_msg| { let handle_ = handle_.clone(); crate::async_runtime::spawn(async move { - let _ = check(handle_.clone()).await; + let _ = builder(handle_.clone()).check().await; }); }); } @@ -617,7 +726,8 @@ pub(crate) async fn download_and_install(update: core::Update) -> update_result } -pub(crate) async fn check(handle: AppHandle) -> Result> { +/// Initializes the [`UpdateBuilder`] using the app configuration. +pub fn builder(handle: AppHandle) -> UpdateBuilder { let updater_config = &handle.config().tauri.updater; let package_info = handle.package_info().clone(); @@ -636,47 +746,9 @@ pub(crate) async fn check(handle: AppHandle) -> Result { - // send notification if we need to update - if update.should_update { - let body = update.body.clone().unwrap_or_else(|| String::from("")); - - // Emit `tauri://update-available` - let _ = handle.emit_all( - EVENT_UPDATE_AVAILABLE, - UpdateManifest { - body: body.clone(), - date: update.date.clone(), - version: update.version.clone(), - }, - ); - let _ = handle.create_proxy().send_event(EventLoopMessage::Updater( - UpdaterEvent::UpdateAvailable { - body, - date: update.date.clone(), - version: update.version.clone(), - }, - )); - - // Listen for `tauri://update-install` - let update_ = update.clone(); - handle.once_global(EVENT_INSTALL_UPDATE, move |_msg| { - crate::async_runtime::spawn(async move { - let _ = download_and_install(update_).await; - }); - }); - } else { - send_status_update(&handle, UpdaterEvent::AlreadyUpToDate); - } - Ok(UpdateResponse { update }) - } - Err(e) => { - send_status_update(&handle, UpdaterEvent::Error(e.to_string())); - Err(e) - } + UpdateBuilder { + inner: builder, + events: true, } }