mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-11-02 18:07:04 +00:00
fix(linux): only update release Arch packages and delegate AUR/repo packages (#586)
- Only consider release Arch packages (bundle ID starting with 'ai.storyteller.') eligible for launcher updates.
- Keep AUR packages (<app>, <app>-bin, <app>-git) directed to the user's AUR helper (yay/paru).
- Direct repository packages (e.g. Omarchy) to 'sudo pacman -Syu' and add translations across all 10 locales.
- Only self-update launcher if installed as the release Arch package ('ai.storyteller.artcraft-launcher'), preventing conflicts on AUR installs.
- Check pacman DB presence before assuming ArchPkg availability.
This commit is contained in:
@@ -17,7 +17,8 @@ pub enum InstallSource {
|
||||
Deb,
|
||||
/// An RPM package.
|
||||
Rpm,
|
||||
/// An Arch Linux package (from the AUR: `<app>`, `<app>-bin` or `<app>-git`).
|
||||
/// An Arch Linux package: the release's own (`ai.storyteller.<app>`), or `<app>`, `<app>-bin`
|
||||
/// or `<app>-git` from the AUR or a repository.
|
||||
Pacman,
|
||||
/// A Flatpak.
|
||||
Flatpak,
|
||||
@@ -35,7 +36,7 @@ impl InstallSource {
|
||||
Self::Msi => "Windows Installer",
|
||||
Self::Deb => "Debian package",
|
||||
Self::Rpm => "RPM package",
|
||||
Self::Pacman => "Arch package (AUR)",
|
||||
Self::Pacman => "Arch package",
|
||||
Self::Flatpak => "Flatpak",
|
||||
Self::AppImage => "AppImage",
|
||||
Self::Binary => "System binary",
|
||||
|
||||
@@ -327,7 +327,7 @@ pub mod linux {
|
||||
}
|
||||
|
||||
/// The local package database of Arch's pacman.
|
||||
const PACMAN_DB: &str = "/var/lib/pacman/local";
|
||||
pub(crate) const PACMAN_DB: &str = "/var/lib/pacman/local";
|
||||
|
||||
/// True when `db` is a directory, i.e. this system really uses pacman.
|
||||
pub(crate) fn pacman_db_present(db: &Path) -> bool {
|
||||
|
||||
@@ -255,7 +255,6 @@ fn record(version: &str, kind: PackageKind, path: PathBuf, files: Vec<PathBuf>,
|
||||
/// stays a `.deb` update), else the user's choice, else the best this system supports.
|
||||
pub fn choose_kinds(existing: Option<&Installed>, record: Option<&Record>, preferred: &[PackageKind]) -> Vec<PackageKind> {
|
||||
let available = available_kinds();
|
||||
let can_pacman = which("pacman").is_some();
|
||||
if let Some(inst) = existing {
|
||||
let kind = match inst.source {
|
||||
InstallSource::Launcher => record.map(|r| r.kind),
|
||||
@@ -263,17 +262,20 @@ 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),
|
||||
InstallSource::Pacman => Some(PackageKind::ArchPkg),
|
||||
// The release's own Arch package is updated like a .deb. AUR builds are updated by the
|
||||
// user's AUR helper (see `managed_elsewhere`); the launcher opens and removes them but
|
||||
// never replaces them.
|
||||
InstallSource::Pacman if is_release_arch_package(inst) => Some(PackageKind::ArchPkg),
|
||||
InstallSource::Pacman => None,
|
||||
InstallSource::Flatpak => Some(PackageKind::Flatpak),
|
||||
InstallSource::AppImage => Some(PackageKind::AppImage),
|
||||
InstallSource::Binary => Some(PackageKind::TarGz),
|
||||
};
|
||||
if let Some(k) = kind.filter(|k| available.contains(k) || (*k == PackageKind::ArchPkg && can_pacman)) {
|
||||
if let Some(k) = kind.filter(|k| available.contains(k)) {
|
||||
return vec![k];
|
||||
}
|
||||
}
|
||||
let mut kinds: Vec<PackageKind> =
|
||||
preferred.iter().copied().filter(|k| available.contains(k) || (*k == PackageKind::ArchPkg && can_pacman)).collect();
|
||||
let mut kinds: Vec<PackageKind> = preferred.iter().copied().filter(|k| available.contains(k)).collect();
|
||||
for k in available {
|
||||
if !kinds.contains(&k) {
|
||||
kinds.push(k);
|
||||
@@ -282,6 +284,14 @@ pub fn choose_kinds(existing: Option<&Installed>, record: Option<&Record>, prefe
|
||||
kinds
|
||||
}
|
||||
|
||||
/// Whether a pacman install is the release's own Arch package rather than an AUR build. The
|
||||
/// release names it by the bundle id (`ai.storyteller.wordcraft`), which no AUR package uses
|
||||
/// (they are `wordcraft`, `wordcraft-bin` and `wordcraft-git`), so an AUR helper never mistakes it
|
||||
/// for one of its own and rebuilds it from source.
|
||||
pub fn is_release_arch_package(installed: &Installed) -> bool {
|
||||
installed.source == InstallSource::Pacman && installed.package.as_deref().is_some_and(|p| p.starts_with("ai.storyteller."))
|
||||
}
|
||||
|
||||
/// Package formats this machine can install, best first.
|
||||
pub fn available_kinds() -> Vec<PackageKind> {
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -335,29 +345,57 @@ fn published_checksum(release: &Release, asset: &Asset, http: &Http) -> Result<O
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// instead of installing a second copy. Arch packages other than the release's own are removed
|
||||
/// through pacman like any system package, but updated by whatever they came from: a system
|
||||
/// update for a repository package (Omarchy's, Chaotic-AUR, ...), the user's AUR helper (which
|
||||
/// builds as the user and can't run behind the password prompt) for anything else.
|
||||
pub fn managed_elsewhere(app: &CraftApp, installed: &Installed, uninstall: bool) -> Option<String> {
|
||||
match installed.source {
|
||||
InstallSource::Pacman if !uninstall => {
|
||||
InstallSource::Pacman if !uninstall && !is_release_arch_package(installed) => {
|
||||
let pkg = installed.package.clone().unwrap_or_else(|| app.id.to_string());
|
||||
if pkg.ends_with("-git") || which("pacman").is_none() {
|
||||
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))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some(pacman_update_hint(app, &pkg, pacman_repo(&pkg).as_deref()))
|
||||
}
|
||||
InstallSource::Binary => Some(format!("{} was installed outside the launcher. Update or remove it the way it was installed.", app.name)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// How to update an Arch package the launcher doesn't own: `repo` is the sync repository it
|
||||
/// comes from, or `None` for a foreign (AUR) package.
|
||||
fn pacman_update_hint(app: &CraftApp, pkg: &str, repo: Option<&str>) -> String {
|
||||
if let Some(repo) = repo {
|
||||
return format!("{} was installed from the {repo} repository ({pkg}). Update it with sudo pacman -Syu", 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})"),
|
||||
};
|
||||
format!("{} was installed from the AUR ({pkg}). Update it with {cmd}", app.name)
|
||||
}
|
||||
|
||||
/// The sync repository (`extra`, `omarchy`, ...) that carries the installed package `pkg`, or
|
||||
/// `None` when no configured repository does (an AUR or locally built package) or pacman can't
|
||||
/// be asked.
|
||||
fn pacman_repo(pkg: &str) -> Option<String> {
|
||||
let pacman = which("pacman")?;
|
||||
// pacman translates its field names; the C locale keeps "Repository" parseable.
|
||||
let out = process::command(pacman).args(["-Si", "--", pkg]).env("LC_ALL", "C").output().ok()?;
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
parse_pacman_repo(&String::from_utf8_lossy(&out.stdout))
|
||||
}
|
||||
|
||||
/// The `Repository` field of `pacman -Si` output (the first one, when several repos carry it:
|
||||
/// that is the one pacman installs from).
|
||||
fn parse_pacman_repo(si: &str) -> Option<String> {
|
||||
si.lines().find_map(|l| {
|
||||
let (key, value) = l.split_once(':')?;
|
||||
(key.trim() == "Repository").then(|| value.trim().to_string()).filter(|r| !r.is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes an installed copy. `phase` hears when the removal has to wait for another install;
|
||||
/// `stop` cancels it while it waits.
|
||||
pub fn uninstall(
|
||||
@@ -710,7 +748,10 @@ pub(crate) mod linux {
|
||||
let root = root && !immutable_ostree();
|
||||
let can_deb = root && native == Some(PackageKind::Deb) && which("apt-get").is_some() && which("dpkg").is_some();
|
||||
let can_rpm = root && native == Some(PackageKind::Rpm) && which("rpm").is_some();
|
||||
let can_arch = root && native == Some(PackageKind::ArchPkg) && which("pacman").is_some();
|
||||
let can_arch = root
|
||||
&& native == Some(PackageKind::ArchPkg)
|
||||
&& which("pacman").is_some()
|
||||
&& crate::detect::linux::pacman_db_present(Path::new(crate::detect::linux::PACMAN_DB));
|
||||
order_available_kinds(is_flatpak, is_appimage, flatpak, can_deb, can_rpm, can_arch)
|
||||
}
|
||||
|
||||
@@ -2092,6 +2133,42 @@ mod tests {
|
||||
let _ = inst;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_release_arch_package_is_updated_by_the_launcher() {
|
||||
let pacman = |name: &str| Installed { version: None, source: InstallSource::Pacman, location: String::new(), package: Some(name.into()) };
|
||||
assert!(is_release_arch_package(&pacman("ai.storyteller.wordcraft")));
|
||||
for aur in ["wordcraft", "wordcraft-bin", "wordcraft-git"] {
|
||||
assert!(!is_release_arch_package(&pacman(aur)), "{aur}");
|
||||
}
|
||||
let app = artcraft_launcher_core::CraftApp::by_id("wordcraft").unwrap();
|
||||
assert_eq!(managed_elsewhere(app, &pacman("ai.storyteller.wordcraft"), false), None);
|
||||
assert_eq!(managed_elsewhere(app, &pacman("wordcraft-bin"), true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arch_packages_from_a_repository_are_updated_with_the_system() {
|
||||
let app = artcraft_launcher_core::CraftApp::by_id("wordcraft").unwrap();
|
||||
assert_eq!(
|
||||
pacman_update_hint(app, "wordcraft-bin", Some("omarchy")),
|
||||
"WordCraft was installed from the omarchy repository (wordcraft-bin). Update it with sudo pacman -Syu"
|
||||
);
|
||||
assert!(pacman_update_hint(app, "wordcraft-bin", None).starts_with("WordCraft was installed from the AUR (wordcraft-bin)."));
|
||||
|
||||
let si = "Repository : omarchy\nName : wordcraft-bin\nVersion : 0.6.1-1\n";
|
||||
assert_eq!(parse_pacman_repo(si).as_deref(), Some("omarchy"));
|
||||
assert_eq!(parse_pacman_repo("Name : wordcraft-bin\n"), None);
|
||||
assert_eq!(parse_pacman_repo("Repository :\n"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aur_packages_are_not_updated_as_arch_pkg() {
|
||||
let pacman = |name: &str| Installed { version: None, source: InstallSource::Pacman, location: String::new(), package: Some(name.into()) };
|
||||
let app = artcraft_launcher_core::CraftApp::by_id("filmcraft").unwrap();
|
||||
let installed = pacman("filmcraft-bin");
|
||||
assert!(managed_elsewhere(app, &installed, false).is_some());
|
||||
assert!(!choose_kinds(Some(&installed), None, &[]).contains(&PackageKind::ArchPkg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_refuses_to_downgrade_when_target_release_is_older() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -151,7 +151,11 @@ fn detect(exe: &Path) -> Option<Install> {
|
||||
if process::capture("rpm", &["-q", LAUNCHER.id], t).is_some() {
|
||||
return Some(Install::Package(PackageKind::Rpm, exe.to_path_buf()));
|
||||
}
|
||||
if process::capture("pacman", &["-Q", LAUNCHER.id], t).is_some() || process::capture("pacman", &["-Q", &LAUNCHER.bundle_id()], t).is_some() {
|
||||
// The release's own Arch package (named by the bundle id). An AUR or repository package
|
||||
// (`artcraft-launcher`, `artcraft-launcher-bin`) is left to pacman and the AUR helper.
|
||||
if crate::detect::linux::pacman_db_present(Path::new(crate::detect::linux::PACMAN_DB))
|
||||
&& process::capture("pacman", &["-Q", &LAUNCHER.bundle_id()], t).is_some()
|
||||
{
|
||||
return Some(Install::Package(PackageKind::ArchPkg, exe.to_path_buf()));
|
||||
}
|
||||
None
|
||||
|
||||
@@ -266,6 +266,7 @@ filter Installed Nainstalováno
|
||||
@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) Aplikace {name} byla nainstalována ve verzi {version} (toto vydání neobsahuje kontrolní součet, ověřena byla pouze velikost)
|
||||
@msg {name} was removed Aplikace {name} byla odebrána
|
||||
@msg {name} is running. Quit it, then try again. Aplikace {name} je spuštěná. Ukončete ji a zkuste to znovu.
|
||||
@msg {name} was installed from the {repo} repository ({pkg}). Update it with sudo pacman -Syu Aplikace {name} byla nainstalována z repozitáře {repo} ({pkg}). Aktualizujte ji příkazem sudo pacman -Syu
|
||||
@id app.install Nainstalovat
|
||||
@id app.update Aktualizovat na {v}
|
||||
@id app.open Otevřít
|
||||
|
||||
|
@@ -280,6 +280,7 @@ filter Installed Installiert
|
||||
@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) {name} installiert: {version} (diese Version veröffentlicht keine Prüfsumme, daher wurde nur die Größe geprüft)
|
||||
@msg {name} was removed {name} wurde entfernt
|
||||
@msg {name} is running. Quit it, then try again. {name} läuft. Beende die App und versuche es erneut.
|
||||
@msg {name} was installed from the {repo} repository ({pkg}). Update it with sudo pacman -Syu {name} wurde aus dem Repository {repo} installiert ({pkg}). Aktualisiere die App mit sudo pacman -Syu
|
||||
|
||||
# Command IDs for menu items (@id)
|
||||
# Format: @id<TAB>command_id<TAB>translation
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 11 and column 16.
|
@@ -279,6 +279,7 @@ filter Installed Instaladas
|
||||
@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) {name} instalada: {version} (esta versión no publica la suma de comprobación, así que solo se comprobó su tamaño)
|
||||
@msg {name} was removed Se quitó {name}
|
||||
@msg {name} is running. Quit it, then try again. {name} está abierta. Ciérrala y vuelve a intentarlo.
|
||||
@msg {name} was installed from the {repo} repository ({pkg}). Update it with sudo pacman -Syu {name} se instaló desde el repositorio {repo} ({pkg}). Actualízala con sudo pacman -Syu
|
||||
@id app.install Instalar
|
||||
@id app.update Actualizar a {v}
|
||||
@id app.open Abrir
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 48.
|
@@ -278,6 +278,7 @@ filter Installed Installées
|
||||
@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) {name} installé : {version} (cette version ne publie pas de somme de contrôle : seule sa taille a été vérifiée)
|
||||
@msg {name} was removed {name} a été supprimé
|
||||
@msg {name} is running. Quit it, then try again. {name} est en cours d'exécution. Quittez-le, puis réessayez.
|
||||
@msg {name} was installed from the {repo} repository ({pkg}). Update it with sudo pacman -Syu {name} a été installé depuis le dépôt {repo} ({pkg}). Mettez-le à jour avec sudo pacman -Syu
|
||||
|
||||
# Command IDs for menu items (@id)
|
||||
# Format: @id<TAB>command_id<TAB>translation
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 50.
|
@@ -294,6 +294,7 @@ filter Installed Telepítve
|
||||
@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) {name} telepítve: {version} (ehhez a kiadáshoz nem adtak meg ellenőrzőösszeget, ezért csak a fájl méretét ellenőriztük)
|
||||
@msg {name} was removed {name} eltávolítva
|
||||
@msg {name} is running. Quit it, then try again. {name} fut. Zárd be, majd próbáld újra.
|
||||
@msg {name} was installed from the {repo} repository ({pkg}). Update it with sudo pacman -Syu {name} a(z) {repo} tárolóból lett telepítve ({pkg}). Frissítsd ezzel: sudo pacman -Syu
|
||||
|
||||
# Command IDs for menu items (@id)
|
||||
# Format: @id<TAB>command_id<TAB>translation
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 11 and column 16.
|
@@ -296,6 +296,7 @@ filter Installed インストール済み
|
||||
@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) {name} をインストールしました: {version}(このリリースはチェックサムを公開していないため、サイズのみ確認しました)
|
||||
@msg {name} was removed {name} をアンインストールしました
|
||||
@msg {name} is running. Quit it, then try again. {name} が実行中です。終了してから再試行してください。
|
||||
@msg {name} was installed from the {repo} repository ({pkg}). Update it with sudo pacman -Syu {name} は {repo} リポジトリからインストールされています ({pkg})。sudo pacman -Syu で更新してください。
|
||||
|
||||
# Command IDs for menu items (@id)
|
||||
# Format: @id<TAB>command_id<TAB>translation
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 14 and column 44.
|
@@ -295,6 +295,7 @@ filter Installed Zainstalowane
|
||||
@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) Zainstalowano {name} w wersji {version} (to wydanie nie zawiera sumy kontrolnej, więc sprawdzono tylko rozmiar)
|
||||
@msg {name} was removed Usunięto {name}
|
||||
@msg {name} is running. Quit it, then try again. {name} jest uruchomiona. Zamknij ją i spróbuj ponownie.
|
||||
@msg {name} was installed from the {repo} repository ({pkg}). Update it with sudo pacman -Syu {name} została zainstalowana z repozytorium {repo} ({pkg}). Zaktualizuj ją poleceniem sudo pacman -Syu
|
||||
|
||||
# Command IDs for menu items (@id)
|
||||
# Format: @id<TAB>command_id<TAB>translation
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 11 and column 15.
|
@@ -268,6 +268,7 @@ filter Installed Instalados
|
||||
@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) {name} instalado: {version} (este lançamento não publica soma de verificação, então apenas o tamanho foi verificado)
|
||||
@msg {name} was removed {name} foi removido
|
||||
@msg {name} is running. Quit it, then try again. {name} está em execução. Feche-o e tente novamente.
|
||||
@msg {name} was installed from the {repo} repository ({pkg}). Update it with sudo pacman -Syu {name} foi instalado pelo repositório {repo} ({pkg}). Atualize-o com sudo pacman -Syu
|
||||
|
||||
# Command IDs for menu items (@id)
|
||||
# Format: @id<TAB>command_id<TAB>translation
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 252 and column 66.
|
@@ -277,6 +277,7 @@ filter Installed Установлены
|
||||
@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) Приложение {name} установлено: {version} (в этом релизе нет контрольных сумм, поэтому проверен только размер)
|
||||
@msg {name} was removed Приложение {name} удалено
|
||||
@msg {name} is running. Quit it, then try again. Приложение {name} запущено. Закройте его и попробуйте снова.
|
||||
@msg {name} was installed from the {repo} repository ({pkg}). Update it with sudo pacman -Syu Приложение {name} установлено из репозитория {repo} ({pkg}). Обновите его командой sudo pacman -Syu
|
||||
|
||||
# Command IDs for menu items (@id)
|
||||
# Format: @id<TAB>command_id<TAB>translation
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 13 and column 44.
|
@@ -314,6 +314,7 @@ figcraft Interface design and prototyping 界面设计与原型制作
|
||||
@msg {name} installed: {version} (this release publishes no checksum, so only its size was checked) {name} 已安装:{version}(此版本未发布校验和,因此只检查了文件大小)
|
||||
@msg {name} was removed {name} 已卸载
|
||||
@msg {name} is running. Quit it, then try again. {name} 正在运行。请先退出,然后重试。
|
||||
@msg {name} was installed from the {repo} repository ({pkg}). Update it with sudo pacman -Syu {name} 是从 {repo} 软件仓库安装的({pkg})。请使用 sudo pacman -Syu 更新。
|
||||
Install folder 安装文件夹
|
||||
Default install folder 默认安装文件夹
|
||||
System default 系统默认
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 3.
|
Reference in New Issue
Block a user