Linux: report undetected-package fallback as a system binary (#99 follow-up)

When dpkg/rpm don't list the app, the launcher can't know which package
manager installed it, so label it a system binary (open it, leave update
and removal to whatever installed it) instead of Deb/Rpm with a guessed
package name. Require the binary itself; fix clippy and rustfmt.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-11 02:11:39 -04:00
1 parent 797071e591
commit 633f09c7cc
2 files changed
+13 -67

No files matched your search

+12 -64
View File
@@ -276,60 +276,19 @@ pub mod linux {
detect_system_install(app)
}
/// When package manager queries don't report the package (e.g. timeout, unlisted name,
/// running inside a container sandbox, or lock conflicts), detect the app from its desktop
/// entry or system binary.
/// When the package managers don't report the app (a query timed out or was locked, an
/// unlisted package name, or a sandbox that can't see them) but its binary is there. It is
/// reported as a system binary: the launcher can open it but leaves updating and removing it
/// to whatever installed it, since it can't tell which package manager that was.
fn detect_system_install(app: &CraftApp) -> Option<Installed> {
let bin = find_binary(app.id);
let desktop = find_desktop_file(app);
if bin.is_none() && desktop.is_none() {
return None;
}
let bin_path = bin.unwrap_or_else(|| PathBuf::from("/usr/bin").join(app.id));
let version = binary_version(&bin_path, app.id);
let source = if Path::new("/usr/bin/dpkg").is_file() || which("dpkg").is_some() {
InstallSource::Deb
} else if Path::new("/usr/bin/rpm").is_file() || which("rpm").is_some() {
InstallSource::Rpm
} else {
InstallSource::Binary
};
Some(Installed {
version,
source,
location: bin_path.display().to_string(),
package: Some(app.id.to_string()),
})
let bin = find_binary(app.id)?;
Some(Installed { version: binary_version(&bin, app.id), source: InstallSource::Binary, location: bin.display().to_string(), package: None })
}
fn find_binary(id: &str) -> Option<PathBuf> {
if let Some(path) = std::env::var_os("PATH") {
if let Some(p) = std::env::split_paths(&path).map(|d| d.join(id)).find(|p| p.is_file()) {
return Some(p);
}
}
for dir in ["/usr/bin", "/usr/local/bin"] {
let p = PathBuf::from(dir).join(id);
if p.is_file() {
return Some(p);
}
}
if let Some(home) = dirs::home_dir() {
let p = home.join(".local/bin").join(id);
if p.is_file() {
return Some(p);
}
}
None
}
fn find_desktop_file(app: &CraftApp) -> Option<PathBuf> {
let mut dirs = vec![PathBuf::from("/usr/share/applications"), PathBuf::from("/usr/local/share/applications")];
if let Some(data) = dirs::data_dir() {
dirs.push(data.join("applications"));
}
let names = [format!("{}.desktop", app.bundle_id()), format!("{}.desktop", app.id)];
dirs.into_iter().flat_map(|d| names.iter().map(move |n| d.join(n))).find(|p| p.is_file())
let on_path = std::env::var_os("PATH").and_then(|path| std::env::split_paths(&path).map(|d| d.join(id)).find(|p| p.is_file()));
let usual = ["/usr/bin", "/usr/local/bin"].into_iter().map(PathBuf::from).chain(dirs::home_dir().map(|h| h.join(".local/bin")));
on_path.or_else(|| usual.map(|d| d.join(id)).find(|p| p.is_file()))
}
pub(crate) fn binary_version(bin: &Path, id: &str) -> Option<version::Version> {
@@ -337,11 +296,6 @@ pub mod linux {
let rest = out.trim().strip_prefix(id)?.trim_start();
version::parse(rest.split_whitespace().next()?)
}
fn which(name: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path).map(|d| d.join(name)).find(|p| p.is_file())
}
}
#[cfg(target_os = "freebsd")]
@@ -367,12 +321,11 @@ pub mod freebsd {
}
}
#[cfg(test)]
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
#[test]
#[cfg(target_os = "linux")]
fn ours_filter_matches_rpm_packages() {
let ours = |name: &str| {
artcraft_launcher_core::SUITE.iter().any(|a| {
@@ -387,19 +340,14 @@ mod tests {
}
#[test]
#[cfg(target_os = "linux")]
fn reads_binary_version_from_cli() {
let photo = CraftApp::by_id("photocraft").unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let bin = temp_dir.path().join("photocraft");
std::fs::write(&bin, "#!/bin/sh\necho 'photocraft 0.6.0 (0c72d9542, 2026-10-10)'\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap();
}
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap();
let ver = linux::binary_version(&bin, photo.id);
assert_eq!(ver.unwrap().to_string(), "0.6.0");
}
}
+1 -3
View File
@@ -73,9 +73,7 @@ pub fn candidate_names(app: &CraftApp, manager: InstallSource) -> Vec<String> {
InstallSource::Pacman => {
app.stems().flat_map(|s| [s.to_string(), format!("{s}-bin"), format!("{s}-git")]).chain(std::iter::once(app.bundle_id())).collect()
}
InstallSource::Deb | InstallSource::Rpm => {
app.stems().map(str::to_string).chain(std::iter::once(app.bundle_id())).collect()
}
InstallSource::Deb | InstallSource::Rpm => app.stems().map(str::to_string).chain(std::iter::once(app.bundle_id())).collect(),
_ => app.stems().map(str::to_string).collect(),
}
}