linux: fix Flatpak bundle updates, serialize installs, filter detection by arch, and add status logging (#149)

* linux: allow updating Flatpak bundles with --reinstall and improve error reporting

* linux: filter flatpak detection by arch, queue bundle installs, and log actions

* test(core): verify strict arch matching across all linux package formats
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-10-11 14:49:33 -04:00
1 parent e2b01e7425
commit c273a94a9a
6 files changed
+104 -13

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ const SETTINGS_KEY: &str = "settings";
type Wake = Arc<OnceLock<Box<dyn Fn() + Send + Sync>>>;
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<String> = std::env::args().skip(1).collect();
+49
View File
@@ -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:?}"
);
}
}
}
+1
View File
@@ -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);
+3 -2
View File
@@ -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;
}
}
+48 -10
View File
@@ -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() {
+2
View File
@@ -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);
}