fix(updater): disable in-app updater on non-AppImage Linux (#4874) (#4897)

Tauri's Linux updater can only self-update AppImage bundles, so deb/rpm/
pacman and Flatpak installs showed a "Software Update" prompt that could
never apply. READEST_DISABLE_UPDATER also had no effect: the variable
reached the process, but its value only flowed to the frontend through a
WebView init-script global (window.__READEST_UPDATER_DISABLED) that is
not reliably visible to page scripts on Linux/WebKitGTK.

Make the decision authoritative in Rust and read it over IPC:

- Add compute_updater_disabled (pure, unit-tested) plus the
  is_updater_disabled desktop command: an env opt-out, Flatpak, or a
  Linux non-AppImage install disables the updater. setup() reuses the
  same helper for the init-script global.
- NativeAppService.init() sets hasUpdater from the command for desktop
  apps instead of relying on the init-script global.

Non-AppImage Linux installs now defer to the system package manager and
fall back to the "What's New" release notes.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-07-03 01:55:02 +09:00
committed by GitHub
parent bd415a8501
commit 77ea87c344
3 changed files with 221 additions and 11 deletions
+91 -11
View File
@@ -247,6 +247,52 @@ fn get_executable_dir() -> String {
.unwrap_or_default()
}
// Pure decision for whether the in-app updater should be hidden. Kept
// dependency-free so it can be unit tested for every platform combination.
//
// - `env_disable`: READEST_DISABLE_UPDATER is set (explicit opt-out).
// - Linux only: Tauri's updater can self-update AppImage bundles *only*, so
// deb/rpm/pacman (`!is_appimage`) and Flatpak installs are updated by the
// system package manager and must not show the in-app updater.
#[cfg(desktop)]
fn compute_updater_disabled(
env_disable: bool,
is_linux: bool,
is_flatpak: bool,
is_appimage: bool,
) -> bool {
env_disable || (is_linux && (is_flatpak || !is_appimage))
}
#[cfg(desktop)]
fn updater_disabled() -> bool {
let env_disable = std::env::var("READEST_DISABLE_UPDATER").is_ok();
#[cfg(target_os = "linux")]
{
let is_flatpak =
std::env::var("FLATPAK_ID").is_ok() || std::path::Path::new("/.flatpak-info").exists();
let is_appimage = std::env::var("APPIMAGE").is_ok()
|| std::env::current_exe()
.map(|path| path.to_string_lossy().contains("/tmp/.mount_"))
.unwrap_or(false);
compute_updater_disabled(env_disable, true, is_flatpak, is_appimage)
}
#[cfg(not(target_os = "linux"))]
{
compute_updater_disabled(env_disable, false, false, false)
}
}
// Authoritative source of truth for the frontend `hasUpdater` capability.
// Read via IPC in `NativeAppService.init()` so the decision does not depend on
// the injected init-script global, which is not reliably visible to page
// scripts on every Linux/WebKitGTK setup (see issue #4874).
#[cfg(desktop)]
#[tauri::command]
fn is_updater_disabled() -> bool {
updater_disabled()
}
#[derive(Clone, serde::Serialize)]
#[allow(dead_code)]
struct SingleInstancePayload {
@@ -273,6 +319,8 @@ pub fn run() {
upload_file,
get_environment_variable,
get_executable_dir,
#[cfg(desktop)]
is_updater_disabled,
allow_paths_in_scopes,
dir_scanner::read_dir,
epub_parser::parse_epub_metadata,
@@ -439,18 +487,13 @@ pub fn run() {
#[cfg(not(target_os = "linux"))]
let is_appimage = false;
// Flatpak mounts the app directory read-only, so the bundled updater can
// download but never apply an update. Disable it and leave updates to the
// Flatpak runtime. Detect via FLATPAK_ID or the /.flatpak-info sandbox file.
// The in-app updater is hidden for installs it can't actually update
// (Linux deb/rpm/pacman and Flatpak) and when READEST_DISABLE_UPDATER
// is set. This mirrors the `is_updater_disabled` command that
// `NativeAppService.init()` reads authoritatively; the injected global
// below is only a best-effort fast path.
#[cfg(desktop)]
let updater_disabled = {
#[cfg(target_os = "linux")]
let is_flatpak = std::env::var("FLATPAK_ID").is_ok()
|| std::path::Path::new("/.flatpak-info").exists();
#[cfg(not(target_os = "linux"))]
let is_flatpak = false;
std::env::var("READEST_DISABLE_UPDATER").is_ok() || is_flatpak
};
let updater_disabled = updater_disabled();
#[cfg(not(desktop))]
let updater_disabled = false;
@@ -634,3 +677,40 @@ pub fn run() {
},
);
}
#[cfg(all(test, desktop))]
mod tests {
use super::compute_updater_disabled;
#[test]
fn env_opt_out_disables_on_any_desktop() {
// READEST_DISABLE_UPDATER is an explicit opt-out on every desktop OS.
assert!(compute_updater_disabled(true, false, false, false));
assert!(compute_updater_disabled(true, true, false, true));
}
#[test]
fn linux_system_package_install_is_disabled() {
// deb/rpm/pacman installs are not AppImage and not Flatpak. Tauri's
// Linux updater can't self-update them, so the in-app updater is hidden.
assert!(compute_updater_disabled(false, true, false, false));
}
#[test]
fn linux_flatpak_is_disabled() {
assert!(compute_updater_disabled(false, true, true, false));
}
#[test]
fn linux_appimage_keeps_updater() {
// AppImage is the one Linux bundle Tauri can self-update.
assert!(!compute_updater_disabled(false, true, false, true));
}
#[test]
fn non_linux_desktop_keeps_updater_without_opt_out() {
// macOS / Windows: the flatpak/appimage clause must not apply, so the
// updater stays enabled unless the env opt-out is set.
assert!(!compute_updater_disabled(false, false, false, false));
}
}