diff --git a/.gitignore b/.gitignore index 38051f65..41022b01 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,4 @@ pids .idea debug.log TODO.md -.aider* +.aider.* diff --git a/Cargo.lock b/Cargo.lock index 23f4bb0b..c76e8ff7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,7 +213,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "tauri-plugin-barcode-scanner", "tauri-plugin-biometric", "tauri-plugin-cli", @@ -245,7 +245,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "tauri-plugin-updater", "time", "tiny_http", @@ -258,7 +258,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "tauri-plugin-updater", "tiny_http", ] @@ -270,7 +270,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "tauri-plugin-store", ] @@ -1428,7 +1428,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "tauri-plugin-deep-link", "tauri-plugin-log", "tauri-plugin-single-instance", @@ -3615,9 +3615,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492" +checksum = "58b89bf91c19bf036347f1ab85a81c560f08c0667c8601bece664d860a600988" dependencies = [ "crossbeam-channel", "dpi", @@ -5729,7 +5729,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "tauri-plugin-cli", "tauri-plugin-single-instance", ] @@ -6348,16 +6348,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7a0f4019c80391d143ee26cd7cd1ed271ac241d3087d333f99f3269ba90812" +version = "2.6.2" +source = "git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1#232265c70e1c213bbb3f84b5541ddc07d330fce1" dependencies = [ "anyhow", "bytes", "dirs 6.0.0", "dunce", "embed_plist", - "getrandom 0.2.15", + "getrandom 0.3.2", "glob", "gtk", "heck 0.5.0", @@ -6383,11 +6382,11 @@ dependencies = [ "serialize-to-javascript", "specta", "swift-rs", - "tauri-build", + "tauri-build 2.3.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)", "tauri-macros", "tauri-runtime", "tauri-runtime-wry", - "tauri-utils", + "tauri-utils 2.5.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)", "thiserror 2.0.12", "tokio", "tray-icon", @@ -6417,8 +6416,29 @@ dependencies = [ "semver", "serde", "serde_json", - "tauri-codegen", - "tauri-utils", + "tauri-codegen 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tauri-utils 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tauri-winres", + "toml", + "walkdir", +] + +[[package]] +name = "tauri-build" +version = "2.3.0" +source = "git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1#232265c70e1c213bbb3f84b5541ddc07d330fce1" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars", + "semver", + "serde", + "serde_json", + "tauri-utils 2.5.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)", "tauri-winres", "toml", "walkdir", @@ -6442,7 +6462,7 @@ dependencies = [ "serde_json", "sha2", "syn 2.0.100", - "tauri-utils", + "tauri-utils 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 2.0.12", "time", "url", @@ -6451,17 +6471,41 @@ dependencies = [ ] [[package]] -name = "tauri-macros" +name = "tauri-codegen" version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f59e1d1fa9651212dcb890a0c66226d819b716490b0cf43c078514da3591705" +source = "git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1#232265c70e1c213bbb3f84b5541ddc07d330fce1" +dependencies = [ + "base64 0.22.1", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.100", + "tauri-utils 2.5.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)", + "thiserror 2.0.12", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.3.1" +source = "git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1#232265c70e1c213bbb3f84b5541ddc07d330fce1" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.100", - "tauri-codegen", - "tauri-utils", + "tauri-codegen 2.3.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)", + "tauri-utils 2.5.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)", ] [[package]] @@ -6476,7 +6520,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "tauri-utils", + "tauri-utils 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "toml", "walkdir", ] @@ -6554,7 +6598,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "tauri-utils", + "tauri-utils 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 2.0.12", "tracing", "url", @@ -6594,7 +6638,7 @@ dependencies = [ "serde_repr", "tauri", "tauri-plugin", - "tauri-utils", + "tauri-utils 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 2.0.12", "toml", "url", @@ -6915,7 +6959,7 @@ dependencies = [ "tokio", "url", "windows-sys 0.60.2", - "zip 4.0.0", + "zip 4.2.0", ] [[package]] @@ -6969,8 +7013,7 @@ dependencies = [ [[package]] name = "tauri-runtime" version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e7bb73d1bceac06c20b3f755b2c8a2cb13b20b50083084a8cf3700daf397ba4" +source = "git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1#232265c70e1c213bbb3f84b5541ddc07d330fce1" dependencies = [ "cookie", "dpi", @@ -6982,7 +7025,7 @@ dependencies = [ "raw-window-handle", "serde", "serde_json", - "tauri-utils", + "tauri-utils 2.5.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)", "thiserror 2.0.12", "url", "windows 0.61.1", @@ -6990,9 +7033,8 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe52ed0ef40fd7ad51a620ecb3018e32eba3040bb95025216a962a37f6f050c5" +version = "2.7.1" +source = "git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1#232265c70e1c213bbb3f84b5541ddc07d330fce1" dependencies = [ "gtk", "http", @@ -7007,7 +7049,7 @@ dependencies = [ "softbuffer", "tao", "tauri-runtime", - "tauri-utils", + "tauri-utils 2.5.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)", "url", "webkit2gtk", "webview2-com", @@ -7055,6 +7097,45 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-utils" +version = "2.5.0" +source = "git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1#232265c70e1c213bbb3f84b5541ddc07d330fce1" +dependencies = [ + "aes-gcm", + "anyhow", + "cargo_metadata", + "ctor", + "dunce", + "getrandom 0.3.2", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "serialize-to-javascript", + "swift-rs", + "thiserror 2.0.12", + "toml", + "url", + "urlpattern", + "uuid", + "walkdir", +] + [[package]] name = "tauri-winres" version = "0.3.0" @@ -7462,9 +7543,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d433764348e7084bad2c5ea22c96c71b61b17afe3a11645710f533bd72b6a2b5" +checksum = "2da75ec677957aa21f6e0b361df0daab972f13a5bee3606de0638fd4ee1c666a" dependencies = [ "crossbeam-channel", "dirs 6.0.0", @@ -7653,7 +7734,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "tauri-plugin-updater", "time", "tiny_http", @@ -8069,7 +8150,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "tauri-build", + "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "tauri-plugin-websocket", "tokio", "tokio-tungstenite", @@ -9068,9 +9149,9 @@ dependencies = [ [[package]] name = "zip" -version = "4.0.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd" +checksum = "95ab361742de920c5535880f89bbd611ee62002bf11341d16a5f057bb8ba6899" dependencies = [ "arbitrary", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index cfa616ca..b92610bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,6 @@ codegen-units = 1 lto = true incremental = false opt-level = "s" + +[patch.crates-io] +tauri = { git = "https://github.com/tauri-apps/tauri", rev = "232265c70e1c213bbb3f84b5541ddc07d330fce1" } diff --git a/plugins/updater/src/error.rs b/plugins/updater/src/error.rs index b82e7d55..b3cacc95 100644 --- a/plugins/updater/src/error.rs +++ b/plugins/updater/src/error.rs @@ -39,9 +39,12 @@ pub enum Error { /// `reqwest` crate errors. #[error(transparent)] Reqwest(#[from] reqwest::Error), - /// The platform was not found on the updater JSON response. - #[error("the platform `{0}` was not found on the response `platforms` object")] + /// The platform was not found in the updater JSON response. + #[error("the platform `{0}` was not found in the response `platforms` object")] TargetNotFound(String), + /// Neither the platform not the fallback platform was not found in the updater JSON response. + #[error("the platform `{0}` and `{1}` were not found in the response `platforms` object")] + TargetsNotFound(String, String), /// Download failed #[error("`{0}`")] Network(String), @@ -67,8 +70,8 @@ pub enum Error { TempDirNotFound, #[error("Authentication failed or was cancelled")] AuthenticationFailed, - #[error("Failed to install .deb package")] - DebInstallFailed, + #[error("Failed to install package")] + PackageInstallFailed, #[error("invalid updater binary format")] InvalidUpdaterFormat, #[error(transparent)] diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index 707c1489..68412fbd 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -26,7 +26,13 @@ use reqwest::{ }; use semver::Version; use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize}; -use tauri::{utils::platform::current_exe, AppHandle, Resource, Runtime}; +use tauri::{ + utils::{ + config::BundleType, + platform::{bundle_type, current_exe}, + }, + AppHandle, Resource, Runtime, +}; use time::OffsetDateTime; use url::Url; @@ -37,6 +43,31 @@ use crate::{ const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); +#[derive(Copy, Clone)] +pub enum Installer { + AppImage, + Deb, + Rpm, + + App, + + Msi, + Nsis, +} + +impl Installer { + fn suffix(self) -> &'static str { + match self { + Self::AppImage => "appimage", + Self::Deb => "deb", + Self::Rpm => "rpm", + Self::App => "app", + Self::Msi => "msi", + Self::Nsis => "nsis", + } + } +} + #[derive(Debug, Deserialize, Serialize, Clone)] pub struct ReleaseManifestPlatform { /// Download URL for the platform @@ -270,7 +301,8 @@ impl UpdaterBuilder { (target.clone(), target) } else { let target = get_updater_target().ok_or(Error::UnsupportedOs)?; - (target.to_string(), format!("{target}-{arch}")) + let json_target = format!("{target}-{arch}"); + (target.to_owned(), json_target) }; let executable_path = self.executable_path.clone().unwrap_or(current_exe()?); @@ -327,7 +359,7 @@ pub struct Updater { proxy: Option, endpoints: Vec, arch: &'static str, - // The `{{target}}` variable we replace in the endpoint + // The `{{target}}` variable we replace in the endpoint and serach for in the JSON target: String, // The value we search if the updater server returns a JSON with the `platforms` object json_target: String, @@ -342,6 +374,18 @@ pub struct Updater { } impl Updater { + fn get_updater_installer(&self) -> Option { + bundle_type().and_then(|t| match t { + BundleType::Deb => Some(Installer::Deb), + BundleType::Rpm => Some(Installer::Rpm), + BundleType::AppImage => Some(Installer::AppImage), + BundleType::Msi => Some(Installer::Msi), + BundleType::Nsis => Some(Installer::Nsis), + BundleType::App => Some(Installer::App), + BundleType::Dmg => None, + }) + } + pub async fn check(&self) -> Result> { // we want JSON only let mut headers = self.headers.clone(); @@ -466,6 +510,31 @@ impl Updater { None => release.version > self.current_version, }; + let mut download_url = release.download_url(&self.json_target); + let mut signature = release.signature(&self.json_target); + + let installer = self.get_updater_installer(); + if installer.is_none() && (download_url.is_err() || signature.is_err()) { + return Err(Error::TargetNotFound(self.json_target.clone())); + } + + if let Some(installer) = installer { + let target = &format!("{}-{}", &self.json_target, installer.suffix()); + download_url = + release + .download_url(target) + .or(download_url.or(Err(Error::TargetsNotFound( + self.json_target.clone(), + target.clone(), + )))); + signature = release + .signature(target) + .or(signature.or(Err(Error::TargetsNotFound( + self.json_target.clone(), + target.clone(), + )))); + } + let update = if should_update { Some(Update { run_on_main_thread: self.run_on_main_thread.clone(), @@ -477,9 +546,10 @@ impl Updater { extract_path: self.extract_path.clone(), version: release.version.to_string(), date: release.pub_date, - download_url: release.download_url(&self.json_target)?.to_owned(), - signature: release.signature(&self.json_target)?.to_owned(), - body: release.notes, + download_url: download_url?.to_owned(), + body: release.notes.clone(), + signature: signature?.to_owned(), + installer, raw_json: raw_json.unwrap(), timeout: None, proxy: self.proxy.clone(), @@ -513,6 +583,8 @@ pub struct Update { pub date: Option, /// Target pub target: String, + /// Current installer + pub installer: Option, /// Download URL announced pub download_url: Url, /// Signature announced @@ -852,11 +924,10 @@ impl Update { /// └── ... /// fn install_inner(&self, bytes: &[u8]) -> Result<()> { - if self.is_deb_package() { - self.install_deb(bytes) - } else { - // Handle AppImage or other formats - self.install_appimage(bytes) + match self.installer { + Some(Installer::Deb) => self.install_deb(bytes), + Some(Installer::Rpm) => self.install_rpm(bytes), + _ => self.install_appimage(bytes), } } @@ -933,39 +1004,6 @@ impl Update { Err(Error::TempDirNotOnSameMountPoint) } - fn is_deb_package(&self) -> bool { - // First check if we're in a typical Debian installation path - let in_system_path = self - .extract_path - .to_str() - .map(|p| p.starts_with("/usr")) - .unwrap_or(false); - - if !in_system_path { - return false; - } - - // Then verify it's actually a Debian-based system by checking for dpkg - let dpkg_exists = std::path::Path::new("/var/lib/dpkg").exists(); - let apt_exists = std::path::Path::new("/etc/apt").exists(); - - // Additional check for the package in dpkg database - let package_in_dpkg = if let Ok(output) = std::process::Command::new("dpkg") - .args(["-S", &self.extract_path.to_string_lossy()]) - .output() - { - output.status.success() - } else { - false - }; - - // Consider it a deb package only if: - // 1. We're in a system path AND - // 2. We have Debian package management tools AND - // 3. The binary is tracked by dpkg - dpkg_exists && apt_exists && package_in_dpkg - } - fn install_deb(&self, bytes: &[u8]) -> Result<()> { // First verify the bytes are actually a .deb package if !infer::archive::is_deb(bytes) { @@ -973,6 +1011,18 @@ impl Update { return Err(Error::InvalidUpdaterFormat); } + self.try_tmp_locations(bytes, "dpkg", "-i") + } + + fn install_rpm(&self, bytes: &[u8]) -> Result<()> { + // First verify the bytes are actually a .rpm package + if !infer::archive::is_rpm(bytes) { + return Err(Error::InvalidUpdaterFormat); + } + self.try_tmp_locations(bytes, "rpm", "-U") + } + + fn try_tmp_locations(&self, bytes: &[u8], install_cmd: &str, install_arg: &str) -> Result<()> { // Try different temp directories let tmp_dir_locations = vec![ Box::new(|| Some(std::env::temp_dir())) as Box Option>, @@ -984,15 +1034,19 @@ impl Update { for tmp_dir_location in tmp_dir_locations { if let Some(path) = tmp_dir_location() { if let Ok(tmp_dir) = tempfile::Builder::new() - .prefix("tauri_deb_update") + .prefix("tauri_rpm_update") .tempdir_in(path) { - let deb_path = tmp_dir.path().join("package.deb"); + let pkg_path = tmp_dir.path().join("package.rpm"); // Try writing the .deb file - if std::fs::write(&deb_path, bytes).is_ok() { + if std::fs::write(&pkg_path, bytes).is_ok() { // If write succeeds, proceed with installation - return self.try_install_with_privileges(&deb_path); + return self.try_install_with_privileges( + &pkg_path, + install_cmd, + install_arg, + ); } // If write fails, continue to next temp location } @@ -1003,12 +1057,17 @@ impl Update { Err(Error::TempDirNotFound) } - fn try_install_with_privileges(&self, deb_path: &Path) -> Result<()> { + fn try_install_with_privileges( + &self, + pkg_path: &Path, + install_cmd: &str, + install_arg: &str, + ) -> Result<()> { // 1. First try using pkexec (graphical sudo prompt) if let Ok(status) = std::process::Command::new("pkexec") - .arg("dpkg") - .arg("-i") - .arg(deb_path) + .arg(install_cmd) + .arg(install_arg) + .arg(pkg_path) .status() { if status.success() { @@ -1019,7 +1078,7 @@ impl Update { // 2. Try zenity or kdialog for a graphical sudo experience if let Ok(password) = self.get_password_graphically() { - if self.install_with_sudo(deb_path, &password)? { + if self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? { log::debug!("installed deb with GUI sudo"); return Ok(()); } @@ -1027,16 +1086,16 @@ impl Update { // 3. Final fallback: terminal sudo let status = std::process::Command::new("sudo") - .arg("dpkg") - .arg("-i") - .arg(deb_path) + .arg(install_cmd) + .arg(install_arg) + .arg(pkg_path) .status()?; if status.success() { log::debug!("installed deb with sudo"); Ok(()) } else { - Err(Error::DebInstallFailed) + Err(Error::PackageInstallFailed) } } @@ -1070,15 +1129,21 @@ impl Update { Err(Error::AuthenticationFailed) } - fn install_with_sudo(&self, deb_path: &Path, password: &str) -> Result { + fn install_with_sudo( + &self, + pkg_path: &Path, + password: &str, + install_cmd: &str, + install_arg: &str, + ) -> Result { use std::io::Write; use std::process::{Command, Stdio}; let mut child = Command::new("sudo") .arg("-S") // read password from stdin - .arg("dpkg") - .arg("-i") - .arg(deb_path) + .arg(install_cmd) + .arg(install_arg) + .arg(pkg_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -1086,7 +1151,7 @@ impl Update { if let Some(mut stdin) = child.stdin.take() { // Write password to stdin - writeln!(stdin, "{}", password)?; + writeln!(stdin, "{password}")?; } let status = child.wait()?; diff --git a/plugins/updater/tests/app-updater/src/main.rs b/plugins/updater/tests/app-updater/src/main.rs index bbe398b5..c20bdcf3 100644 --- a/plugins/updater/tests/app-updater/src/main.rs +++ b/plugins/updater/tests/app-updater/src/main.rs @@ -1,57 +1,56 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -use tauri_plugin_updater::UpdaterExt; - -fn main() { - #[allow(unused_mut)] - let mut context = tauri::generate_context!(); - - tauri::Builder::default() - .plugin(tauri_plugin_updater::Builder::new().build()) - .setup(|app| { - let handle = app.handle().clone(); - tauri::async_runtime::spawn(async move { - let mut builder = handle.updater_builder(); - if std::env::var("TARGET").unwrap_or_default() == "nsis" { - // /D sets the default installation directory ($INSTDIR), - // overriding InstallDir and InstallDirRegKey. - // It must be the last parameter used in the command line and must not contain any quotes, even if the path contains spaces. - // Only absolute paths are supported. - // NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder - builder = builder.installer_args(vec![format!( - "/D={}", - tauri::utils::platform::current_exe() - .unwrap() - .parent() - .unwrap() - .display() - )]); - } - let updater = builder.build().unwrap(); - - match updater.check().await { - Ok(Some(update)) => { - if let Err(e) = update.download_and_install(|_, _| {}, || {}).await { - println!("{e}"); - std::process::exit(1); - } - std::process::exit(0); - } - Ok(None) => { - std::process::exit(2); - } - Err(e) => { - println!("{e}"); - std::process::exit(1); - } - } - }); - Ok(()) - }) - .run(context) - .expect("error while running tauri application"); -} +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use tauri_plugin_updater::UpdaterExt; + +fn main() { + #[allow(unused_mut)] + let mut context = tauri::generate_context!(); + tauri::Builder::default() + .plugin(tauri_plugin_updater::Builder::new().build()) + .setup(|app| { + let handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + let mut builder = handle.updater_builder(); + if std::env::var("TARGET").unwrap_or_default() == "nsis" { + // /D sets the default installation directory ($INSTDIR), + // overriding InstallDir and InstallDirRegKey. + // It must be the last parameter used in the command line and must not contain any quotes, even if the path contains spaces. + // Only absolute paths are supported. + // NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder + builder = builder.installer_args(vec![format!( + "/D={}", + tauri::utils::platform::current_exe() + .unwrap() + .parent() + .unwrap() + .display() + )]); + } + let updater = builder.build().unwrap(); + + match updater.check().await { + Ok(Some(update)) => { + if let Err(e) = update.download_and_install(|_, _| {}, || {}).await { + println!("{e}"); + std::process::exit(1); + } + std::process::exit(0); + } + Ok(None) => { + std::process::exit(2); + } + Err(e) => { + println!("{e}"); + std::process::exit(1); + } + } + }); + Ok(()) + }) + .run(context) + .expect("error while running tauri application"); +} diff --git a/plugins/updater/tests/app-updater/tauri.conf.json b/plugins/updater/tests/app-updater/tauri.conf.json index f2c6df21..fc70993e 100644 --- a/plugins/updater/tests/app-updater/tauri.conf.json +++ b/plugins/updater/tests/app-updater/tauri.conf.json @@ -2,6 +2,7 @@ "identifier": "com.tauri.updater", "plugins": { "updater": { + "dangerousInsecureTransportProtocol": true, "endpoints": ["http://localhost:3007"], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUwNDRGMjkwRjg2MDhCRDAKUldUUWkyRDRrUEpFNEQ4SmdwcU5PaXl6R2ZRUUNvUnhIaVkwVUltV0NMaEx6VTkrWVhpT0ZqeEEK", "windows": { diff --git a/plugins/updater/tests/app-updater/tests/update.rs b/plugins/updater/tests/app-updater/tests/update.rs index d308b317..8087c1e9 100644 --- a/plugins/updater/tests/app-updater/tests/update.rs +++ b/plugins/updater/tests/app-updater/tests/update.rs @@ -17,6 +17,7 @@ use tauri::utils::config::{Updater, V1Compatible}; const UPDATER_PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5TlFOMFpXYzJFOUdjeHJEVXY4WE1TMUxGNDJVUjNrMmk1WlR3UVJVUWwva0FBQkFBQUFBQUFBQUFBQUlBQUFBQUpVK3ZkM3R3eWhyN3hiUXhQb2hvWFVzUW9FbEs3NlNWYjVkK1F2VGFRU1FEaGxuRUtlell5U0gxYS9DbVRrS0YyZVJGblhjeXJibmpZeGJjS0ZKSUYwYndYc2FCNXpHalM3MHcrODMwN3kwUG9SOWpFNVhCSUd6L0E4TGRUT096TEtLR1JwT1JEVFU9Cg=="; const UPDATED_EXIT_CODE: i32 = 0; +const ERROR_EXIT_CODE: i32 = 1; const UP_TO_DATE_EXIT_CODE: i32 = 2; #[derive(Serialize)] @@ -48,7 +49,7 @@ struct Update { fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTarget) { let mut command = Command::new("cargo"); command - .args(["tauri", "build", "--debug", "--verbose"]) + .args(["tauri", "build", "--verbose"]) .arg("--config") .arg(serde_json::to_string(config).unwrap()) .env("TAURI_SIGNING_PRIVATE_KEY", UPDATER_PRIVATE_KEY) @@ -80,6 +81,8 @@ fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTa #[derive(Copy, Clone)] enum BundleTarget { AppImage, + Deb, + Rpm, App, @@ -91,6 +94,8 @@ impl BundleTarget { fn name(self) -> &'static str { match self { Self::AppImage => "appimage", + Self::Deb => "deb", + Self::Rpm => "rpm", Self::App => "app", Self::Msi => "msi", Self::Nsis => "nsis", @@ -98,7 +103,7 @@ impl BundleTarget { } } -impl Default for BundleTarget { +impl BundleTarget { fn default() -> Self { #[cfg(any(target_os = "macos", target_os = "ios"))] return Self::App; @@ -109,57 +114,168 @@ impl Default for BundleTarget { } } +fn target_to_platforms( + update_platform: Option, + signature: String, +) -> HashMap { + let mut platforms = HashMap::new(); + if let Some(platform) = update_platform { + platforms.insert( + platform, + PlatformUpdate { + signature, + url: "http://localhost:3007/download", + with_elevated_task: false, + }, + ); + } + + platforms +} + #[cfg(target_os = "linux")] -fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> { - vec![( - BundleTarget::AppImage, - root_dir.join(format!( - "target/debug/bundle/appimage/app-updater_{version}_amd64.AppImage" - )), - )] +fn test_cases( + root_dir: &Path, + version: &str, + target: String, +) -> Vec<(BundleTarget, PathBuf, Option, Vec)> { + vec![ + // update using fallback + ( + BundleTarget::AppImage, + root_dir.join(format!( + "target/release/bundle/appimage/app-updater_{version}_amd64.AppImage" + )), + Some(target.clone()), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], + ), + // update using full name + ( + BundleTarget::AppImage, + root_dir.join(format!( + "target/release/bundle/appimage/app-updater_{version}_amd64.AppImage" + )), + Some(format!("{target}-{}", BundleTarget::AppImage.name())), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], + ), + // no update + ( + BundleTarget::AppImage, + root_dir.join(format!( + "target/release/bundle/appimage/app-updater_{version}_amd64.AppImage" + )), + None, + vec![ERROR_EXIT_CODE], + ), + ] } #[cfg(target_os = "macos")] -fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> { - vec![( - BundleTarget::App, - root_dir.join("target/debug/bundle/macos/app-updater.app"), - )] +fn test_cases( + root_dir: &Path, + _version: &str, + target: String, +) -> Vec<(BundleTarget, PathBuf, Option, Vec)> { + vec![ + ( + BundleTarget::App, + root_dir.join("target/release/bundle/macos/app-updater.app"), + Some(target.clone()), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], + ), + // update with installer + ( + BundleTarget::App, + root_dir.join("target/release/bundle/macos/app-updater.app"), + Some(format!("{target}-{}", BundleTarget::App.name())), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], + ), + // no update + ( + BundleTarget::App, + root_dir.join("target/release/bundle/macos/app-updater.app"), + None, + vec![ERROR_EXIT_CODE], + ), + ] } #[cfg(target_os = "ios")] -fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> { +fn bundle_paths( + root_dir: &Path, + _version: &str, + v1compatible: bool, +) -> Vec<(BundleTarget, PathBuf)> { vec![( BundleTarget::App, - root_dir.join("target/debug/bundle/ios/app-updater.ipa"), + root_dir.join("target/release/bundle/ios/app-updater.ipa"), )] } #[cfg(target_os = "android")] -fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf { - root_dir.join("target/debug/bundle/android/app-updater.apk") +fn bundle_path(root_dir: &Path, _version: &str, v1compatible: bool) -> PathBuf { + root_dir.join("target/release/bundle/android/app-updater.apk") } #[cfg(windows)] -fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> { +fn test_cases( + root_dir: &Path, + version: &str, + target: String, +) -> Vec<(BundleTarget, PathBuf, Option, Vec)> { vec![ ( BundleTarget::Nsis, root_dir.join(format!( - "target/debug/bundle/nsis/app-updater_{version}_x64-setup.exe" + "target/release/bundle/nsis/app-updater_{version}_x64-setup.exe" )), + Some(target.clone()), + vec![UPDATED_EXIT_CODE], + ), + ( + BundleTarget::Nsis, + root_dir.join(format!( + "target/release/bundle/nsis/app-updater_{version}_x64-setup.exe" + )), + Some(format!("{target}-{}", BundleTarget::Nsis.name())), + vec![UPDATED_EXIT_CODE], + ), + ( + BundleTarget::Nsis, + root_dir.join(format!( + "target/release/bundle/nsis/app-updater_{version}_x64-setup.exe" + )), + None, + vec![ERROR_EXIT_CODE], ), ( BundleTarget::Msi, root_dir.join(format!( - "target/debug/bundle/msi/app-updater_{version}_x64_en-US.msi" + "target/release/bundle/msi/app-updater_{version}_x64_en-US.msi" )), + Some(target.clone()), + vec![UPDATED_EXIT_CODE], + ), + ( + BundleTarget::Msi, + root_dir.join(format!( + "target/release/bundle/msi/app-updater_{version}_x64_en-US.msi" + )), + Some(format!("{target}-{}", BundleTarget::Msi.name())), + vec![UPDATED_EXIT_CODE], + ), + ( + BundleTarget::Msi, + root_dir.join(format!( + "target/release/bundle/msi/app-updater_{version}_x64_en-US.msi" + )), + None, + vec![ERROR_EXIT_CODE], ), ] } #[test] -#[ignore] fn update_app() { let target = tauri_plugin_updater::target().expect("running updater test in an unsupported platform"); @@ -185,9 +301,6 @@ fn update_app() { Updater::String(V1Compatible::V1Compatible) ); - // bundle app update - build_app(&manifest_dir, &config, true, Default::default()); - let updater_zip_ext = if v1_compatible { if cfg!(windows) { Some("zip") @@ -200,7 +313,13 @@ fn update_app() { None }; - for (bundle_target, out_bundle_path) in bundle_paths(&root_dir, "1.0.0") { + for (bundle_target, out_bundle_path, update_platform, status_checks) in + test_cases(&root_dir, "1.0.0", target.clone()) + { + // bundle app update + config.version = "1.0.0"; + build_app(&manifest_dir, &config, true, BundleTarget::default()); + let bundle_updater_ext = if v1_compatible { out_bundle_path .extension() @@ -228,13 +347,11 @@ fn update_app() { }); let out_updater_path = out_bundle_path.with_extension(updater_extension); let updater_path = root_dir.join(format!( - "target/debug/{}", + "target/release/{}", out_updater_path.file_name().unwrap().to_str().unwrap() )); std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle"); - let target = target.clone(); - // start the updater server let server = Arc::new( tiny_http::Server::http("localhost:3007").expect("failed to start updater server"), @@ -245,16 +362,9 @@ fn update_app() { for request in server_.incoming_requests() { match request.url() { "/" => { - let mut platforms = HashMap::new(); - - platforms.insert( - target.clone(), - PlatformUpdate { - signature: signature.clone(), - url: "http://localhost:3007/download", - with_elevated_task: false, - }, - ); + let platforms = + target_to_platforms(update_platform.clone(), signature.clone()); + let body = serde_json::to_vec(&Update { version: "1.0.0", date: time::OffsetDateTime::now_utc() @@ -293,19 +403,12 @@ fn update_app() { // bundle initial app version build_app(&manifest_dir, &config, false, bundle_target); - let status_checks = if matches!(bundle_target, BundleTarget::Msi) { - // for msi we can't really check if the app was updated, because we can't change the install path - vec![UPDATED_EXIT_CODE] - } else { - vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE] - }; - for expected_exit_code in status_checks { let mut binary_cmd = if cfg!(windows) { - Command::new(root_dir.join("target/debug/app-updater.exe")) + Command::new(root_dir.join("target/release/app-updater.exe")) } else if cfg!(target_os = "macos") { Command::new( - bundle_paths(&root_dir, "0.1.0") + test_cases(&root_dir, "0.1.0", target.clone()) .first() .unwrap() .1 @@ -313,11 +416,20 @@ fn update_app() { ) } else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() { let mut c = Command::new("xvfb-run"); - c.arg("--auto-servernum") - .arg(&bundle_paths(&root_dir, "0.1.0").first().unwrap().1); + c.arg("--auto-servernum").arg( + &test_cases(&root_dir, "0.1.0", target.clone()) + .first() + .unwrap() + .1, + ); c } else { - Command::new(&bundle_paths(&root_dir, "0.1.0").first().unwrap().1) + Command::new( + &test_cases(&root_dir, "0.1.0", target.clone()) + .first() + .unwrap() + .1, + ) }; binary_cmd.env("TARGET", bundle_target.name()); @@ -327,7 +439,7 @@ fn update_app() { if code != expected_exit_code { panic!( - "failed to run app, expected exit code {expected_exit_code}, got {code}" + "failed to run app bundled as {}, expected exit code {expected_exit_code}, got {code}", bundle_target.name() ); } #[cfg(windows)]