From 0c0a70774424fddf502fc2f5dfc755d6c9430932 Mon Sep 17 00:00:00 2001 From: Gille <4317663+helix4u@users.noreply.github.com> Date: Fri, 5 Jun 2026 19:05:32 -0600 Subject: [PATCH] fix(desktop): repair macOS updater helper (#40217) --- .../src-tauri/src/paths.rs | 29 +++++++++++++++++++ apps/desktop/electron/main.cjs | 26 +++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/apps/bootstrap-installer/src-tauri/src/paths.rs b/apps/bootstrap-installer/src-tauri/src/paths.rs index ad5112e7109..c9171f361ce 100644 --- a/apps/bootstrap-installer/src-tauri/src/paths.rs +++ b/apps/bootstrap-installer/src-tauri/src/paths.rs @@ -17,6 +17,8 @@ //! the bootstrap-complete check. use std::path::{Path, PathBuf}; +#[cfg(target_os = "macos")] +use std::process::Command; use tracing_appender::non_blocking::WorkerGuard; /// Returns the canonical Hermes home directory, respecting $HERMES_HOME if set. @@ -103,10 +105,37 @@ pub fn copy_self_to_hermes_home() -> std::io::Result<()> { std::fs::create_dir_all(parent)?; } std::fs::copy(&src, &dest)?; + repair_macos_installer_helper(&dest); tracing::info!(?src, ?dest, "copied installer to HERMES_HOME"); Ok(()) } +#[cfg(target_os = "macos")] +fn repair_macos_installer_helper(path: &Path) { + // The staged helper may inherit quarantine from the downloaded installer. + // Desktop later launches this exact file for in-app updates, so make it + // executable before the update handoff reaches LaunchServices/Gatekeeper. + let _ = Command::new("/usr/bin/xattr") + .args(["-cr"]) + .arg(path) + .status(); + + let verify = Command::new("/usr/bin/codesign") + .arg("--verify") + .arg(path) + .status(); + + if !matches!(verify, Ok(status) if status.success()) { + let _ = Command::new("/usr/bin/codesign") + .args(["--force", "--sign", "-"]) + .arg(path) + .status(); + } +} + +#[cfg(not(target_os = "macos"))] +fn repair_macos_installer_helper(_path: &Path) {} + /// Where install.ps1 writes the bootstrap-complete marker (existence-only file /// the Electron app also checks). Per main.cjs: /// const BOOTSTRAP_COMPLETE_MARKER = path.join(ACTIVE_HERMES_ROOT, '.hermes-bootstrap-complete') diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 91b813d5e81..ce8e4bb83ca 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -1314,6 +1314,31 @@ function resolveUpdaterBinary() { return fileExists(candidate) ? candidate : null } +function repairMacUpdaterHelper(updater) { + if (!IS_MAC || !updater) return + + try { + execFileSync('/usr/bin/xattr', ['-cr', updater], { stdio: 'ignore' }) + } catch (err) { + rememberLog(`[updates] macOS updater helper quarantine repair skipped: ${err.message}`) + } + + try { + execFileSync('/usr/bin/codesign', ['--verify', updater], { stdio: 'ignore' }) + return + } catch { + // Unsigned or invalid helper. Apply a local ad-hoc signature so Gatekeeper + // does not block the staged updater before it can run. + } + + try { + execFileSync('/usr/bin/codesign', ['--force', '--sign', '-', updater], { stdio: 'ignore' }) + rememberLog('[updates] repaired macOS updater helper signature') + } catch (err) { + rememberLog(`[updates] macOS updater helper signature repair skipped: ${err.message}`) + } +} + // Path to the venv shim whose lock decides whether `hermes update` can write // fresh entry points. On Windows this is the file the running backend // `hermes.exe` holds open; on POSIX it's never mandatory-locked. @@ -1474,6 +1499,7 @@ async function applyUpdates(opts = {}) { } emitUpdateProgress({ stage: 'restart', message: 'Handing off to the Hermes updater…', percent: 100 }) + repairMacUpdaterHelper(updater) const updateRoot = resolveUpdateRoot() const { branch: configuredBranch } = readDesktopUpdateConfig()