mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-10-13 23:08:16 +00:00
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:
1 parent
e2b01e7425
commit
c273a94a9a
6 files changed
+104
-13
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user