diff --git a/apps/artcraft-launcher/src/main.rs b/apps/artcraft-launcher/src/main.rs index cb7e87d..6d43354 100644 --- a/apps/artcraft-launcher/src/main.rs +++ b/apps/artcraft-launcher/src/main.rs @@ -30,7 +30,7 @@ const SETTINGS_KEY: &str = "settings"; type Wake = Arc>>; fn main() -> ExitCode { - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); // Detect system language in the background so the first frame can use it. artcraft_launcher_ui_egui::i18n::detect_system_lang_in_background(); let args: Vec = std::env::args().skip(1).collect(); diff --git a/crates/core/src/platform.rs b/crates/core/src/platform.rs index c8b2fac..b044d29 100644 --- a/crates/core/src/platform.rs +++ b/crates/core/src/platform.rs @@ -300,4 +300,53 @@ mod tests { Some("photocraft-0.5.0-linux-riscv64.deb") ); } + + #[test] + fn strictly_matches_architecture_for_all_linux_package_formats() { + let multi_arch_release = release(&[ + "photocraft-0.5.0-linux-aarch64.AppImage", + "photocraft-0.5.0-linux-x86_64.AppImage", + "photocraft-0.5.0-linux-aarch64.deb", + "photocraft-0.5.0-linux-x86_64.deb", + "photocraft-0.5.0-linux-aarch64.rpm", + "photocraft-0.5.0-linux-x86_64.rpm", + "photocraft-0.5.0-linux-aarch64.flatpak", + "photocraft-0.5.0-linux-x86_64.flatpak", + "photocraft-0.5.0-linux-aarch64.tar.gz", + "photocraft-0.5.0-linux-x86_64.tar.gz", + ]); + + let p_x64 = Platform { os: Os::Linux, arch: Arch::X64 }; + let p_arm64 = Platform { os: Os::Linux, arch: Arch::Arm64 }; + + for kind in [PackageKind::AppImage, PackageKind::Deb, PackageKind::Rpm, PackageKind::Flatpak, PackageKind::TarGz] { + let pick_x64 = pick_asset(&multi_arch_release, &["photocraft"], p_x64, &[kind]).map(|(a, _)| a.name.clone()); + assert!( + pick_x64.as_deref().unwrap().contains("linux-x86_64"), + "Expected x86_64 asset for {kind:?}, got {pick_x64:?}" + ); + + let pick_arm64 = pick_asset(&multi_arch_release, &["photocraft"], p_arm64, &[kind]).map(|(a, _)| a.name.clone()); + assert!( + pick_arm64.as_deref().unwrap().contains("linux-aarch64"), + "Expected aarch64 asset for {kind:?}, got {pick_arm64:?}" + ); + } + + // On x86_64 Linux, an aarch64-only asset must NEVER match or fall back + let arm_only_release = release(&[ + "photocraft-0.5.0-linux-aarch64.AppImage", + "photocraft-0.5.0-linux-aarch64.deb", + "photocraft-0.5.0-linux-aarch64.rpm", + "photocraft-0.5.0-linux-aarch64.flatpak", + "photocraft-0.5.0-linux-aarch64.tar.gz", + ]); + for kind in [PackageKind::AppImage, PackageKind::Deb, PackageKind::Rpm, PackageKind::Flatpak, PackageKind::TarGz] { + assert_eq!( + pick_asset(&arm_only_release, &["photocraft"], p_x64, &[kind]), + None, + "x86_64 must never match an aarch64 asset for {kind:?}" + ); + } + } } diff --git a/crates/native/src/backend.rs b/crates/native/src/backend.rs index 6e81873..725d2e3 100644 --- a/crates/native/src/backend.rs +++ b/crates/native/src/backend.rs @@ -309,6 +309,7 @@ impl NativeBackend { fn start_job(&self, app_id: &str, uninstall: bool) { let Some(app) = CraftApp::by_id(app_id) else { return }; + log::info!("{}: starting {} job", app.name, if uninstall { "uninstall" } else { "install/update" }); let stop = Arc::new(Stop::default()); let restarting = { let mut jobs = self.shared.jobs.lock().unwrap_or_else(PoisonError::into_inner); diff --git a/crates/native/src/detect.rs b/crates/native/src/detect.rs index 88631f2..73c39a0 100644 --- a/crates/native/src/detect.rs +++ b/crates/native/src/detect.rs @@ -235,11 +235,12 @@ pub mod linux { if let Some(list) = capture("pacman", &["-Q"], t) { add(InstallSource::Pacman, packages::parse_pacman(&list)); } - if let Some(list) = crate::process::capture_flatpak(&["list", "--app", "--columns=application,version"], t) { + let arch = format!("--arch={}", std::env::consts::ARCH); + if let Some(list) = crate::process::capture_flatpak(&["list", "--app", &arch, "--columns=application,version"], t) { let mut flatpaks = packages::parse_flatpak_list(&list); // Bundles built from a release tarball often list no version: ask Flatpak directly. for (id, version) in flatpaks.iter_mut().filter(|(id, v)| v.is_empty() && id.starts_with("ai.storyteller.")) { - if let Some(v) = crate::process::capture_flatpak(&["info", id], t).as_deref().and_then(packages::parse_flatpak_info) { + if let Some(v) = crate::process::capture_flatpak(&["info", &arch, id], t).as_deref().and_then(packages::parse_flatpak_info) { *version = v; } } diff --git a/crates/native/src/install.rs b/crates/native/src/install.rs index 7398ff8..d31d8b2 100644 --- a/crates/native/src/install.rs +++ b/crates/native/src/install.rs @@ -53,7 +53,9 @@ pub fn install( { return Err(format!("{} is running. Quit it, then try again.", app.name)); } + log::info!("{}: starting {} to {} ({})", app.name, if existing.is_some() { "update" } else { "install" }, release.version_label(), kind.label()); let (file, verified) = download(release, asset, job)?; + log::info!("{}: download ready ({}), running installer...", app.name, file.display()); // Past this point the install can't be interrupted halfway. (job.phase)(JobPhase::Installing); #[allow(unused_variables)] @@ -74,6 +76,11 @@ pub fn install( PackageKind::TarGz => freebsd::install_tree(app, &file, job.paths).map(|(bin, files, root)| Some(record(&version, kind, bin, files, root))), other => Err(format!("{} isn't supported on this system", other.label())), }; + match &result { + Ok(_) => log::info!("{}: successfully installed/updated to {}", app.name, release.version_label()), + Err(e) if e == CANCELLED => log::info!("{}: installation cancelled", app.name), + Err(e) => log::error!("{}: installation failed: {e}", app.name), + } if !keep_download(result.is_ok(), verified) { let _ = std::fs::remove_file(&file); } @@ -489,16 +496,33 @@ pub(crate) mod linux { } match kind { PackageKind::Flatpak => { - let status = process::flatpak_command() - .args(["install", "--user", "-y", "--noninteractive", "--bundle"]) - .arg(f) - .status() - .map_err(|e| format!("couldn't start Flatpak ({e})"))?; - if status.success() { - Ok(()) - } else { - Err("Flatpak couldn't install the bundle (it needs the Flathub remote for its runtime)".into()) - } + let install_one = || { + log::info!("executing flatpak install --reinstall --bundle for {}", file.display()); + let output = process::flatpak_command() + .args(["install", "--user", "-y", "--noninteractive", "--reinstall", "--bundle"]) + .arg(f) + .output() + .map_err(|e| format!("couldn't start Flatpak ({e})"))?; + if output.status.success() { + log::info!("flatpak install succeeded for {}", file.display()); + Ok(()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + let err = stderr.trim(); + let err = if err.is_empty() { + String::from_utf8_lossy(&output.stdout).trim().to_string() + } else { + err.to_string() + }; + if err.is_empty() { + Err("Flatpak couldn't install the bundle (it needs the Flathub remote for its runtime)".into()) + } else { + let clean_err = err.strip_prefix("error: ").or_else(|| err.strip_prefix("Error: ")).unwrap_or(&err); + Err(format!("Flatpak couldn't install the bundle: {clean_err}")) + } + } + }; + root.queue.alone(&cancelled(stop), &mut turn(phase, JobPhase::Installing), install_one).unwrap_or(Err(CANCELLED.into())) } other => Err(format!("{} isn't a system package", other.label())), } @@ -1260,6 +1284,20 @@ mod tests { assert_eq!(results, vec![Err(error.to_string()); 2]); } + #[cfg(target_os = "linux")] + #[test] + fn flatpak_bundle_install_failure_reports_clean_error() { + if !crate::process::is_flatpak_available() { + return; + } + let queue = crate::queue::Queue::new(); + let root = linux::Root { queue: &queue, run: &|_| Ok(()) }; + let result = linux::install_package_via(&root, PackageKind::Flatpak, Path::new("/nonexistent/file.flatpak"), &Stop::default(), &mut |_| ()); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.starts_with("Flatpak couldn't install the bundle: "), "unexpected error: {err}"); + } + #[cfg(target_os = "linux")] #[test] fn adds_and_removes_only_its_own_menu_entry_and_icon() { diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index 16d0591..21a8936 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -407,6 +407,7 @@ impl LauncherUi { }) .collect(); for (id, version) in to_update { + log::info!("auto-update: queuing update for {id} to {version}"); self.auto_update_attempted.insert((id.clone(), version)); self.send(Command::Install { app: id }); } @@ -417,6 +418,7 @@ impl LauncherUi { let id = artcraft_launcher_core::catalog::LAUNCHER_ID.to_string(); let v = r.version_label(); if !self.auto_update_attempted.contains(&(id.clone(), v.clone())) { + log::info!("auto-update: queuing launcher self-update to {v}"); self.auto_update_attempted.insert((id, v)); self.send(Command::UpdateLauncher); }