mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-10-14 14:26:48 -04:00
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 <[email protected]>
This commit is contained in:
1 parent
8143a26014
commit
37a1ef72ad
4 files changed
+70
-25
No files matched your search
@@ -3,6 +3,8 @@ plan/
|
|||||||
plans/
|
plans/
|
||||||
/target
|
/target
|
||||||
target/
|
target/
|
||||||
|
/builds
|
||||||
|
builds/
|
||||||
/dist
|
/dist
|
||||||
/temp
|
/temp
|
||||||
/craft-fonts
|
/craft-fonts
|
||||||
@@ -57,10 +57,46 @@ pub fn parse_package(text: &str) -> Option<Version> {
|
|||||||
match parts.as_slice() {
|
match parts.as_slice() {
|
||||||
[a, b, c] => Version::parse(&format!("{a}.{b}.{c}")).ok(),
|
[a, b, c] => Version::parse(&format!("{a}.{b}.{c}")).ok(),
|
||||||
[a, b] => Version::parse(&format!("{a}.{b}.0")).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.
|
/// A version that's a plain `X.Y.Z` with no pre-release suffix.
|
||||||
pub fn is_stable(v: &Version) -> bool {
|
pub fn is_stable(v: &Version) -> bool {
|
||||||
v.pre.is_empty()
|
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"));
|
assert_eq!(p("0.3.0.r47.g47f9306-1").as_deref(), Some("0.3.0"));
|
||||||
// Plain versions (rpm %{VERSION}, Flatpak) pass through.
|
// Plain versions (rpm %{VERSION}, Flatpak) pass through.
|
||||||
assert_eq!(p("0.4.0").as_deref(), Some("0.4.0"));
|
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);
|
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
|
// 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.
|
// latest release always looked out of date.
|
||||||
|
|||||||
@@ -93,11 +93,12 @@ pub fn choose_kinds(existing: Option<&Installed>, record: Option<&Record>, prefe
|
|||||||
InstallSource::Msi => Some(PackageKind::Msi),
|
InstallSource::Msi => Some(PackageKind::Msi),
|
||||||
InstallSource::Deb => Some(PackageKind::Deb),
|
InstallSource::Deb => Some(PackageKind::Deb),
|
||||||
InstallSource::Rpm => Some(PackageKind::Rpm),
|
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::Flatpak => Some(PackageKind::Flatpak),
|
||||||
InstallSource::AppImage => Some(PackageKind::AppImage),
|
InstallSource::AppImage => Some(PackageKind::AppImage),
|
||||||
InstallSource::Binary => Some(PackageKind::TarGz),
|
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)) {
|
if let Some(k) = kind.filter(|k| available.contains(k)) {
|
||||||
return vec![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))
|
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
|
/// When `installed` is owned by something the launcher doesn't drive, what to tell the user
|
||||||
/// tell the user instead of installing a second copy.
|
/// 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> {
|
pub fn managed_elsewhere(app: &CraftApp, installed: &Installed, uninstall: bool) -> Option<String> {
|
||||||
match installed.source {
|
match installed.source {
|
||||||
InstallSource::Pacman => {
|
InstallSource::Pacman if !uninstall => {
|
||||||
let pkg = installed.package.clone().unwrap_or_else(|| app.id.to_string());
|
let pkg = installed.package.clone().unwrap_or_else(|| app.id.to_string());
|
||||||
Some(if uninstall {
|
let helper = ["paru", "yay"].into_iter().find(|h| which(h).is_some());
|
||||||
format!("{} was installed from the AUR ({pkg}). Remove it with: sudo pacman -R {pkg}", app.name)
|
let cmd = match helper {
|
||||||
} else {
|
Some(h) => format!("{h} -S {pkg}"),
|
||||||
let helper = ["paru", "yay"].into_iter().find(|h| which(h).is_some());
|
None => format!("your AUR helper (for example: yay -S {pkg})"),
|
||||||
let cmd = match helper {
|
};
|
||||||
Some(h) => format!("{h} -S {pkg}"),
|
Some(format!("{} was installed from the AUR ({pkg}). Update it with {cmd}", app.name))
|
||||||
None => format!("your AUR helper (for example: yay -S {pkg})"),
|
|
||||||
};
|
|
||||||
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)),
|
InstallSource::Binary => Some(format!("{} was installed outside the launcher. Update or remove it the way it was installed.", app.name)),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -201,7 +200,7 @@ pub fn uninstall(app: &CraftApp, installed: &Installed, record: Option<&Record>)
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
InstallSource::MacApp | InstallSource::Launcher => macos::move_to_trash(&path),
|
InstallSource::MacApp | InstallSource::Launcher => macos::move_to_trash(&path),
|
||||||
#[cfg(target_os = "linux")]
|
#[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")]
|
#[cfg(target_os = "freebsd")]
|
||||||
InstallSource::Launcher => match record {
|
InstallSource::Launcher => match record {
|
||||||
Some(r) => freebsd::remove_files(&r.files, r.needs_root),
|
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> {
|
pub fn remove_package(app: &CraftApp, installed: &Installed) -> Result<(), String> {
|
||||||
match source {
|
match installed.source {
|
||||||
InstallSource::Deb => run_as_root(&[OsStr::new("apt-get"), OsStr::new("remove"), OsStr::new("-y"), OsStr::new(app.id)]),
|
InstallSource::Deb => run_as_root(&[OsStr::new("apt-get"), OsStr::new("remove"), OsStr::new("-y"), OsStr::new(app.id)]),
|
||||||
InstallSource::Rpm => {
|
InstallSource::Rpm => {
|
||||||
if which("dnf").is_some() {
|
if which("dnf").is_some() {
|
||||||
@@ -383,6 +382,11 @@ mod linux {
|
|||||||
run_as_root(&[OsStr::new("rpm"), OsStr::new("-e"), OsStr::new(app.id)])
|
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 => {
|
InstallSource::Flatpak => {
|
||||||
let id = app.bundle_id();
|
let id = app.bundle_id();
|
||||||
let user = process::command("flatpak").args(["uninstall", "--user", "-y", "--noninteractive", &id]).status();
|
let user = process::command("flatpak").args(["uninstall", "--user", "-y", "--noninteractive", &id]).status();
|
||||||
|
|||||||
@@ -80,12 +80,11 @@ impl LauncherUi {
|
|||||||
Some(artcraft_launcher_core::InstallSource::Msi) => {
|
Some(artcraft_launcher_core::InstallSource::Msi) => {
|
||||||
"Windows will remove it like any installed program. Your documents and settings stay."
|
"Windows will remove it like any installed program. Your documents and settings stay."
|
||||||
}
|
}
|
||||||
Some(artcraft_launcher_core::InstallSource::Deb | artcraft_launcher_core::InstallSource::Rpm) => {
|
Some(
|
||||||
"Your system's package manager will remove it, and may ask for your password. Your documents and settings stay."
|
artcraft_launcher_core::InstallSource::Deb
|
||||||
}
|
| artcraft_launcher_core::InstallSource::Rpm
|
||||||
Some(artcraft_launcher_core::InstallSource::Pacman) => {
|
| artcraft_launcher_core::InstallSource::Pacman,
|
||||||
"It was installed from the AUR, so the launcher will show you the pacman command to remove it."
|
) => "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.",
|
_ => "The app will be removed. Your documents and settings stay.",
|
||||||
};
|
};
|
||||||
ui.label(RichText::new(how).color(t.text_muted));
|
ui.label(RichText::new(how).color(t.text_muted));
|
||||||
|
|||||||
Reference in new issue
Block a user