Linux: fix detection of system package installs (#99)
- Include InstallSource::Rpm in ours package filter - Add bundle_id to candidate names for Deb and Rpm packages - Strip architecture suffix from package name in parse_dpkg - Clear AppImage environment variables in process::command to prevent polluting system tools - Fall back to detecting system desktop entries and binaries with CLI version extraction - Add unit tests for candidate names, RPM filtering, and binary CLI version Fixes #97
This commit is contained in:
parent
bd1182e75c
commit
797071e591
|
|
@ -184,7 +184,7 @@ pub mod windows {
|
|||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux {
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use artcraft_launcher_core::platform::parse_asset_name;
|
||||
|
|
@ -198,7 +198,7 @@ pub mod linux {
|
|||
pub fn packages() -> Vec<Package> {
|
||||
let ours = |name: &str| {
|
||||
artcraft_launcher_core::SUITE.iter().any(|a| {
|
||||
[InstallSource::Deb, InstallSource::Pacman, InstallSource::Flatpak]
|
||||
[InstallSource::Deb, InstallSource::Rpm, InstallSource::Pacman, InstallSource::Flatpak]
|
||||
.iter()
|
||||
.any(|m| crate::packages::candidate_names(a, *m).iter().any(|n| n == name))
|
||||
})
|
||||
|
|
@ -269,9 +269,78 @@ pub mod linux {
|
|||
return Some(Installed { version, source: InstallSource::AppImage, location: path.display().to_string(), package: None });
|
||||
}
|
||||
let order = [InstallSource::Deb, InstallSource::Rpm, InstallSource::Pacman, InstallSource::Flatpak];
|
||||
let p = packages::find(app, &snapshot.packages, &order)?;
|
||||
let location = if p.manager == InstallSource::Flatpak { app.bundle_id() } else { binary_on_path(app.id) };
|
||||
Some(Installed { version: packages::version_of(p), source: p.manager, location, package: Some(p.name.clone()) })
|
||||
if let Some(p) = packages::find(app, &snapshot.packages, &order) {
|
||||
let location = if p.manager == InstallSource::Flatpak { app.bundle_id() } else { binary_on_path(app.id) };
|
||||
return Some(Installed { version: packages::version_of(p), source: p.manager, location, package: Some(p.name.clone()) });
|
||||
}
|
||||
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.
|
||||
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()),
|
||||
})
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
pub(crate) fn binary_version(bin: &Path, id: &str) -> Option<version::Version> {
|
||||
let out = crate::process::capture(bin.to_str()?, &["--version"], Duration::from_secs(5))?;
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -297,3 +366,40 @@ pub mod freebsd {
|
|||
version::parse(rest.split_whitespace().next()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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| {
|
||||
[InstallSource::Deb, InstallSource::Rpm, InstallSource::Pacman, InstallSource::Flatpak]
|
||||
.iter()
|
||||
.any(|m| crate::packages::candidate_names(a, *m).iter().any(|n| n == name))
|
||||
})
|
||||
};
|
||||
assert!(ours("photocraft"));
|
||||
assert!(ours("ai.storyteller.photocraft"));
|
||||
assert!(!ours("unrelated-package"));
|
||||
}
|
||||
|
||||
#[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();
|
||||
}
|
||||
let ver = linux::binary_version(&bin, photo.id);
|
||||
assert_eq!(ver.unwrap().to_string(), "0.6.0");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ pub fn parse_dpkg(text: &str) -> Vec<(String, String)> {
|
|||
.filter_map(|line| {
|
||||
let mut f = line.split('|');
|
||||
match (f.next(), f.next(), f.next()) {
|
||||
(Some(name), Some("installed"), Some(ver)) => Some((name.to_string(), ver.to_string())),
|
||||
(Some(name), Some("installed"), Some(ver)) => {
|
||||
let name = name.split_once(':').map(|(n, _)| n).unwrap_or(name);
|
||||
Some((name.to_string(), ver.to_string()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
|
|
@ -62,11 +65,17 @@ pub fn parse_flatpak_info(text: &str) -> Option<String> {
|
|||
|
||||
/// The package names a Craft App may be installed under with `manager`. On Arch the AUR carries
|
||||
/// each app three ways: built from source (`photocraft`), the release binary (`photocraft-bin`)
|
||||
/// and the latest commit (`photocraft-git`).
|
||||
/// and the latest commit (`photocraft-git`). Debian and RPM packages may use either the short stem
|
||||
/// (`photocraft`) or the reverse-DNS bundle id (`ai.storyteller.photocraft`).
|
||||
pub fn candidate_names(app: &CraftApp, manager: InstallSource) -> Vec<String> {
|
||||
match manager {
|
||||
InstallSource::Flatpak => vec![app.bundle_id()],
|
||||
InstallSource::Pacman => app.stems().flat_map(|s| [s.to_string(), format!("{s}-bin"), format!("{s}-git")]).collect(),
|
||||
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()
|
||||
}
|
||||
_ => app.stems().map(str::to_string).collect(),
|
||||
}
|
||||
}
|
||||
|
|
@ -94,7 +103,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn reads_each_package_managers_listing() {
|
||||
assert_eq!(parse_dpkg("photocraft|installed|0.5.0-1\nwordcraft|config-files|0.3.0-1\n"), [("photocraft".into(), "0.5.0-1".into())]);
|
||||
assert_eq!(
|
||||
parse_dpkg("photocraft|installed|0.5.0-1\nphotocraft:amd64|installed|0.6.0-1\nwordcraft|config-files|0.3.0-1\n"),
|
||||
[("photocraft".into(), "0.5.0-1".into()), ("photocraft".into(), "0.6.0-1".into())]
|
||||
);
|
||||
assert_eq!(parse_rpm("gridcraft|0.3.0\n"), [("gridcraft".into(), "0.3.0".into())]);
|
||||
assert_eq!(parse_pacman("photocraft-bin 0.5.0-1\nlinux 6.11.1.arch1-1\n").len(), 2);
|
||||
assert_eq!(
|
||||
|
|
@ -106,6 +118,23 @@ mod tests {
|
|||
assert_eq!(parse_flatpak_info("Version: \n"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidate_names_cover_bundle_ids_for_deb_rpm_and_pacman() {
|
||||
let photo = CraftApp::by_id("photocraft").unwrap();
|
||||
let deb_names = candidate_names(photo, InstallSource::Deb);
|
||||
assert!(deb_names.contains(&"photocraft".to_string()));
|
||||
assert!(deb_names.contains(&"ai.storyteller.photocraft".to_string()));
|
||||
|
||||
let rpm_names = candidate_names(photo, InstallSource::Rpm);
|
||||
assert!(rpm_names.contains(&"photocraft".to_string()));
|
||||
assert!(rpm_names.contains(&"ai.storyteller.photocraft".to_string()));
|
||||
|
||||
let pacman_names = candidate_names(photo, InstallSource::Pacman);
|
||||
assert!(pacman_names.contains(&"photocraft".to_string()));
|
||||
assert!(pacman_names.contains(&"photocraft-bin".to_string()));
|
||||
assert!(pacman_names.contains(&"ai.storyteller.photocraft".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_aur_packages_in_every_flavour() {
|
||||
let photo = CraftApp::by_id("photocraft").unwrap();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ use std::path::Path;
|
|||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// A `Command` that won't flash a console window on Windows.
|
||||
/// A `Command` that won't flash a console window on Windows, and clears AppImage-injected
|
||||
/// environment variables that corrupt system helper binaries and external applications.
|
||||
pub fn command(program: impl AsRef<std::ffi::OsStr>) -> Command {
|
||||
#[allow(unused_mut)]
|
||||
let mut cmd = Command::new(program);
|
||||
|
|
@ -14,9 +15,34 @@ pub fn command(program: impl AsRef<std::ffi::OsStr>) -> Command {
|
|||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
#[cfg(unix)]
|
||||
clean_appimage_env(&mut cmd);
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Cleans AppImage runtime environment variables so child processes don't load incompatible
|
||||
/// libraries from the launcher's bundled AppImage.
|
||||
#[cfg(unix)]
|
||||
pub fn clean_appimage_env(cmd: &mut Command) {
|
||||
if std::env::var_os("APPIMAGE").is_some() {
|
||||
for var in [
|
||||
"LD_LIBRARY_PATH",
|
||||
"LD_PRELOAD",
|
||||
"APPDIR",
|
||||
"APPIMAGE",
|
||||
"ARGV0",
|
||||
"OWD",
|
||||
"GDK_PIXBUF_MODULE_FILE",
|
||||
"GIO_MODULE_DIR",
|
||||
"GTK_PATH",
|
||||
"PYTHONHOME",
|
||||
"QT_PLUGIN_PATH",
|
||||
] {
|
||||
cmd.env_remove(var);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs a helper and returns its standard output when it succeeds within `timeout`.
|
||||
pub fn capture(program: &str, args: &[&str], timeout: Duration) -> Option<String> {
|
||||
let mut child = command(program).args(args).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().ok()?;
|
||||
|
|
@ -58,24 +84,6 @@ pub fn launch_binary(exe: &Path, args: &[&str]) -> Result<(), String> {
|
|||
if let Some(dir) = exe.parent() {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
// An AppImage'd launcher sets these for itself; they'd break the app we start.
|
||||
for var in [
|
||||
"LD_LIBRARY_PATH",
|
||||
"LD_PRELOAD",
|
||||
"APPDIR",
|
||||
"APPIMAGE",
|
||||
"ARGV0",
|
||||
"OWD",
|
||||
"GDK_PIXBUF_MODULE_FILE",
|
||||
"GIO_MODULE_DIR",
|
||||
"GTK_PATH",
|
||||
"PYTHONHOME",
|
||||
"QT_PLUGIN_PATH",
|
||||
] {
|
||||
if std::env::var_os("APPIMAGE").is_some() {
|
||||
cmd.env_remove(var);
|
||||
}
|
||||
}
|
||||
spawn_detached(cmd)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue