Explorar o código

feat(updater): separate intel and apple silicon targets, closes #3359 (#3739)

Lucas Fernandes Nogueira %!s(int64=3) %!d(string=hai) anos
pai
achega
579312f834

+ 5 - 0
.changes/custom-updater-target.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Added `updater_target` method to the `Builder` struct.

+ 5 - 0
.changes/refactor-updater-target.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+**Breaking change:** The updater default targets have been renamed to include better support for different architectures.

+ 11 - 0
core/tauri-utils/src/config.rs

@@ -1822,6 +1822,17 @@ pub struct UpdaterConfig {
   #[serde(default = "default_dialog")]
   pub dialog: bool,
   /// The updater endpoints. TLS is enforced on production.
+  ///
+  /// The updater URL can contain the following variables:
+  /// - {{current_version}}: The version of the app that is requesting the update
+  /// - {{target}}: The operating system name (one of `linux`, `windows` or `darwin`).
+  /// - {{arch}}: The architecture of the machine (one of `x86_64`, `i686`, `aarch64` or `armv7`).
+  ///
+  /// # Examples
+  ///
+  /// - "https://my.cdn.com/latest.json": a raw JSON endpoint that returns the latest version and download links for each platform.
+  /// - "https://updates.app.dev/{{target}}?version={{current_version}}&arch={{arch}}": a dedicated API with positional and query string arguments.
+  #[allow(rustdoc::bare_urls)]
   pub endpoints: Option<Vec<UpdaterEndpoint>>,
   /// Signature public key.
   #[serde(default)] // use default just so the schema doesn't flag it as required

+ 58 - 0
core/tauri/src/app.rs

@@ -167,6 +167,12 @@ impl<R: Runtime> GlobalWindowEvent<R> {
   }
 }
 
+#[cfg(updater)]
+#[derive(Debug, Clone, Default)]
+pub(crate) struct UpdaterSettings {
+  pub(crate) target: Option<String>,
+}
+
 /// The path resolver is a helper for the application-specific [`crate::api::path`] APIs.
 #[derive(Debug, Clone)]
 pub struct PathResolver {
@@ -217,6 +223,9 @@ pub struct AppHandle<R: Runtime> {
   clipboard_manager: R::ClipboardManager,
   #[cfg(feature = "system-tray")]
   tray_handle: Option<tray::SystemTrayHandle<R>>,
+  /// The updater configuration.
+  #[cfg(updater)]
+  pub(crate) updater_settings: UpdaterSettings,
 }
 
 impl<R: Runtime> AppHandle<R> {
@@ -264,6 +273,8 @@ impl<R: Runtime> Clone for AppHandle<R> {
       clipboard_manager: self.clipboard_manager.clone(),
       #[cfg(feature = "system-tray")]
       tray_handle: self.tray_handle.clone(),
+      #[cfg(updater)]
+      updater_settings: self.updater_settings.clone(),
     }
   }
 }
@@ -677,6 +688,10 @@ pub struct Builder<R: Runtime> {
   /// System tray event handlers.
   #[cfg(feature = "system-tray")]
   system_tray_event_listeners: Vec<SystemTrayEventListener<R>>,
+
+  /// The updater configuration.
+  #[cfg(updater)]
+  updater_settings: UpdaterSettings,
 }
 
 impl<R: Runtime> Builder<R> {
@@ -702,6 +717,8 @@ impl<R: Runtime> Builder<R> {
       system_tray: None,
       #[cfg(feature = "system-tray")]
       system_tray_event_listeners: Vec::new(),
+      #[cfg(updater)]
+      updater_settings: Default::default(),
     }
   }
 
@@ -1087,6 +1104,45 @@ impl<R: Runtime> Builder<R> {
     self
   }
 
+  /// Sets the current platform's target name for the updater.
+  ///
+  /// By default Tauri looks for a target in the format "{target}-{arch}",
+  /// where *target* is one of `darwin`, `linux` and `windows`
+  /// and *arch* is one of `i686`, `x86_64`, `aarch64` and `armv7`
+  /// based on the running platform. You can change the target name with this function.
+  ///
+  /// # Examples
+  ///
+  /// - Use a macOS Universal binary target name:
+  ///
+  /// ```no_run
+  /// let mut builder = tauri::Builder::default();
+  /// #[cfg(target_os = "macos")]
+  /// {
+  ///   builder = builder.updater_target("darwin-universal");
+  /// }
+  /// ```
+  ///
+  /// - Append debug information to the target:
+  ///
+  /// ```no_run
+  /// let kind = if cfg!(debug_assertions) { "debug" } else { "release" };
+  /// tauri::Builder::default()
+  ///   .updater_target(format!("{}-{}", tauri::updater::target().unwrap(), kind));
+  /// ```
+  ///
+  /// - Use the platform's target triple:
+  ///
+  /// ```no_run
+  /// tauri::Builder::default()
+  ///   .updater_target(tauri::utils::platform::target_triple().unwrap());
+  /// ```
+  #[cfg(updater)]
+  pub fn updater_target<T: Into<String>>(mut self, target: T) -> Self {
+    self.updater_settings.target.replace(target.into());
+    self
+  }
+
   /// Builds the application.
   #[allow(clippy::type_complexity)]
   pub fn build<A: Assets>(mut self, context: Context<A>) -> crate::Result<App<R>> {
@@ -1186,6 +1242,8 @@ impl<R: Runtime> Builder<R> {
         clipboard_manager,
         #[cfg(feature = "system-tray")]
         tray_handle: None,
+        #[cfg(updater)]
+        updater_settings: self.updater_settings,
       },
     };
 

+ 49 - 42
core/tauri/src/updater/core.rs

@@ -235,8 +235,7 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
     self
   }
 
-  /// Set the target (os)
-  /// win32, win64, darwin and linux are currently supported
+  /// 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());
@@ -266,12 +265,17 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
     // If no executable path provided, we use current_exe from tauri_utils
     let executable_path = self.executable_path.unwrap_or(current_exe()?);
 
-    // Did the target is provided by the config?
-    // Should be: linux, darwin, win32 or win64
+    let has_custom_target = self.target.is_some();
     let target = self
       .target
-      .or_else(get_updater_target)
+      .or_else(|| get_updater_target().map(Into::into))
       .ok_or(Error::UnsupportedPlatform)?;
+    let arch = get_updater_arch().ok_or(Error::UnsupportedPlatform)?;
+    let json_target = if has_custom_target {
+      target.clone()
+    } else {
+      format!("{}-{}", target, arch)
+    };
 
     // Get the extract_path from the provided executable_path
     let extract_path = extract_path_from_executable(&self.app.state::<Env>(), &executable_path);
@@ -292,18 +296,17 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
     // Allow fallback if more than 1 urls is provided
     let mut last_error: Option<Error> = None;
     for url in &self.urls {
-      // replace {{current_version}} and {{target}} in the provided URL
+      // replace {{current_version}}, {{target}} and {{arch}} in the provided URL
       // this is usefull if we need to query example
-      // https://releases.myapp.com/update/{{target}}/{{current_version}}
+      // https://releases.myapp.com/update/{{target}}/{{arch}}/{{current_version}}
       // will be transleted into ->
-      // https://releases.myapp.com/update/darwin/1.0.0
+      // https://releases.myapp.com/update/darwin/aarch64/1.0.0
       // The main objective is if the update URL is defined via the Cargo.toml
       // the URL will be generated dynamicly
-      let fixed_link = str::replace(
-        &str::replace(url, "{{current_version}}", current_version),
-        "{{target}}",
-        &target,
-      );
+      let fixed_link = url
+        .replace("{{current_version}}", current_version)
+        .replace("{{target}}", &target)
+        .replace("{{arch}}", arch);
 
       // we want JSON only
       let mut headers = HashMap::new();
@@ -335,7 +338,7 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
             return Err(Error::UpToDate);
           };
           // Convert the remote result to our local struct
-          let built_release = RemoteRelease::from_release(&res.data, &target);
+          let built_release = RemoteRelease::from_release(&res.data, &json_target);
           // make sure all went well and the remote data is compatible
           // with what we need locally
           match built_release {
@@ -745,23 +748,27 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
   Ok(())
 }
 
-/// Returns a target os
-/// We do not use a helper function like the target_triple
-/// from tauri-utils because this function return `None` if
-/// the updater do not support the platform.
-///
-/// Available target: `linux, darwin, win32, win64`
-pub fn get_updater_target() -> Option<String> {
+pub(crate) fn get_updater_target() -> Option<&'static str> {
   if cfg!(target_os = "linux") {
-    Some("linux".into())
+    Some("linux")
   } else if cfg!(target_os = "macos") {
-    Some("darwin".into())
+    Some("darwin")
   } else if cfg!(target_os = "windows") {
-    if cfg!(target_pointer_width = "32") {
-      Some("win32".into())
-    } else {
-      Some("win64".into())
-    }
+    Some("windows")
+  } else {
+    None
+  }
+}
+
+pub(crate) fn get_updater_arch() -> Option<&'static str> {
+  if cfg!(target_arch = "x86") {
+    Some("i686")
+  } else if cfg!(target_arch = "x86_64") {
+    Some("x86_64")
+  } else if cfg!(target_arch = "arm") {
+    Some("armv7")
+  } else if cfg!(target_arch = "aarch64") {
+    Some("aarch64")
   } else {
     None
   }
@@ -859,15 +866,15 @@ mod test {
       "notes": "Test version !",
       "pub_date": "2020-06-22T19:25:57Z",
       "platforms": {
-        "darwin": {
+        "darwin-aarch64": {
           "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJZVGdpKzJmRWZ0SkRvWS9TdFpqTU9xcm1mUmJSSG5OWVlwSklrWkN1SFpWbmh4SDlBcTU3SXpjbm0xMmRjRkphbkpVeGhGcTdrdzlrWGpGVWZQSWdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1MDU3CWZpbGU6L1VzZXJzL3J1bm5lci9ydW5uZXJzLzIuMjYzLjAvd29yay90YXVyaS90YXVyaS90YXVyaS9leGFtcGxlcy9jb21tdW5pY2F0aW9uL3NyYy10YXVyaS90YXJnZXQvZGVidWcvYnVuZGxlL29zeC9hcHAuYXBwLnRhci5negp4ZHFlUkJTVnpGUXdDdEhydTE5TGgvRlVPeVhjTnM5RHdmaGx3c0ZPWjZXWnFwVDRNWEFSbUJTZ1ZkU1IwckJGdmlwSzJPd00zZEZFN2hJOFUvL1FDZz09Cg==",
           "url": "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.app.tar.gz"
         },
-        "linux": {
+        "linux-x86_64": {
           "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOWZSM29hTFNmUEdXMHRoOC81WDFFVVFRaXdWOUdXUUdwT0NlMldqdXkyaWVieXpoUmdZeXBJaXRqSm1YVmczNXdRL1Brc0tHb1NOTzhrL1hadFcxdmdnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE3MzQzCWZpbGU6L2hvbWUvcnVubmVyL3dvcmsvdGF1cmkvdGF1cmkvdGF1cmkvZXhhbXBsZXMvY29tbXVuaWNhdGlvbi9zcmMtdGF1cmkvdGFyZ2V0L2RlYnVnL2J1bmRsZS9hcHBpbWFnZS9hcHAuQXBwSW1hZ2UudGFyLmd6CmRUTUM2bWxnbEtTbUhOZGtERUtaZnpUMG5qbVo5TGhtZWE1SFNWMk5OOENaVEZHcnAvVW0zc1A2ajJEbWZUbU0yalRHT0FYYjJNVTVHOHdTQlYwQkF3PT0K",
           "url": "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.AppImage.tar.gz"
         },
-        "win64": {
+        "windows-x86_64": {
           "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K",
           "url": "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.x64.msi.zip"
         }
@@ -965,7 +972,7 @@ mod test {
   }
 
   #[test]
-  fn simple_http_updater_raw_json_win64() {
+  fn simple_http_updater_raw_json_windows_x86_64() {
     let _m = mockito::mock("GET", "/")
       .with_status(200)
       .with_header("content-type", "application/json")
@@ -975,7 +982,7 @@ mod test {
     let app = crate::test::mock_app();
     let check_update = block!(builder(app.handle())
       .current_version("0.0.0")
-      .target("win64")
+      .target("windows-x86_64")
       .url(mockito::server_url())
       .build());
 
@@ -1013,7 +1020,7 @@ mod test {
 
   #[test]
   fn simple_http_updater_without_version() {
-    let _m = mockito::mock("GET", "/darwin/1.0.0")
+    let _m = mockito::mock("GET", "/darwin-aarch64/1.0.0")
       .with_status(200)
       .with_header("content-type", "application/json")
       .with_body(generate_sample_platform_json(
@@ -1027,7 +1034,7 @@ mod test {
     let check_update = block!(builder(app.handle())
       .current_version("1.0.0")
       .url(format!(
-        "{}/darwin/{{{{current_version}}}}",
+        "{}/darwin-aarch64/{{{{current_version}}}}",
         mockito::server_url()
       ))
       .build());
@@ -1040,7 +1047,7 @@ mod test {
 
   #[test]
   fn simple_http_updater_percent_decode() {
-    let _m = mockito::mock("GET", "/darwin/1.0.0")
+    let _m = mockito::mock("GET", "/darwin-aarch64/1.0.0")
       .with_status(200)
       .with_header("content-type", "application/json")
       .with_body(generate_sample_platform_json(
@@ -1055,7 +1062,7 @@ mod test {
       .current_version("1.0.0")
       .url(
         url::Url::parse(&format!(
-          "{}/darwin/{{{{current_version}}}}",
+          "{}/darwin-aarch64/{{{{current_version}}}}",
           mockito::server_url()
         ))
         .unwrap()
@@ -1072,7 +1079,7 @@ mod test {
     let check_update = block!(builder(app.handle())
       .current_version("1.0.0")
       .urls(&[url::Url::parse(&format!(
-        "{}/darwin/{{{{current_version}}}}",
+        "{}/darwin-aarch64/{{{{current_version}}}}",
         mockito::server_url()
       ))
       .unwrap()
@@ -1087,7 +1094,7 @@ mod test {
 
   #[test]
   fn simple_http_updater_with_elevated_task() {
-    let _m = mockito::mock("GET", "/win64/1.0.0")
+    let _m = mockito::mock("GET", "/windows-x86_64/1.0.0")
       .with_status(200)
       .with_header("content-type", "application/json")
       .with_body(generate_sample_with_elevated_task_platform_json(
@@ -1102,7 +1109,7 @@ mod test {
     let check_update = block!(builder(app.handle())
       .current_version("1.0.0")
       .url(format!(
-        "{}/win64/{{{{current_version}}}}",
+        "{}/windows-x86_64/{{{{current_version}}}}",
         mockito::server_url()
       ))
       .build());
@@ -1115,7 +1122,7 @@ mod test {
 
   #[test]
   fn http_updater_uptodate() {
-    let _m = mockito::mock("GET", "/darwin/10.0.0")
+    let _m = mockito::mock("GET", "/darwin-aarch64/10.0.0")
       .with_status(200)
       .with_header("content-type", "application/json")
       .with_body(generate_sample_platform_json(
@@ -1129,7 +1136,7 @@ mod test {
     let check_update = block!(builder(app.handle())
       .current_version("10.0.0")
       .url(format!(
-        "{}/darwin/{{{{current_version}}}}",
+        "{}/darwin-aarch64/{{{{current_version}}}}",
         mockito::server_url()
       ))
       .build());

+ 40 - 18
core/tauri/src/updater/mod.rs

@@ -319,17 +319,25 @@
 //!   "notes":"Test version",
 //!   "pub_date":"2020-06-22T19:25:57Z",
 //!   "platforms": {
-//!     "darwin": {
+//!     "darwin-aarch64": {
 //!       "signature":"",
-//!       "url":"https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.app.tar.gz"
+//!       "url":"https://github.com/tauri-apps/tauri-test/releases/download/v1.0.0/app-aarch64.app.tar.gz"
 //!     },
-//!      "linux": {
+//!     "darwin-intel": {
 //!       "signature":"",
-//!       "url":"https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.AppImage.tar.gz"
+//!       "url":"https://github.com/tauri-apps/tauri-test/releases/download/v1.0.0/app-x86_64.app.tar.gz"
 //!     },
-//!     "win64": {
+//!     "linux-x86_64": {
 //!       "signature":"",
-//!       "url":"https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.x64.msi.zip"
+//!       "url":"https://github.com/tauri-apps/tauri-test/releases/download/v1.0.0/app.AppImage.tar.gz"
+//!     },
+//!     "windows-x86_64": {
+//!       "signature":"",
+//!       "url":"https://github.com/tauri-apps/tauri-test/releases/download/v1.0.0/app.x64.msi.zip"
+//!     },
+//!     "windows-i686": {
+//!       "signature":"",
+//!       "url":"https://github.com/tauri-apps/tauri-test/releases/download/v1.0.0/app.x86.msi.zip"
 //!     }
 //!   }
 //! }
@@ -462,6 +470,15 @@ pub const EVENT_STATUS_SUCCESS: &str = "DONE";
 /// When you receive this status, this is because the application is running last version
 pub const EVENT_STATUS_UPTODATE: &str = "UPTODATE";
 
+/// Gets the target string used on the updater.
+pub fn target() -> Option<String> {
+  if let (Some(target), Some(arch)) = (core::get_updater_target(), core::get_updater_arch()) {
+    Some(format!("{}-{}", target, arch))
+  } else {
+    None
+  }
+}
+
 #[derive(Clone, serde::Serialize)]
 struct StatusEvent {
   status: String,
@@ -526,13 +543,16 @@ pub(crate) async fn check_update_with_dialog<R: Runtime>(handle: AppHandle<R>) {
       .iter()
       .map(|e| e.to_string())
       .collect::<Vec<String>>();
-    // check updates
-    match self::core::builder(handle.clone())
+
+    let mut builder = self::core::builder(handle.clone())
       .urls(&endpoints[..])
-      .current_version(&package_info.version)
-      .build()
-      .await
-    {
+      .current_version(&package_info.version);
+    if let Some(target) = &handle.updater_settings.target {
+      builder = builder.target(target);
+    }
+
+    // check updates
+    match builder.build().await {
       Ok(updater) => {
         let pubkey = updater_config.pubkey.clone();
 
@@ -610,13 +630,15 @@ pub(crate) async fn check<R: Runtime>(handle: AppHandle<R>) -> Result<UpdateResp
     .map(|e| e.to_string())
     .collect::<Vec<String>>();
 
-  // check updates
-  match self::core::builder(handle.clone())
+  let mut builder = self::core::builder(handle.clone())
     .urls(&endpoints[..])
-    .current_version(&package_info.version)
-    .build()
-    .await
-  {
+    .current_version(&package_info.version);
+  if let Some(target) = &handle.updater_settings.target {
+    builder = builder.target(target);
+  }
+
+  // check updates
+  match builder.build().await {
     Ok(update) => {
       // send notification if we need to update
       if update.should_update {

+ 6 - 6
core/tauri/tests/restart/Cargo.lock

@@ -3181,18 +3181,18 @@ dependencies = [
 
 [[package]]
 name = "zstd"
-version = "0.10.0+zstd.1.5.2"
+version = "0.11.1+zstd.1.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b1365becbe415f3f0fcd024e2f7b45bacfb5bdd055f0dc113571394114e7bdd"
+checksum = "77a16b8414fde0414e90c612eba70985577451c4c504b99885ebed24762cb81a"
 dependencies = [
  "zstd-safe",
 ]
 
 [[package]]
 name = "zstd-safe"
-version = "4.1.4+zstd.1.5.2"
+version = "5.0.1+zstd.1.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f7cd17c9af1a4d6c24beb1cc54b17e2ef7b593dc92f19e9d9acad8b182bbaee"
+checksum = "7c12659121420dd6365c5c3de4901f97145b79651fb1d25814020ed2ed0585ae"
 dependencies = [
  "libc",
  "zstd-sys",
@@ -3200,9 +3200,9 @@ dependencies = [
 
 [[package]]
 name = "zstd-sys"
-version = "1.6.3+zstd.1.5.2"
+version = "2.0.1+zstd.1.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8"
+checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b"
 dependencies = [
  "cc",
  "libc",

+ 1 - 1
tooling/cli/schema.json

@@ -1741,7 +1741,7 @@
           "type": "boolean"
         },
         "endpoints": {
-          "description": "The updater endpoints. TLS is enforced on production.",
+          "description": "The updater endpoints. TLS is enforced on production.\n\nThe updater URL can contain the following variables: - {{current_version}}: The version of the app that is requesting the update - {{target}}: The operating system name (one of `linux`, `windows` or `darwin`). - {{arch}}: The architecture of the machine (one of `x86_64`, `i686`, `aarch64` or `armv7`).\n\n# Examples\n\n- \"https://my.cdn.com/latest.json\": a raw JSON endpoint that returns the latest version and download links for each platform. - \"https://updates.app.dev/{{target}}?version={{current_version}}&arch={{arch}}\": a dedicated API with positional and query string arguments.",
           "type": [
             "array",
             "null"