From a4e76179ccb2fa740d4ff2515049c3d704e294b8 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 1 Nov 2026 23:02:03 -0500 Subject: [PATCH] fix: preserve repository ownership and Flatpak update results --- crates/native/src/backend.rs | 7 ------- crates/native/src/detect.rs | 6 +++--- crates/native/src/install.rs | 9 +++++++-- crates/native/src/packages.rs | 28 ++++++++++++++++++++++++++++ crates/native/src/process.rs | 6 ++++++ 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/crates/native/src/backend.rs b/crates/native/src/backend.rs index e3e43782..bea5c506 100644 --- a/crates/native/src/backend.rs +++ b/crates/native/src/backend.rs @@ -379,13 +379,6 @@ impl NativeBackend { // One installer call can cover several apps, and fail after some of them went in. let latest = shared.latest.lock().unwrap_or_else(PoisonError::into_inner).get(app.id).cloned(); let result = match (result, latest) { - (Ok(_), _) - if existing.as_ref().is_some_and(|e| e.externally_managed) - && found.is_some() - && existing.as_ref().and_then(|e| e.version.as_ref()) == found.as_ref().and_then(|f| f.version.as_ref()) => - { - Ok(format!("{} is already up to date", app.name)) - } (Err(e), Some(release)) if !uninstall && release.version.as_ref().is_some_and(|v| landed_despite(&e, existing.as_ref(), found.as_ref(), v)) => { diff --git a/crates/native/src/detect.rs b/crates/native/src/detect.rs index f62630d2..75dcfe8d 100644 --- a/crates/native/src/detect.rs +++ b/crates/native/src/detect.rs @@ -620,7 +620,7 @@ pub mod linux { // A Flatpak runs under the id it was found under, which may be an older one. let location = if p.manager == InstallSource::Flatpak { p.name.clone() } else { binary_on_path(app.id) }; let externally_managed = p.manager == InstallSource::Flatpak - && p.origin.as_deref().is_some_and(|origin| { + && p.origin.as_deref().is_none_or(|origin| { if packages::is_flatpark_origin(origin) { return true; } @@ -629,8 +629,8 @@ pub mod linux { Some("system") | None => "--system".to_string(), Some(name) => format!("--installation={name}"), }; - crate::process::capture_flatpak(&["remotes", &scope, "--columns=name,url"], Duration::from_secs(10)) - .is_some_and(|remotes| packages::is_artcraft_remote(origin, &remotes)) + crate::process::capture_flatpak(&["remotes", &scope, "--show-disabled", "--columns=name,url"], Duration::from_secs(10)) + .is_none_or(|remotes| packages::remote_manages_flatpak(origin, &remotes)) }); return Some(Installed { version: packages::version_of(p), diff --git a/crates/native/src/install.rs b/crates/native/src/install.rs index 229b7096..c67510d9 100644 --- a/crates/native/src/install.rs +++ b/crates/native/src/install.rs @@ -121,6 +121,11 @@ pub fn install_at( { return Err(format!("{} is running. Quit it, then try again.", app.name)); } + if kind == PackageKind::Flatpak + && let Some(installed) = existing.filter(|installed| installed.source == InstallSource::Flatpak && installed.externally_managed) + { + return Err(managed_elsewhere(app, installed, false).unwrap_or_else(|| "Update this app through its Flatpak repository".into())); + } bind_artifact_version(app, release, asset)?; // A feed can't send an app back to an older build; only a version the person chose by name // (a downgrade with a way back if it fails) gets past this. @@ -1262,7 +1267,7 @@ pub(crate) mod linux { if !reference.starts_with(&format!("app/{id}/")) || !reference.ends_with("/stable") { return Ok(false); } - let remotes = run(&["remotes", "--columns=name,url"])?; + let remotes = run(&["remotes", "--show-disabled", "--columns=name,url"])?; let remote = remotes.lines().find_map(|line| { let (name, _) = line.split_once('\t')?; crate::packages::is_artcraft_remote(name, line).then(|| name.to_string()) @@ -1274,7 +1279,7 @@ pub(crate) mod linux { return Err("The artcraft-oci remote points to a different repository".into()); } run(&["remote-add", "--noninteractive", "artcraft-oci", "https://getartcraft.com/artcraft-oci.flatpakrepo"])?; - let remotes = run(&["remotes", "--columns=name,url"])?; + let remotes = run(&["remotes", "--show-disabled", "--columns=name,url"])?; if !crate::packages::is_artcraft_remote("artcraft-oci", &remotes) { return Err("The ArtCraft OCI descriptor configured an unexpected repository".into()); } diff --git a/crates/native/src/packages.rs b/crates/native/src/packages.rs index 3ec4157a..913bb67c 100644 --- a/crates/native/src/packages.rs +++ b/crates/native/src/packages.rs @@ -305,3 +305,31 @@ mod migration_tests { } } } + +/// Only a confirmed bundle origin with no repository URL permits bundle replacement. +/// Missing rows are unknown, so preserve remote ownership conservatively. +pub fn remote_manages_flatpak(origin: &str, remotes: &str) -> bool { + remotes + .lines() + .find_map(|line| { + let mut fields = line.split('\t'); + if fields.next().map(str::trim) != Some(origin) { + return None; + } + Some(fields.next().is_some_and(|url| !url.trim().is_empty())) + }) + .unwrap_or(true) +} + +#[cfg(test)] +mod remote_ownership_tests { + use super::*; + #[test] + fn only_confirmed_empty_urls_allow_bundle_replacement() { + assert!(!remote_manages_flatpak("app-origin", "app-origin\t\n")); + assert!(remote_manages_flatpak("renamed", "renamed\thttps://example.org/repo\n")); + assert!(remote_manages_flatpak("renamed", "renamed\toci+https://getartcraft.com\n")); + assert!(remote_manages_flatpak("missing", "other\t\n")); + assert!(!remote_manages_flatpak("app-origin", "app-origin")); + } +} diff --git a/crates/native/src/process.rs b/crates/native/src/process.rs index 04b70099..e4c38e36 100644 --- a/crates/native/src/process.rs +++ b/crates/native/src/process.rs @@ -797,7 +797,13 @@ pub(crate) fn cancellable_output(mut cmd: Command, check: impl Fn() -> Result<() let mut child = cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().map_err(|e| format!("couldn't run flatpak ({e})"))?; let (stdout, stderr) = (read_all(child.stdout.take()), read_all(child.stderr.take())); + let started = Instant::now(); let status = loop { + if started.elapsed() >= Duration::from_secs(60 * 60) { + let _ = child.kill(); + let _ = child.wait(); + return Err("Flatpak did not finish within one hour".into()); + } if let Err(e) = check() { let _ = child.kill(); let _ = child.wait();