From 261d7852cb7c0d74b700935c750e6c97fe03902e Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Wed, 30 Sep 2026 18:46:11 -0400 Subject: [PATCH] fix(updater): package installs update through their package manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On CachyOS the in-app update failed with "Permission denied (os error 13) at path /usr/bin/tauri_current_app…": the app was installed from the Arch package, and Tauri's Linux updater can only replace an AppImage — for anything else it tries to write next to the binary in /usr/bin. - update_install_kind command: "in-app" on Windows and for an AppImage ($APPIMAGE set); on Linux package installs "pacman" (ID/ID_LIKE arch: Arch, CachyOS, Manjaro, EndeavourOS…), "deb" (debian/ubuntu and derivatives) or "manual". The web UI shows the matching update command. - install_update refuses up front on a package install ("install: package-managed (…)") instead of downloading the whole update and failing at the last step. Tests: CachyOS/Arch → pacman; Ubuntu/Debian/Mint → deb; Fedora/unknown → manual; AppImage and Windows → in-app. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src-tauri/src/lib.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d0b9753..f6c1830 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -189,6 +189,47 @@ mod update_retry { } } +/// How this copy of the app gets updated. +/// +/// Tauri's updater can replace the Windows install and a Linux AppImage, but +/// not a copy installed by a package manager: it tries to write next to the +/// binary in /usr/bin and fails with "Permission denied (os error 13)" +/// (reported on CachyOS). Those installs update through their package +/// manager instead; the web UI shows the right command. +pub(crate) fn install_kind(linux: bool, appimage: bool, os_release: &str) -> &'static str { + if !linux || appimage { + return "in-app"; + } + let field = |key: &str| { + os_release + .lines() + .find_map(|l| l.strip_prefix(key).and_then(|v| v.strip_prefix('='))) + .map(|v| v.trim().trim_matches('"').to_ascii_lowercase()) + .unwrap_or_default() + }; + let ids = format!("{} {}", field("ID"), field("ID_LIKE")); + let has = |name: &str| ids.split_whitespace().any(|w| w == name); + if has("arch") { + "pacman" + } else if has("debian") || has("ubuntu") { + "deb" + } else { + "manual" + } +} + +#[tauri::command] +fn update_install_kind() -> &'static str { + let linux = cfg!(target_os = "linux"); + let appimage = std::env::var_os("APPIMAGE").is_some(); + let os_release = if linux { + std::fs::read_to_string("/etc/os-release").unwrap_or_default() + } else { + String::new() + }; + install_kind(linux, appimage, &os_release) +} + #[tauri::command] async fn check_for_update(app: tauri::AppHandle) -> Result { #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -211,6 +252,15 @@ async fn install_update(app: tauri::AppHandle) -> Result<(), String> { { use std::time::{Duration, Instant}; + // A package-manager install can't be replaced in place (see + // install_kind); refuse before downloading anything. + let kind = update_install_kind(); + if kind != "in-app" { + return Err(format!( + "install: package-managed ({kind}): update Lotus Chat with your package manager" + )); + } + let emit = |detail: serde_json::Value| { native::emit_to_web(&app, "lotus-update-progress", &detail.to_string()); }; @@ -939,6 +989,7 @@ pub fn run() { send_notification, check_for_update, install_update, + update_install_kind, native::power::set_call_active, native::jumplist::set_jump_list, native::thumbbar::set_thumbbar, @@ -1333,6 +1384,36 @@ mod webview2_args_tests { } } +#[cfg(test)] +mod install_kind_tests { + use super::install_kind; + + #[test] + fn package_installs_update_through_their_package_manager() { + let cachy = "NAME=\"CachyOS Linux\"\nID=cachyos\nID_LIKE=arch\n"; + let arch = "NAME=\"Arch Linux\"\nID=arch\n"; + let ubuntu = "NAME=\"Ubuntu\"\nID=ubuntu\nID_LIKE=debian\n"; + let debian = "ID=debian\n"; + let mint = "ID=linuxmint\nID_LIKE=\"ubuntu debian\"\n"; + let fedora = "ID=fedora\n"; + assert_eq!(install_kind(true, false, cachy), "pacman"); + assert_eq!(install_kind(true, false, arch), "pacman"); + assert_eq!(install_kind(true, false, ubuntu), "deb"); + assert_eq!(install_kind(true, false, debian), "deb"); + assert_eq!(install_kind(true, false, mint), "deb"); + assert_eq!(install_kind(true, false, fedora), "manual"); + assert_eq!(install_kind(true, false, ""), "manual"); + // ID_LIKE mentioning arch only as part of a longer word doesn't count. + assert_eq!(install_kind(true, false, "ID=x\nID_LIKE=archlike\n"), "manual"); + } + + #[test] + fn appimage_and_windows_update_in_app() { + assert_eq!(install_kind(true, true, "ID=cachyos\nID_LIKE=arch\n"), "in-app"); + assert_eq!(install_kind(false, false, ""), "in-app"); + } +} + #[cfg(test)] mod tray_tests { use super::*;