From 6ae53cfeaeab684c4f21ea880bfc41cbf0566c8e Mon Sep 17 00:00:00 2001 From: Krzysztof Andrelczyk Date: Sat, 1 Mar 2025 18:03:10 +0100 Subject: [PATCH] linux test --- plugins/updater/src/updater.rs | 144 +++++++++++------- .../updater/tests/app-updater/tests/update.rs | 143 ++++++++--------- 2 files changed, 163 insertions(+), 124 deletions(-) diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index de4cd40c..dacb15af 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -34,6 +34,31 @@ use crate::{ const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); +#[derive(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 @@ -68,28 +93,32 @@ pub struct RemoteRelease { impl RemoteRelease { /// The release's download URL for the given target. - pub fn download_url(&self, target: &str, fallback_target: &Option) -> Result<&Url> { + pub fn download_url(&self, target: &str, installer: Option) -> Result<&Url> { + + let fallback_target = installer.map(|installer| format!("{target}-{}", installer.suffix())); match self.data { RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.url), RemoteReleaseInner::Static { ref platforms } => platforms .get(target) .map_or_else( || match fallback_target { - Some(fallback) => platforms.get(fallback).map_or(Err(Error::TargetsNotFound(target.to_string(), fallback.to_string())), |p| Ok(&p.url)), + Some(fallback) => platforms.get(&fallback).map_or(Err(Error::TargetsNotFound(target.to_string(), fallback)), |p| Ok(&p.url)), None => Err(Error::TargetNotFound(target.to_string())) }, |p| { Ok(&p.url) }) } } /// The release's signature for the given target. - pub fn signature(&self, target: &str, fallback_target: &Option) -> Result<&String> { + pub fn signature(&self, target: &str, installer: Option) -> Result<&String> { + let fallback_target = installer.map(|installer| format!("{target}-{}", installer.suffix())); + match self.data { RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.signature), RemoteReleaseInner::Static { ref platforms } => platforms .get(target) .map_or_else( || match fallback_target { - Some(fallback) => platforms.get(fallback).map_or(Err(Error::TargetsNotFound(target.to_string(), fallback.to_string())), |p| Ok(&p.signature)), + Some(fallback) => platforms.get(&fallback).map_or(Err(Error::TargetsNotFound(target.to_string(), fallback)), |p| Ok(&p.signature)), None => Err(Error::TargetNotFound(target.to_string())) }, |p| { Ok(&p.signature) }) } @@ -246,16 +275,12 @@ impl UpdaterBuilder { }; let arch = get_updater_arch().ok_or(Error::UnsupportedArch)?; - let (target, json_target, fallback_target) = if let Some(target) = self.target { - (target.clone(), target, None) + let (target, json_target) = if let Some(target) = self.target { + (target.clone(), target ) } else { let target = get_updater_target().ok_or(Error::UnsupportedOs)?; - let installer = get_updater_installer()?; let json_target = format!("{target}-{arch}"); - match installer { - Some(installer) => (target.to_owned(), format!("{json_target}-{installer}"), Some(json_target)), - None => (target.to_owned(), json_target, None) - } + (target.to_owned(), json_target) }; let executable_path = self.executable_path.clone().unwrap_or(current_exe()?); @@ -280,7 +305,6 @@ impl UpdaterBuilder { arch, target, json_target, - fallback_target, headers: self.headers, extract_path, on_before_exit: self.on_before_exit, @@ -313,8 +337,6 @@ pub struct Updater { target: String, // The value we search if the updater server returns a JSON with the `platforms` object json_target: String, - // If target doesn't exist in the JSON check for this one - fallback_target: Option, headers: HeaderMap, extract_path: PathBuf, on_before_exit: Option, @@ -325,6 +347,7 @@ pub struct Updater { } impl Updater { + pub async fn check(&self) -> Result> { // we want JSON only let mut headers = self.headers.clone(); @@ -420,6 +443,8 @@ impl Updater { Some(comparator) => comparator(self.current_version.clone(), release.clone()), None => release.version > self.current_version, }; + + let installer = get_updater_installer(&self.extract_path)?; let update = if should_update { Some(Update { @@ -431,9 +456,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, &self.fallback_target)?.to_owned(), + download_url: release.download_url(&self.json_target, installer.clone())?.to_owned(), body: release.notes.clone(), - signature: release.signature(&self.json_target, &self.fallback_target)?.to_owned(), + signature: release.signature(&self.json_target, installer.clone())?.to_owned(), + installer, raw_json: raw_json.unwrap(), timeout: self.timeout, proxy: self.proxy.clone(), @@ -464,6 +490,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 @@ -791,11 +819,9 @@ 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), + _ =>self.install_appimage(bytes) } } @@ -870,38 +896,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 @@ -1125,11 +1119,49 @@ pub(crate) fn get_updater_arch() -> Option<&'static str> { } } -pub(crate) fn get_updater_installer() -> Result> { +fn is_deb_package(extract_path: &PathBuf) -> bool { + // First check if we're in a typical Debian installation path + let in_system_path = 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", &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 +} + +pub(crate) fn get_updater_installer(extract_path: &PathBuf) -> Result> { if cfg!(target_os = "linux") { - Ok(Some("deb")) + if std::env::var_os("APPIMAGE").is_some() { + Ok(Some(Installer::AppImage)) + } else if is_deb_package(extract_path) { + Ok(Some(Installer::Deb)) + } else { + Err(Error::UnknownInstaller) + } } else if cfg!(target_os = "windows") { - Ok(Some("wix")) + Ok(Some(Installer::Msi)) } else if cfg!(target_os = "macos") { Ok(None) } else { diff --git a/plugins/updater/tests/app-updater/tests/update.rs b/plugins/updater/tests/app-updater/tests/update.rs index bb248426..748b11d7 100644 --- a/plugins/updater/tests/app-updater/tests/update.rs +++ b/plugins/updater/tests/app-updater/tests/update.rs @@ -71,7 +71,6 @@ fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, targets: Vec, - target: String, +fn target_to_platforms( + update_platform: Option, signature: String, -) { - match bundle_target { - // Should use deb, no fallback - BundleTarget::Deb => { - platforms.insert( - format!("{target}-deb"), - PlatformUpdate { - signature, - url: "http://localhost:3007/download", - with_elevated_task: false, - }, - ); - } - // Should fail - BundleTarget::Rpm => {} - // AppImage should use fallback - _ => { - platforms.insert( - target, - PlatformUpdate { - signature, - url: "http://localhost:3007/download", - with_elevated_task: false, - }, - ); - } +) -> 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)> { +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/debug/bundle/appimage/app-updater_{version}_amd64.AppImage" )), + Some(target.clone()), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], ), + // update using full name ( - BundleTarget::Deb, + BundleTarget::AppImage, root_dir.join(format!( - "target/debug/bundle/deb/app-updater_{version}_amd64.deb" + "target/debug/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::Rpm, + BundleTarget::AppImage, root_dir.join(format!( - "target/debug/bundle/rpm/app-updater_{version}_amd64.rpm" + "target/debug/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)> { +fn bundle_paths( + root_dir: &Path, + _version: &str, + v1compatible: bool, +) -> Vec<(BundleTarget, PathBuf)> { vec![( BundleTarget::App, root_dir.join("target/debug/bundle/macos/app-updater.app"), @@ -184,7 +185,11 @@ fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> } #[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"), @@ -192,12 +197,16 @@ fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> } #[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") } #[cfg(windows)] -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::Nsis, @@ -240,8 +249,6 @@ fn update_app() { Updater::String(V1Compatible::V1Compatible) ); - // bundle app update - build_app(&manifest_dir, &config, true, BundleTarget::get_targets()); let updater_zip_ext = if v1_compatible { if cfg!(windows) { @@ -255,7 +262,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::get_targets()); + let bundle_updater_ext = if v1_compatible { out_bundle_path .extension() @@ -288,8 +301,6 @@ fn update_app() { )); 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"), @@ -300,14 +311,8 @@ fn update_app() { for request in server_.incoming_requests() { match request.url() { "/" => { - let mut platforms = HashMap::new(); - - insert_plaforms( - bundle_target, - &mut platforms, - target.clone(), - signature.clone(), - ); + let platforms = + target_to_platforms(update_platform.clone(), signature.clone()); let body = serde_json::to_vec(&Update { version: "1.0.0", @@ -347,21 +352,12 @@ fn update_app() { // bundle initial app version build_app(&manifest_dir, &config, false, vec![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 if matches!(bundle_target, BundleTarget::Rpm) { - vec![ERROR_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")) } 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 @@ -369,11 +365,22 @@ 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 if matches!(bundle_target, BundleTarget::AppImage) { + Command::new( + &test_cases(&root_dir, "0.1.0", target.clone()) + .first() + .unwrap() + .1, + ) } else { - Command::new(&bundle_paths(&root_dir, "0.1.0").first().unwrap().1) + Command::new(root_dir.join("target/debug/app-updater")) }; binary_cmd.env("TARGET", bundle_target.name()); @@ -383,7 +390,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)]