From 37a1ef72ad37673312965692a3ebd9d5464849f2 Mon Sep 17 00:00:00 2001 From: Lyraedan <61066562+Lyraedan@users.noreply.github.com> Date: Sat, 10 Oct 2026 04:32:08 +0100 Subject: [PATCH] Fixed debian installs being flagged as "Update available" when updated to latest version Rebased onto main and combined with 8143a26: keeps the -bin/-git AUR detection and pre-release-aware version parsing from main, adds this change's pacman removal (now using the detected package name), its X.Y.Z-anywhere fallback and its dialog wording. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 2 ++ crates/core/src/version.rs | 42 ++++++++++++++++++++++++++++++++++- crates/native/src/install.rs | 40 ++++++++++++++++++--------------- crates/ui-egui/src/dialogs.rs | 11 +++++---- 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index f37514d..2fe4440 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ plan/ plans/ /target target/ +/builds +builds/ /dist /temp /craft-fonts diff --git a/crates/core/src/version.rs b/crates/core/src/version.rs index d6a0220..0beceef 100644 --- a/crates/core/src/version.rs +++ b/crates/core/src/version.rs @@ -57,10 +57,46 @@ pub fn parse_package(text: &str) -> Option { match parts.as_slice() { [a, b, c] => Version::parse(&format!("{a}.{b}.{c}")).ok(), [a, b] => Version::parse(&format!("{a}.{b}.0")).ok(), - _ => None, + // Text around the version, as in `--version` output (`PhotoCraft 0.5.0 (abc, 2026)`): + // the first `X.Y.Z` run anywhere. + _ => first_xyz(&t), } } +/// The first `major.minor.patch` run anywhere in `text`. +fn first_xyz(text: &str) -> Option { + let bytes = text.as_bytes(); + for start in 0..bytes.len() { + if !bytes.get(start).is_some_and(u8::is_ascii_digit) || (start > 0 && bytes.get(start - 1).is_some_and(u8::is_ascii_digit)) { + continue; + } + let Some((major, i)) = digits(text, start) else { continue }; + if bytes.get(i) != Some(&b'.') { + continue; + } + let Some((minor, j)) = digits(text, i + 1) else { continue }; + if bytes.get(j) != Some(&b'.') { + continue; + } + let Some((patch, _)) = digits(text, j + 1) else { continue }; + return Some(Version::new(major, minor, patch)); + } + None +} + +/// A run of ASCII digits from `from`, as a number and the index just past it. +fn digits(text: &str, from: usize) -> Option<(u64, usize)> { + let bytes = text.as_bytes(); + let mut end = from; + while bytes.get(end).is_some_and(u8::is_ascii_digit) { + end += 1; + } + if end == from { + return None; + } + text.get(from..end)?.parse().ok().map(|n| (n, end)) +} + /// A version that's a plain `X.Y.Z` with no pre-release suffix. pub fn is_stable(v: &Version) -> bool { v.pre.is_empty() @@ -122,6 +158,10 @@ mod tests { assert_eq!(p("0.3.0.r47.g47f9306-1").as_deref(), Some("0.3.0")); // Plain versions (rpm %{VERSION}, Flatpak) pass through. assert_eq!(p("0.4.0").as_deref(), Some("0.4.0")); + assert_eq!(p("0.5").as_deref(), Some("0.5.0")); + // A version inside text, as `--version` prints it. + assert_eq!(p("PhotoCraft 0.5.0 (abc, 2026)").as_deref(), Some("0.5.0")); + assert_eq!(p("junk"), None); assert_eq!(p("garbage"), None); // The bug this guards: 0.5.0-1 used to read as a pre-release, so an installed .deb of the // latest release always looked out of date. diff --git a/crates/native/src/install.rs b/crates/native/src/install.rs index 1d84b90..60243b0 100644 --- a/crates/native/src/install.rs +++ b/crates/native/src/install.rs @@ -93,11 +93,12 @@ pub fn choose_kinds(existing: Option<&Installed>, record: Option<&Record>, prefe InstallSource::Msi => Some(PackageKind::Msi), InstallSource::Deb => Some(PackageKind::Deb), InstallSource::Rpm => Some(PackageKind::Rpm), + // Arch/AUR installs are owned by pacman and updated by the user's AUR helper (see + // `managed_elsewhere`); the launcher opens and removes them but never replaces them. + InstallSource::Pacman => None, InstallSource::Flatpak => Some(PackageKind::Flatpak), InstallSource::AppImage => Some(PackageKind::AppImage), InstallSource::Binary => Some(PackageKind::TarGz), - // Updated by the user's AUR helper; see `managed_elsewhere`. - InstallSource::Pacman => None, }; if let Some(k) = kind.filter(|k| available.contains(k)) { return vec![k]; @@ -161,22 +162,20 @@ fn published_checksum(release: &Release, asset: &Asset, http: &Http) -> Result Option { match installed.source { - InstallSource::Pacman => { + InstallSource::Pacman if !uninstall => { let pkg = installed.package.clone().unwrap_or_else(|| app.id.to_string()); - Some(if uninstall { - format!("{} was installed from the AUR ({pkg}). Remove it with: sudo pacman -R {pkg}", app.name) - } else { - let helper = ["paru", "yay"].into_iter().find(|h| which(h).is_some()); - let cmd = match helper { - Some(h) => format!("{h} -S {pkg}"), - None => format!("your AUR helper (for example: yay -S {pkg})"), - }; - format!("{} was installed from the AUR ({pkg}). Update it with {cmd}", app.name) - }) + let helper = ["paru", "yay"].into_iter().find(|h| which(h).is_some()); + let cmd = match helper { + Some(h) => format!("{h} -S {pkg}"), + None => format!("your AUR helper (for example: yay -S {pkg})"), + }; + Some(format!("{} was installed from the AUR ({pkg}). Update it with {cmd}", app.name)) } InstallSource::Binary => Some(format!("{} was installed outside the launcher. Update or remove it the way it was installed.", app.name)), _ => None, @@ -201,7 +200,7 @@ pub fn uninstall(app: &CraftApp, installed: &Installed, record: Option<&Record>) #[cfg(target_os = "macos")] InstallSource::MacApp | InstallSource::Launcher => macos::move_to_trash(&path), #[cfg(target_os = "linux")] - InstallSource::Deb | InstallSource::Rpm | InstallSource::Flatpak => linux::remove_package(app, installed.source), + InstallSource::Deb | InstallSource::Rpm | InstallSource::Pacman | InstallSource::Flatpak => linux::remove_package(app, installed), #[cfg(target_os = "freebsd")] InstallSource::Launcher => match record { Some(r) => freebsd::remove_files(&r.files, r.needs_root), @@ -373,8 +372,8 @@ mod linux { } } - pub fn remove_package(app: &CraftApp, source: InstallSource) -> Result<(), String> { - match source { + pub fn remove_package(app: &CraftApp, installed: &Installed) -> Result<(), String> { + match installed.source { InstallSource::Deb => run_as_root(&[OsStr::new("apt-get"), OsStr::new("remove"), OsStr::new("-y"), OsStr::new(app.id)]), InstallSource::Rpm => { if which("dnf").is_some() { @@ -383,6 +382,11 @@ mod linux { run_as_root(&[OsStr::new("rpm"), OsStr::new("-e"), OsStr::new(app.id)]) } } + // The package detected (photocraft, photocraft-bin or photocraft-git). + InstallSource::Pacman => { + let pkg = installed.package.as_deref().unwrap_or(app.id); + run_as_root(&[OsStr::new("pacman"), OsStr::new("-R"), OsStr::new("--noconfirm"), OsStr::new(pkg)]) + } InstallSource::Flatpak => { let id = app.bundle_id(); let user = process::command("flatpak").args(["uninstall", "--user", "-y", "--noninteractive", &id]).status(); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index b36aec2..dae7848 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -80,12 +80,11 @@ impl LauncherUi { Some(artcraft_launcher_core::InstallSource::Msi) => { "Windows will remove it like any installed program. Your documents and settings stay." } - Some(artcraft_launcher_core::InstallSource::Deb | artcraft_launcher_core::InstallSource::Rpm) => { - "Your system's package manager will remove it, and may ask for your password. Your documents and settings stay." - } - Some(artcraft_launcher_core::InstallSource::Pacman) => { - "It was installed from the AUR, so the launcher will show you the pacman command to remove it." - } + Some( + artcraft_launcher_core::InstallSource::Deb + | artcraft_launcher_core::InstallSource::Rpm + | artcraft_launcher_core::InstallSource::Pacman, + ) => "Your system's package manager will remove it, and may ask for your password. Your documents and settings stay.", _ => "The app will be removed. Your documents and settings stay.", }; ui.label(RichText::new(how).color(t.text_muted));