craft-update: only /usr/bin/<id> is a package; leave system folders to their package manager

The launcher's self-update checked the package databases only for /usr/bin/<id> and left any
other copy alone. The extraction (#311) dropped that guard and added a Standalone fallback, so on
Linux:

- a copy in ~/bin was classed as the deb/rpm/Arch package whenever the package was also
  installed: the update installed the package with pkexec and relaunched the copy it never
  touched;
- an AUR build in /usr/bin (which the comment says is left to pacman) became Standalone, so the
  update tried to rename over a file pacman owns.

linux_location restores the /usr/bin/<id> check, returns no self-update for anything else under
/usr, /opt, /bin or /sbin, and keeps the Standalone update for a copy outside them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
smittie2000andClaude Opus 5.5 committed 2026-10-27 09:01:40 +02:00
1 parent f3706afe32
commit 6d0982eb05
1 file changed
+52 -3
+52 -3
View File
@@ -226,6 +226,11 @@ fn detect(app: &App, exe: &Path) -> Option<Install> {
if let Some(appimage) = std::env::var_os("APPIMAGE") {
return Some(Install::AppImage(PathBuf::from(appimage)));
}
match linux_location(app, exe) {
LinuxLocation::PackagePath => {}
LinuxLocation::System => return None,
LinuxLocation::Elsewhere => return exe.is_file().then(|| Install::Standalone(exe.to_path_buf())),
}
let t = std::time::Duration::from_secs(10);
if process::capture("dpkg-query", &["-W", "-f", "${db:Status-Status}", app.id], t).is_some_and(|s| s.trim() == "installed") {
return Some(Install::Package(PackageKind::Deb, exe.to_path_buf()));
@@ -238,12 +243,36 @@ fn detect(app: &App, exe: &Path) -> Option<Install> {
if crate::detect::linux::has_pacman_db() && process::capture("pacman", &["-Q", &app.bundle_id()], t).is_some() {
return Some(Install::Package(PackageKind::ArchPkg, exe.to_path_buf()));
}
if exe.is_file() {
return Some(Install::Standalone(exe.to_path_buf()));
}
None
}
/// Where a Linux executable outside a Flatpak or AppImage runs from, for [`detect`].
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[derive(Debug, PartialEq, Eq)]
enum LinuxLocation {
/// `/usr/bin/<id>`, where the release's deb, rpm and Arch packages put it. Only this path is
/// checked against the package databases: a copy elsewhere isn't the package's even when the
/// package is installed too.
PackagePath,
/// Anywhere else under a folder the system's package manager owns (`/usr`, `/opt`, `/bin`,
/// `/sbin`): a distribution or AUR package's copy, or one a system administrator put there.
/// It's theirs to update, and replacing it in place would write over a file pacman or dpkg owns.
System,
/// Anywhere else, such as a folder in the user's home: a standalone copy that replaces itself.
Elsewhere,
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn linux_location(app: &App, exe: &Path) -> LinuxLocation {
if exe == Path::new("/usr/bin").join(app.id) {
LinuxLocation::PackagePath
} else if ["/usr", "/opt", "/bin", "/sbin"].iter().any(|dir| exe.starts_with(dir)) {
LinuxLocation::System
} else {
LinuxLocation::Elsewhere
}
}
/// Whether this sandbox runs the per-user installation's copy, from `app-path` in `/.flatpak-info`.
#[cfg(target_os = "linux")]
fn is_user_install() -> bool {
@@ -1739,6 +1768,26 @@ mod tests {
assert!(result.unwrap_err().contains("is not newer than currently running version"));
}
/// Only `/usr/bin/<id>` is checked against the package databases, as before the launcher's
/// self-update moved here; another copy under a system folder (an AUR build, say) is left to
/// its package manager, and only a copy outside them replaces itself.
#[test]
fn only_the_packages_own_path_is_a_package_and_system_folders_are_left_alone() {
let app = &crate::suite::LAUNCHER;
assert_eq!(linux_location(app, Path::new("/usr/bin/artcraft-launcher")), LinuxLocation::PackagePath);
for system in [
"/usr/bin/artcraft-launcher-bin",
"/usr/local/bin/artcraft-launcher",
"/opt/artcraft-launcher/artcraft-launcher",
"/bin/artcraft-launcher",
] {
assert_eq!(linux_location(app, Path::new(system)), LinuxLocation::System, "{system}");
}
for own in ["/home/me/bin/artcraft-launcher", "/home/me/.local/bin/artcraft-launcher", "/usrlocal/artcraft-launcher"] {
assert_eq!(linux_location(app, Path::new(own)), LinuxLocation::Elsewhere, "{own}");
}
}
/// storytold/craft-launcher#764: a self-update downloads only its own release's link. One into
/// another app's releases, with the right tag and name, is refused before anything is fetched.
#[test]