diff --git a/.changes/fix-macos-user-install-update.md b/.changes/fix-macos-user-install-update.md new file mode 100644 index 00000000..e54982cd --- /dev/null +++ b/.changes/fix-macos-user-install-update.md @@ -0,0 +1,6 @@ +--- +"updater": patch +"updater-js": patch +--- + +Fix update installation on macOS when using an user without admin privileges. diff --git a/.changes/updater-new-fn.md b/.changes/updater-new-fn.md new file mode 100644 index 00000000..73797271 --- /dev/null +++ b/.changes/updater-new-fn.md @@ -0,0 +1,6 @@ +--- +"updater": minor +"updater-js": minor +--- + +Remove the `UpdaterBuilder::new` function, use `UpdaterExt::updater_builder` instead. \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 643f85ef..7253bf76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,6 +685,25 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "block2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f" +dependencies = [ + "block-sys", + "objc2", +] + [[package]] name = "block2" version = "0.5.1" @@ -2851,6 +2870,16 @@ dependencies = [ "png", ] +[[package]] +name = "icrate" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642" +dependencies = [ + "block2 0.4.0", + "objc2", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -3941,7 +3970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.7.0", - "block2", + "block2 0.5.1", "libc", "objc2", "objc2-core-data", @@ -3957,7 +3986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.7.0", - "block2", + "block2 0.5.1", "objc2", "objc2-core-location", "objc2-foundation", @@ -3969,7 +3998,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", + "block2 0.5.1", "objc2", "objc2-foundation", ] @@ -3981,7 +4010,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.7.0", - "block2", + "block2 0.5.1", "objc2", "objc2-foundation", ] @@ -3992,7 +4021,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", + "block2 0.5.1", "objc2", "objc2-foundation", "objc2-metal", @@ -4004,7 +4033,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", + "block2 0.5.1", "objc2", "objc2-contacts", "objc2-foundation", @@ -4023,7 +4052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.7.0", - "block2", + "block2 0.5.1", "dispatch", "libc", "objc2", @@ -4035,7 +4064,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", + "block2 0.5.1", "objc2", "objc2-app-kit", "objc2-foundation", @@ -4048,11 +4077,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.7.0", - "block2", + "block2 0.5.1", "objc2", "objc2-foundation", ] +[[package]] +name = "objc2-osa-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6788b04a18ea31e3dc3ab256b8546639e5bbae07c1a0dc4ea8615252bc6aee9a" +dependencies = [ + "bitflags 2.7.0", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -4060,7 +4101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.7.0", - "block2", + "block2 0.5.1", "objc2", "objc2-foundation", "objc2-metal", @@ -4083,7 +4124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.7.0", - "block2", + "block2 0.5.1", "objc2", "objc2-cloud-kit", "objc2-core-data", @@ -4103,7 +4144,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", + "block2 0.5.1", "objc2", "objc2-foundation", ] @@ -4115,7 +4156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.7.0", - "block2", + "block2 0.5.1", "objc2", "objc2-core-location", "objc2-foundation", @@ -4128,7 +4169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68bc69301064cebefc6c4c90ce9cba69225239e4b8ff99d445a2b5563797da65" dependencies = [ "bitflags 2.7.0", - "block2", + "block2 0.5.1", "objc2", "objc2-app-kit", "objc2-foundation", @@ -4276,6 +4317,20 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "osakit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35366a452fce3f8947eb2f33226a133aaf0cacedef2af67ade348d58be7f85d0" +dependencies = [ + "icrate", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "pango" version = "0.18.3" @@ -5080,7 +5135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46f6f80a9b882647d9014673ca9925d30ffc9750f2eed2b4490e189eaebd01e8" dependencies = [ "ashpd", - "block2", + "block2 0.5.1", "glib-sys", "gobject-sys", "gtk-sys", @@ -6894,6 +6949,7 @@ dependencies = [ "http", "infer", "minisign-verify", + "osakit", "percent-encoding", "reqwest", "semver", @@ -8693,7 +8749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e644bf458e27b11b0ecafc9e5633d1304fdae82baca1d42185669752fe6ca4f" dependencies = [ "base64 0.22.1", - "block2", + "block2 0.5.1", "cookie", "crossbeam-channel", "dpi", diff --git a/plugins/updater/Cargo.toml b/plugins/updater/Cargo.toml index 31ae3166..ccb079a5 100644 --- a/plugins/updater/Cargo.toml +++ b/plugins/updater/Cargo.toml @@ -62,6 +62,7 @@ flate2 = { version = "1", optional = true } [target."cfg(target_os = \"macos\")".dependencies] tar = "0.4" flate2 = "1" +osakit = { version = "0.3", features = ["full"] } [features] default = ["rustls-tls", "zip"] diff --git a/plugins/updater/src/lib.rs b/plugins/updater/src/lib.rs index b2b5c014..75b014bc 100644 --- a/plugins/updater/src/lib.rs +++ b/plugins/updater/src/lib.rs @@ -70,7 +70,6 @@ pub trait UpdaterExt { impl> UpdaterExt for T { fn updater_builder(&self) -> UpdaterBuilder { let app = self.app_handle(); - let package_info = app.package_info(); let UpdaterState { config, target, @@ -78,12 +77,7 @@ impl> UpdaterExt for T { headers, } = self.state::().inner(); - let mut builder = UpdaterBuilder::new( - package_info.name.clone(), - package_info.version.clone(), - config.clone(), - ) - .headers(headers.clone()); + let mut builder = UpdaterBuilder::new(app, config.clone()).headers(headers.clone()); if let Some(target) = target { builder = builder.target(target); diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index e93f093a..f9209a00 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -23,7 +23,7 @@ use reqwest::{ }; use semver::Version; use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize}; -use tauri::{utils::platform::current_exe, Resource}; +use tauri::{utils::platform::current_exe, AppHandle, Resource, Runtime}; use time::OffsetDateTime; use url::Url; @@ -94,8 +94,13 @@ impl RemoteRelease { pub type OnBeforeExit = Arc; pub type VersionComparator = Arc bool + Send + Sync>; +type MainThreadClosure = Box; +type RunOnMainThread = + Box std::result::Result<(), tauri::Error> + Send + Sync + 'static>; pub struct UpdaterBuilder { + #[allow(dead_code)] + run_on_main_thread: RunOnMainThread, app_name: String, current_version: Version, config: Config, @@ -112,18 +117,20 @@ pub struct UpdaterBuilder { } impl UpdaterBuilder { - /// It's prefered to use [`crate::UpdaterExt::updater_builder`] instead of - /// constructing a [`UpdaterBuilder`] with this function yourself - pub fn new(app_name: String, current_version: Version, config: crate::Config) -> Self { + pub(crate) fn new(app: &AppHandle, config: crate::Config) -> Self { + let app_ = app.clone(); + let run_on_main_thread = + move |f: Box| app_.run_on_main_thread(f); Self { + run_on_main_thread: Box::new(run_on_main_thread), installer_args: config .windows .as_ref() .map(|w| w.installer_args.clone()) .unwrap_or_default(), current_exe_args: Vec::new(), - app_name, - current_version, + app_name: app.package_info().name.clone(), + current_version: app.package_info().version.clone(), config, version_comparator: None, executable_path: None, @@ -259,6 +266,7 @@ impl UpdaterBuilder { }; Ok(Updater { + run_on_main_thread: Arc::new(self.run_on_main_thread), config: self.config, app_name: self.app_name, current_version: self.current_version, @@ -291,6 +299,8 @@ impl UpdaterBuilder { } pub struct Updater { + #[allow(dead_code)] + run_on_main_thread: Arc, config: Config, app_name: String, current_version: Version, @@ -412,6 +422,7 @@ impl Updater { let update = if should_update { Some(Update { + run_on_main_thread: self.run_on_main_thread.clone(), config: self.config.clone(), on_before_exit: self.on_before_exit.clone(), app_name: self.app_name.clone(), @@ -440,6 +451,8 @@ impl Updater { #[derive(Clone)] pub struct Update { + #[allow(dead_code)] + run_on_main_thread: Arc, config: Config, #[allow(unused)] on_before_exit: Option, @@ -1031,42 +1044,85 @@ impl Update { let cursor = Cursor::new(bytes); let mut extracted_files: Vec = Vec::new(); - // the first file in the tar.gz will always be - // /Contents - let tmp_dir = tempfile::Builder::new() + // Create temp directories for backup and extraction + let tmp_backup_dir = tempfile::Builder::new() .prefix("tauri_current_app") .tempdir()?; - // create backup of our current app - std::fs::rename(&self.extract_path, tmp_dir.path())?; + let tmp_extract_dir = tempfile::Builder::new() + .prefix("tauri_updated_app") + .tempdir()?; let decoder = GzDecoder::new(cursor); let mut archive = tar::Archive::new(decoder); - std::fs::create_dir(&self.extract_path)?; - + // Extract files to temporary directory for entry in archive.entries()? { let mut entry = entry?; - - // skip the first folder (should be the app name) let collected_path: PathBuf = entry.path()?.iter().skip(1).collect(); - let extraction_path = &self.extract_path.join(collected_path); - - // if something went wrong during the extraction, we should restore previous app - if let Err(err) = entry.unpack(extraction_path) { - for file in extracted_files.iter().rev() { - // delete all the files we extracted - if file.is_dir() { - std::fs::remove_dir(file)?; - } else { - std::fs::remove_file(file)?; - } - } - std::fs::rename(tmp_dir.path(), &self.extract_path)?; + let extraction_path = tmp_extract_dir.path().join(&collected_path); + + // Ensure parent directories exist + if let Some(parent) = extraction_path.parent() { + std::fs::create_dir_all(parent)?; + } + + if let Err(err) = entry.unpack(&extraction_path) { + // Cleanup on error + std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); + return Err(err.into()); + } + extracted_files.push(extraction_path); + } + + // Try to move the current app to backup + let move_result = std::fs::rename( + &self.extract_path, + tmp_backup_dir.path().join("current_app"), + ); + let need_authorization = if let Err(err) = move_result { + if err.kind() == std::io::ErrorKind::PermissionDenied { + true + } else { + std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); return Err(err.into()); } + } else { + false + }; - extracted_files.push(extraction_path.to_path_buf()); + if need_authorization { + // Use AppleScript to perform moves with admin privileges + let apple_script = format!( + "do shell script \"rm -rf '{src}' && mv -f '{new}' '{src}'\" with administrator privileges", + src = self.extract_path.display(), + new = tmp_extract_dir.path().display() + ); + + let (tx, rx) = std::sync::mpsc::channel(); + let res = (self.run_on_main_thread)(Box::new(move || { + let mut script = + osakit::Script::new_from_source(osakit::Language::AppleScript, &apple_script); + script.compile().expect("invalid AppleScript"); + let r = script.execute(); + tx.send(r).unwrap(); + })); + let result = rx.recv().unwrap(); + + if res.is_err() || result.is_err() { + std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Failed to move the new app into place", + ))); + } + } else { + // Remove existing directory if it exists + if self.extract_path.exists() { + std::fs::remove_dir_all(&self.extract_path)?; + } + // Move the new app to the target path + std::fs::rename(tmp_extract_dir.path(), &self.extract_path)?; } let _ = std::process::Command::new("touch")