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 <noreply@anthropic.com>
This commit is contained in:
LyraedanandClaude Opus 5.5 authored and Brandon Thomas committed 2026-10-09 23:37:12 -04:00
1 parent 8143a26014
commit 37a1ef72ad
4 files changed
+70 -25

No files matched your search

+2
View File
@@ -3,6 +3,8 @@ plan/
plans/
/target
target/
/builds
builds/
/dist
/temp
/craft-fonts
+41 -1
View File
@@ -57,10 +57,46 @@ pub fn parse_package(text: &str) -> Option<Version> {
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<Version> {
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.
+22 -18
View File
@@ -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<O
Ok(release::parse_sha256sums(&text).into_iter().find(|(_, n)| *n == asset.name).map(|(h, _)| h))
}
/// When `installed` is owned by a package manager the launcher doesn't drive (the AUR), what to
/// tell the user instead of installing a second copy.
/// When `installed` is owned by something the launcher doesn't drive, what to tell the user
/// instead of installing a second copy. AUR packages are removed through pacman like any system
/// package, but updated by the user's AUR helper, which builds as the user and can't run behind
/// the password prompt.
pub fn managed_elsewhere(app: &CraftApp, installed: &Installed, uninstall: bool) -> Option<String> {
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();
+5 -6
View File
@@ -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));