pull/2624/merge
kandrelczyk 1 day ago committed by GitHub
commit 1e9c1cc89a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

2
.gitignore vendored

@ -58,4 +58,4 @@ pids
.idea .idea
debug.log debug.log
TODO.md TODO.md
.aider* .aider.*

159
Cargo.lock generated

@ -213,7 +213,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-plugin-barcode-scanner", "tauri-plugin-barcode-scanner",
"tauri-plugin-biometric", "tauri-plugin-biometric",
"tauri-plugin-cli", "tauri-plugin-cli",
@ -245,7 +245,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-plugin-updater", "tauri-plugin-updater",
"time", "time",
"tiny_http", "tiny_http",
@ -258,7 +258,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-plugin-updater", "tauri-plugin-updater",
"tiny_http", "tiny_http",
] ]
@ -270,7 +270,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-plugin-store", "tauri-plugin-store",
] ]
@ -1428,7 +1428,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-plugin-deep-link", "tauri-plugin-deep-link",
"tauri-plugin-log", "tauri-plugin-log",
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
@ -3615,9 +3615,9 @@ dependencies = [
[[package]] [[package]]
name = "muda" name = "muda"
version = "0.16.1" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492" checksum = "58b89bf91c19bf036347f1ab85a81c560f08c0667c8601bece664d860a600988"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"dpi", "dpi",
@ -5729,7 +5729,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-plugin-cli", "tauri-plugin-cli",
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
] ]
@ -6348,16 +6348,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.6.0" version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1#232265c70e1c213bbb3f84b5541ddc07d330fce1"
checksum = "2f7a0f4019c80391d143ee26cd7cd1ed271ac241d3087d333f99f3269ba90812"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
"dirs 6.0.0", "dirs 6.0.0",
"dunce", "dunce",
"embed_plist", "embed_plist",
"getrandom 0.2.15", "getrandom 0.3.2",
"glob", "glob",
"gtk", "gtk",
"heck 0.5.0", "heck 0.5.0",
@ -6383,11 +6382,11 @@ dependencies = [
"serialize-to-javascript", "serialize-to-javascript",
"specta", "specta",
"swift-rs", "swift-rs",
"tauri-build", "tauri-build 2.3.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)",
"tauri-macros", "tauri-macros",
"tauri-runtime", "tauri-runtime",
"tauri-runtime-wry", "tauri-runtime-wry",
"tauri-utils", "tauri-utils 2.5.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"tray-icon", "tray-icon",
@ -6417,8 +6416,29 @@ dependencies = [
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
"tauri-codegen", "tauri-codegen 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-utils", "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", "tauri-winres",
"toml", "toml",
"walkdir", "walkdir",
@ -6442,7 +6462,7 @@ dependencies = [
"serde_json", "serde_json",
"sha2", "sha2",
"syn 2.0.100", "syn 2.0.100",
"tauri-utils", "tauri-utils 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"thiserror 2.0.12", "thiserror 2.0.12",
"time", "time",
"url", "url",
@ -6451,17 +6471,41 @@ dependencies = [
] ]
[[package]] [[package]]
name = "tauri-macros" name = "tauri-codegen"
version = "2.3.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1#232265c70e1c213bbb3f84b5541ddc07d330fce1"
checksum = "1f59e1d1fa9651212dcb890a0c66226d819b716490b0cf43c078514da3591705" 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 = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.100", "syn 2.0.100",
"tauri-codegen", "tauri-codegen 2.3.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)",
"tauri-utils", "tauri-utils 2.5.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)",
] ]
[[package]] [[package]]
@ -6476,7 +6520,7 @@ dependencies = [
"schemars", "schemars",
"serde", "serde",
"serde_json", "serde_json",
"tauri-utils", "tauri-utils 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"toml", "toml",
"walkdir", "walkdir",
] ]
@ -6554,7 +6598,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"tauri-utils", "tauri-utils 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"thiserror 2.0.12", "thiserror 2.0.12",
"tracing", "tracing",
"url", "url",
@ -6594,7 +6638,7 @@ dependencies = [
"serde_repr", "serde_repr",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"tauri-utils", "tauri-utils 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"thiserror 2.0.12", "thiserror 2.0.12",
"toml", "toml",
"url", "url",
@ -6915,7 +6959,7 @@ dependencies = [
"tokio", "tokio",
"url", "url",
"windows-sys 0.60.2", "windows-sys 0.60.2",
"zip 4.0.0", "zip 4.2.0",
] ]
[[package]] [[package]]
@ -6969,8 +7013,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.7.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1#232265c70e1c213bbb3f84b5541ddc07d330fce1"
checksum = "9e7bb73d1bceac06c20b3f755b2c8a2cb13b20b50083084a8cf3700daf397ba4"
dependencies = [ dependencies = [
"cookie", "cookie",
"dpi", "dpi",
@ -6982,7 +7025,7 @@ dependencies = [
"raw-window-handle", "raw-window-handle",
"serde", "serde",
"serde_json", "serde_json",
"tauri-utils", "tauri-utils 2.5.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)",
"thiserror 2.0.12", "thiserror 2.0.12",
"url", "url",
"windows 0.61.1", "windows 0.61.1",
@ -6990,9 +7033,8 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "2.7.0" version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1#232265c70e1c213bbb3f84b5541ddc07d330fce1"
checksum = "fe52ed0ef40fd7ad51a620ecb3018e32eba3040bb95025216a962a37f6f050c5"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
@ -7007,7 +7049,7 @@ dependencies = [
"softbuffer", "softbuffer",
"tao", "tao",
"tauri-runtime", "tauri-runtime",
"tauri-utils", "tauri-utils 2.5.0 (git+https://github.com/tauri-apps/tauri?rev=232265c70e1c213bbb3f84b5541ddc07d330fce1)",
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
@ -7055,6 +7097,45 @@ dependencies = [
"walkdir", "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]] [[package]]
name = "tauri-winres" name = "tauri-winres"
version = "0.3.0" version = "0.3.0"
@ -7462,9 +7543,9 @@ dependencies = [
[[package]] [[package]]
name = "tray-icon" name = "tray-icon"
version = "0.20.0" version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d433764348e7084bad2c5ea22c96c71b61b17afe3a11645710f533bd72b6a2b5" checksum = "2da75ec677957aa21f6e0b361df0daab972f13a5bee3606de0638fd4ee1c666a"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"dirs 6.0.0", "dirs 6.0.0",
@ -7653,7 +7734,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-plugin-updater", "tauri-plugin-updater",
"time", "time",
"tiny_http", "tiny_http",
@ -8069,7 +8150,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-plugin-websocket", "tauri-plugin-websocket",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
@ -9068,9 +9149,9 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "4.0.0" version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd" checksum = "95ab361742de920c5535880f89bbd611ee62002bf11341d16a5f057bb8ba6899"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
"crc32fast", "crc32fast",

@ -39,3 +39,6 @@ codegen-units = 1
lto = true lto = true
incremental = false incremental = false
opt-level = "s" opt-level = "s"
[patch.crates-io]
tauri = { git = "https://github.com/tauri-apps/tauri", rev = "232265c70e1c213bbb3f84b5541ddc07d330fce1" }

@ -39,9 +39,12 @@ pub enum Error {
/// `reqwest` crate errors. /// `reqwest` crate errors.
#[error(transparent)] #[error(transparent)]
Reqwest(#[from] reqwest::Error), Reqwest(#[from] reqwest::Error),
/// The platform was not found on the updater JSON response. /// The platform was not found in the updater JSON response.
#[error("the platform `{0}` was not found on the response `platforms` object")] #[error("the platform `{0}` was not found in the response `platforms` object")]
TargetNotFound(String), 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 /// Download failed
#[error("`{0}`")] #[error("`{0}`")]
Network(String), Network(String),
@ -67,8 +70,8 @@ pub enum Error {
TempDirNotFound, TempDirNotFound,
#[error("Authentication failed or was cancelled")] #[error("Authentication failed or was cancelled")]
AuthenticationFailed, AuthenticationFailed,
#[error("Failed to install .deb package")] #[error("Failed to install package")]
DebInstallFailed, PackageInstallFailed,
#[error("invalid updater binary format")] #[error("invalid updater binary format")]
InvalidUpdaterFormat, InvalidUpdaterFormat,
#[error(transparent)] #[error(transparent)]

@ -26,7 +26,13 @@ use reqwest::{
}; };
use semver::Version; use semver::Version;
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize}; 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 time::OffsetDateTime;
use url::Url; use url::Url;
@ -37,6 +43,31 @@ use crate::{
const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); 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)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ReleaseManifestPlatform { pub struct ReleaseManifestPlatform {
/// Download URL for the platform /// Download URL for the platform
@ -270,7 +301,8 @@ impl UpdaterBuilder {
(target.clone(), target) (target.clone(), target)
} else { } else {
let target = get_updater_target().ok_or(Error::UnsupportedOs)?; 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()?); let executable_path = self.executable_path.clone().unwrap_or(current_exe()?);
@ -327,7 +359,7 @@ pub struct Updater {
proxy: Option<Url>, proxy: Option<Url>,
endpoints: Vec<Url>, endpoints: Vec<Url>,
arch: &'static str, 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, target: String,
// The value we search if the updater server returns a JSON with the `platforms` object // The value we search if the updater server returns a JSON with the `platforms` object
json_target: String, json_target: String,
@ -342,6 +374,18 @@ pub struct Updater {
} }
impl Updater { impl Updater {
fn get_updater_installer(&self) -> Option<Installer> {
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<Option<Update>> { pub async fn check(&self) -> Result<Option<Update>> {
// we want JSON only // we want JSON only
let mut headers = self.headers.clone(); let mut headers = self.headers.clone();
@ -466,6 +510,31 @@ impl Updater {
None => release.version > self.current_version, 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 { let update = if should_update {
Some(Update { Some(Update {
run_on_main_thread: self.run_on_main_thread.clone(), run_on_main_thread: self.run_on_main_thread.clone(),
@ -477,9 +546,10 @@ impl Updater {
extract_path: self.extract_path.clone(), extract_path: self.extract_path.clone(),
version: release.version.to_string(), version: release.version.to_string(),
date: release.pub_date, date: release.pub_date,
download_url: release.download_url(&self.json_target)?.to_owned(), download_url: download_url?.to_owned(),
signature: release.signature(&self.json_target)?.to_owned(), body: release.notes.clone(),
body: release.notes, signature: signature?.to_owned(),
installer,
raw_json: raw_json.unwrap(), raw_json: raw_json.unwrap(),
timeout: None, timeout: None,
proxy: self.proxy.clone(), proxy: self.proxy.clone(),
@ -513,6 +583,8 @@ pub struct Update {
pub date: Option<OffsetDateTime>, pub date: Option<OffsetDateTime>,
/// Target /// Target
pub target: String, pub target: String,
/// Current installer
pub installer: Option<Installer>,
/// Download URL announced /// Download URL announced
pub download_url: Url, pub download_url: Url,
/// Signature announced /// Signature announced
@ -852,11 +924,10 @@ impl Update {
/// └── ... /// └── ...
/// ///
fn install_inner(&self, bytes: &[u8]) -> Result<()> { fn install_inner(&self, bytes: &[u8]) -> Result<()> {
if self.is_deb_package() { match self.installer {
self.install_deb(bytes) Some(Installer::Deb) => self.install_deb(bytes),
} else { Some(Installer::Rpm) => self.install_rpm(bytes),
// Handle AppImage or other formats _ => self.install_appimage(bytes),
self.install_appimage(bytes)
} }
} }
@ -933,39 +1004,6 @@ impl Update {
Err(Error::TempDirNotOnSameMountPoint) 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<()> { fn install_deb(&self, bytes: &[u8]) -> Result<()> {
// First verify the bytes are actually a .deb package // First verify the bytes are actually a .deb package
if !infer::archive::is_deb(bytes) { if !infer::archive::is_deb(bytes) {
@ -973,6 +1011,18 @@ impl Update {
return Err(Error::InvalidUpdaterFormat); 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 // Try different temp directories
let tmp_dir_locations = vec![ let tmp_dir_locations = vec![
Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>, Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
@ -984,15 +1034,19 @@ impl Update {
for tmp_dir_location in tmp_dir_locations { for tmp_dir_location in tmp_dir_locations {
if let Some(path) = tmp_dir_location() { if let Some(path) = tmp_dir_location() {
if let Ok(tmp_dir) = tempfile::Builder::new() if let Ok(tmp_dir) = tempfile::Builder::new()
.prefix("tauri_deb_update") .prefix("tauri_rpm_update")
.tempdir_in(path) .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 // 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 // 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 // If write fails, continue to next temp location
} }
@ -1003,12 +1057,17 @@ impl Update {
Err(Error::TempDirNotFound) 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) // 1. First try using pkexec (graphical sudo prompt)
if let Ok(status) = std::process::Command::new("pkexec") if let Ok(status) = std::process::Command::new("pkexec")
.arg("dpkg") .arg(install_cmd)
.arg("-i") .arg(install_arg)
.arg(deb_path) .arg(pkg_path)
.status() .status()
{ {
if status.success() { if status.success() {
@ -1019,7 +1078,7 @@ impl Update {
// 2. Try zenity or kdialog for a graphical sudo experience // 2. Try zenity or kdialog for a graphical sudo experience
if let Ok(password) = self.get_password_graphically() { 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"); log::debug!("installed deb with GUI sudo");
return Ok(()); return Ok(());
} }
@ -1027,16 +1086,16 @@ impl Update {
// 3. Final fallback: terminal sudo // 3. Final fallback: terminal sudo
let status = std::process::Command::new("sudo") let status = std::process::Command::new("sudo")
.arg("dpkg") .arg(install_cmd)
.arg("-i") .arg(install_arg)
.arg(deb_path) .arg(pkg_path)
.status()?; .status()?;
if status.success() { if status.success() {
log::debug!("installed deb with sudo"); log::debug!("installed deb with sudo");
Ok(()) Ok(())
} else { } else {
Err(Error::DebInstallFailed) Err(Error::PackageInstallFailed)
} }
} }
@ -1070,15 +1129,21 @@ impl Update {
Err(Error::AuthenticationFailed) Err(Error::AuthenticationFailed)
} }
fn install_with_sudo(&self, deb_path: &Path, password: &str) -> Result<bool> { fn install_with_sudo(
&self,
pkg_path: &Path,
password: &str,
install_cmd: &str,
install_arg: &str,
) -> Result<bool> {
use std::io::Write; use std::io::Write;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
let mut child = Command::new("sudo") let mut child = Command::new("sudo")
.arg("-S") // read password from stdin .arg("-S") // read password from stdin
.arg("dpkg") .arg(install_cmd)
.arg("-i") .arg(install_arg)
.arg(deb_path) .arg(pkg_path)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
@ -1086,7 +1151,7 @@ impl Update {
if let Some(mut stdin) = child.stdin.take() { if let Some(mut stdin) = child.stdin.take() {
// Write password to stdin // Write password to stdin
writeln!(stdin, "{}", password)?; writeln!(stdin, "{password}")?;
} }
let status = child.wait()?; let status = child.wait()?;

@ -9,7 +9,6 @@ use tauri_plugin_updater::UpdaterExt;
fn main() { fn main() {
#[allow(unused_mut)] #[allow(unused_mut)]
let mut context = tauri::generate_context!(); let mut context = tauri::generate_context!();
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| { .setup(|app| {

@ -2,6 +2,7 @@
"identifier": "com.tauri.updater", "identifier": "com.tauri.updater",
"plugins": { "plugins": {
"updater": { "updater": {
"dangerousInsecureTransportProtocol": true,
"endpoints": ["http://localhost:3007"], "endpoints": ["http://localhost:3007"],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUwNDRGMjkwRjg2MDhCRDAKUldUUWkyRDRrUEpFNEQ4SmdwcU5PaXl6R2ZRUUNvUnhIaVkwVUltV0NMaEx6VTkrWVhpT0ZqeEEK", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUwNDRGMjkwRjg2MDhCRDAKUldUUWkyRDRrUEpFNEQ4SmdwcU5PaXl6R2ZRUUNvUnhIaVkwVUltV0NMaEx6VTkrWVhpT0ZqeEEK",
"windows": { "windows": {

@ -17,6 +17,7 @@ use tauri::utils::config::{Updater, V1Compatible};
const UPDATER_PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5TlFOMFpXYzJFOUdjeHJEVXY4WE1TMUxGNDJVUjNrMmk1WlR3UVJVUWwva0FBQkFBQUFBQUFBQUFBQUlBQUFBQUpVK3ZkM3R3eWhyN3hiUXhQb2hvWFVzUW9FbEs3NlNWYjVkK1F2VGFRU1FEaGxuRUtlell5U0gxYS9DbVRrS0YyZVJGblhjeXJibmpZeGJjS0ZKSUYwYndYc2FCNXpHalM3MHcrODMwN3kwUG9SOWpFNVhCSUd6L0E4TGRUT096TEtLR1JwT1JEVFU9Cg=="; const UPDATER_PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5TlFOMFpXYzJFOUdjeHJEVXY4WE1TMUxGNDJVUjNrMmk1WlR3UVJVUWwva0FBQkFBQUFBQUFBQUFBQUlBQUFBQUpVK3ZkM3R3eWhyN3hiUXhQb2hvWFVzUW9FbEs3NlNWYjVkK1F2VGFRU1FEaGxuRUtlell5U0gxYS9DbVRrS0YyZVJGblhjeXJibmpZeGJjS0ZKSUYwYndYc2FCNXpHalM3MHcrODMwN3kwUG9SOWpFNVhCSUd6L0E4TGRUT096TEtLR1JwT1JEVFU9Cg==";
const UPDATED_EXIT_CODE: i32 = 0; const UPDATED_EXIT_CODE: i32 = 0;
const ERROR_EXIT_CODE: i32 = 1;
const UP_TO_DATE_EXIT_CODE: i32 = 2; const UP_TO_DATE_EXIT_CODE: i32 = 2;
#[derive(Serialize)] #[derive(Serialize)]
@ -48,7 +49,7 @@ struct Update {
fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTarget) { fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTarget) {
let mut command = Command::new("cargo"); let mut command = Command::new("cargo");
command command
.args(["tauri", "build", "--debug", "--verbose"]) .args(["tauri", "build", "--verbose"])
.arg("--config") .arg("--config")
.arg(serde_json::to_string(config).unwrap()) .arg(serde_json::to_string(config).unwrap())
.env("TAURI_SIGNING_PRIVATE_KEY", UPDATER_PRIVATE_KEY) .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)] #[derive(Copy, Clone)]
enum BundleTarget { enum BundleTarget {
AppImage, AppImage,
Deb,
Rpm,
App, App,
@ -91,6 +94,8 @@ impl BundleTarget {
fn name(self) -> &'static str { fn name(self) -> &'static str {
match self { match self {
Self::AppImage => "appimage", Self::AppImage => "appimage",
Self::Deb => "deb",
Self::Rpm => "rpm",
Self::App => "app", Self::App => "app",
Self::Msi => "msi", Self::Msi => "msi",
Self::Nsis => "nsis", Self::Nsis => "nsis",
@ -98,7 +103,7 @@ impl BundleTarget {
} }
} }
impl Default for BundleTarget { impl BundleTarget {
fn default() -> Self { fn default() -> Self {
#[cfg(any(target_os = "macos", target_os = "ios"))] #[cfg(any(target_os = "macos", target_os = "ios"))]
return Self::App; return Self::App;
@ -109,57 +114,168 @@ impl Default for BundleTarget {
} }
} }
fn target_to_platforms(
update_platform: Option<String>,
signature: String,
) -> HashMap<String, PlatformUpdate> {
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")] #[cfg(target_os = "linux")]
fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> { fn test_cases(
vec![( root_dir: &Path,
BundleTarget::AppImage, version: &str,
root_dir.join(format!( target: String,
"target/debug/bundle/appimage/app-updater_{version}_amd64.AppImage" ) -> Vec<(BundleTarget, PathBuf, Option<String>, Vec<i32>)> {
)), 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")] #[cfg(target_os = "macos")]
fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> { fn test_cases(
vec![( root_dir: &Path,
BundleTarget::App, _version: &str,
root_dir.join("target/debug/bundle/macos/app-updater.app"), target: String,
)] ) -> Vec<(BundleTarget, PathBuf, Option<String>, Vec<i32>)> {
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")] #[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![( vec![(
BundleTarget::App, 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")] #[cfg(target_os = "android")]
fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf { fn bundle_path(root_dir: &Path, _version: &str, v1compatible: bool) -> PathBuf {
root_dir.join("target/debug/bundle/android/app-updater.apk") root_dir.join("target/release/bundle/android/app-updater.apk")
} }
#[cfg(windows)] #[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<String>, Vec<i32>)> {
vec![ vec![
( (
BundleTarget::Nsis, BundleTarget::Nsis,
root_dir.join(format!( 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, BundleTarget::Msi,
root_dir.join(format!( 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] #[test]
#[ignore]
fn update_app() { fn update_app() {
let target = let target =
tauri_plugin_updater::target().expect("running updater test in an unsupported platform"); tauri_plugin_updater::target().expect("running updater test in an unsupported platform");
@ -185,9 +301,6 @@ fn update_app() {
Updater::String(V1Compatible::V1Compatible) Updater::String(V1Compatible::V1Compatible)
); );
// bundle app update
build_app(&manifest_dir, &config, true, Default::default());
let updater_zip_ext = if v1_compatible { let updater_zip_ext = if v1_compatible {
if cfg!(windows) { if cfg!(windows) {
Some("zip") Some("zip")
@ -200,7 +313,13 @@ fn update_app() {
None 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 { let bundle_updater_ext = if v1_compatible {
out_bundle_path out_bundle_path
.extension() .extension()
@ -228,13 +347,11 @@ fn update_app() {
}); });
let out_updater_path = out_bundle_path.with_extension(updater_extension); let out_updater_path = out_bundle_path.with_extension(updater_extension);
let updater_path = root_dir.join(format!( let updater_path = root_dir.join(format!(
"target/debug/{}", "target/release/{}",
out_updater_path.file_name().unwrap().to_str().unwrap() out_updater_path.file_name().unwrap().to_str().unwrap()
)); ));
std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle"); std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle");
let target = target.clone();
// start the updater server // start the updater server
let server = Arc::new( let server = Arc::new(
tiny_http::Server::http("localhost:3007").expect("failed to start updater server"), 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() { for request in server_.incoming_requests() {
match request.url() { match request.url() {
"/" => { "/" => {
let mut platforms = HashMap::new(); let platforms =
target_to_platforms(update_platform.clone(), signature.clone());
platforms.insert(
target.clone(),
PlatformUpdate {
signature: signature.clone(),
url: "http://localhost:3007/download",
with_elevated_task: false,
},
);
let body = serde_json::to_vec(&Update { let body = serde_json::to_vec(&Update {
version: "1.0.0", version: "1.0.0",
date: time::OffsetDateTime::now_utc() date: time::OffsetDateTime::now_utc()
@ -293,19 +403,12 @@ fn update_app() {
// bundle initial app version // bundle initial app version
build_app(&manifest_dir, &config, false, bundle_target); 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 { for expected_exit_code in status_checks {
let mut binary_cmd = if cfg!(windows) { 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") { } else if cfg!(target_os = "macos") {
Command::new( Command::new(
bundle_paths(&root_dir, "0.1.0") test_cases(&root_dir, "0.1.0", target.clone())
.first() .first()
.unwrap() .unwrap()
.1 .1
@ -313,11 +416,20 @@ fn update_app() {
) )
} else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() { } else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() {
let mut c = Command::new("xvfb-run"); let mut c = Command::new("xvfb-run");
c.arg("--auto-servernum") c.arg("--auto-servernum").arg(
.arg(&bundle_paths(&root_dir, "0.1.0").first().unwrap().1); &test_cases(&root_dir, "0.1.0", target.clone())
.first()
.unwrap()
.1,
);
c c
} else { } 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()); binary_cmd.env("TARGET", bundle_target.name());
@ -327,7 +439,7 @@ fn update_app() {
if code != expected_exit_code { if code != expected_exit_code {
panic!( 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)] #[cfg(windows)]

Loading…
Cancel
Save