feat(update): one-click desktop self-update (AppImage, .deb, Windows)

Same change as Guará: update_apply used to flatly refuse to run for
the GUI binary and point the user to a manual download instead. Since
the desktop app already elevates to root before showing its window,
the process applying the update already has everything it needs — no
second authentication round is required.

- New sabia_core::update::apply_desktop[_with_progress], alongside the
  existing CLI-only apply() (left untouched): detects the install kind
  at runtime (AppImage via the APPIMAGE env var, same FUSE-mount issue
  elevated_exe() already works around and this reuses; Deb otherwise
  on Linux; WindowsInstaller on Windows), matches the right release
  asset by the GUI bundle naming (_amd64/_aarch64.AppImage,
  _amd64/_arm64.deb, _x64-setup.exe), verifies SHA-256 when a sidecar
  is published, and installs:
  - AppImage: downloads to a temp file in the same directory, renames
    over the running file, execs it in place (same PID, already root).
  - .deb: runs dpkg -i directly (already root), execs current_exe()
    again now that dpkg replaced it on disk.
  - Windows: runs the installer silently and detached, exits
    immediately to release the file lock. Does not claim to relaunch
    the app afterwards — no documented NSIS or Tauri flag does that;
    needs a real Windows machine to confirm actual post-install
    behavior.
- Engine::update_apply_desktop wires it into the Tauri update_apply
  command (now takes AppHandle, emits update-progress events).
- Fixed a real gap: the existing update button handler changed its own
  label when an update was available but never actually called apply
  — wired it for real, with per-phase progress feedback in the
  interface's existing language (download/verify/install/restart).
- scripts/build-gui.sh: generates a checksum file next to every file
  in dist-gui/, matching the CLI release archive convention.
- 3 new unit tests: install-kind detection, GUI asset suffix matching
  against real release naming, existing CLI-refusal test unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
This commit is contained in:
Lucas RayanandClaude Sonnet 5 committed 2026-10-31 10:18:31 -03:00
1 parent 7d69f2e4b2
commit 6191c7bbc6
6 files changed
+455 -14

No files matched your search

+13 -2
View File
@@ -502,8 +502,19 @@ fn update_check(state: tauri::State<'_, State>) -> Result<serde_json::Value, Str
}
#[tauri::command]
fn update_apply(state: tauri::State<'_, State>) -> Result<String, String> {
state.0.update_apply().map_err(|e| e.to_string())
fn update_apply(app: tauri::AppHandle, state: tauri::State<'_, State>) -> Result<String, String> {
use tauri::Emitter;
#[cfg(any(target_os = "linux", target_os = "macos"))]
let exe = elevated_exe().unwrap_or_else(|_| std::env::current_exe().unwrap_or_default());
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
let exe = std::env::current_exe().unwrap_or_default();
state
.0
.update_apply_desktop(&exe, move |phase| {
let _ = app.emit("update-progress", serde_json::json!({ "phase": phase }));
})
.map_err(|e| e.to_string())
}
/// `true` when the process already has sufficient privileges for all