From 79f7e7a1e9d83ecc75144ddfb1406c2037c9e476 Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Sun, 31 May 2026 21:27:23 -0500 Subject: [PATCH] fix(desktop): make locally-built macOS app relaunchable after in-place self-update (#36198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On macOS the desktop app is built locally and ad-hoc signed (no Developer ID on the user's machine). An ad-hoc bundle has no stable Designated Requirement, so when the self-updater rebuilds it in place with a fresh build (new cdhash) — plus the com.apple.quarantine flag inherited from the downloaded installer process chain — Gatekeeper/LaunchServices treats the changed code as tampering and macOS reports "Hermes is damaged and can't be opened," and the app fails to relaunch. First launch works (fresh registration); the in-place update relaunch is what breaks. Fix: after building the desktop app locally, strip quarantine xattrs and re-apply a clean deep ad-hoc signature (omitting the hardened-runtime flag, which an ad-hoc build can't satisfy). Applied in both build entry points: - hermes_cli/main.py cmd_gui (the `hermes desktop --build-only` path the updater drives) — so the fix ships via `hermes update` (git), no installer re-download needed. - scripts/install.sh install_desktop (first install) for parity. Both are no-ops on non-macOS and when a real signing identity (CSC_LINK / APPLE_SIGNING_IDENTITY) is configured, so signed/notarized builds are untouched. --- hermes_cli/main.py | 42 ++++++++++++++++++++++++++++++++++++++++++ scripts/install.sh | 12 ++++++++++++ 2 files changed, 54 insertions(+) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 1c3c6c202..3c02c9818 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -6891,6 +6891,43 @@ def _desktop_packaged_executable(desktop_dir: Path) -> Optional[Path]: return max(existing, key=lambda p: p.stat().st_mtime) +def _desktop_macos_relaunchable_fixup(desktop_dir: Path) -> None: + """Make a locally-built (unsigned) macOS desktop app survive in-place self-update. + + An ad-hoc-signed .app has no stable Designated Requirement (no Team ID), so + when the self-updater rebuilds the bundle in place with a fresh build (a new, + different cdhash) Gatekeeper/LaunchServices treats the changed code as + tampering and macOS reports "Hermes is damaged and can't be opened." The + bundle also inherits the com.apple.quarantine flag from the downloaded + installer process chain. Both make the relaunch fail. + + Clearing the quarantine xattrs and re-applying a clean deep ad-hoc signature + (omitting the hardened-runtime flag, which is meaningless without a real + Developer ID) lets the rebuilt app relaunch. No-op when a real signing + identity is configured (CSC_LINK / APPLE_SIGNING_IDENTITY) so a properly + signed/notarized build is never clobbered. Best-effort: never raises. + """ + if sys.platform != "darwin": + return + if os.environ.get("CSC_LINK") or os.environ.get("APPLE_SIGNING_IDENTITY"): + return + exe = _desktop_packaged_executable(desktop_dir) + if exe is None: + return + # exe = .../Hermes.app/Contents/MacOS/Hermes -> app bundle = .../Hermes.app + app = exe.parents[2] + if not str(app).endswith(".app") or not app.is_dir(): + return + codesign = shutil.which("codesign") + if not codesign: + return + try: + subprocess.run(["xattr", "-cr", str(app)], check=False) + subprocess.run([codesign, "--force", "--deep", "--sign", "-", str(app)], check=False) + except Exception as exc: + print(f" (warning: macOS relaunch fixup skipped: {exc})") + + def cmd_gui(args): """Build and launch the native Electron desktop GUI.""" desktop_dir = PROJECT_ROOT / "apps" / "desktop" @@ -6964,6 +7001,11 @@ def cmd_gui(args): print(f" Run manually: cd apps/desktop && npm run {build_script}") sys.exit(build_result.returncode or 1) packaged_executable = _desktop_packaged_executable(desktop_dir) + if not source_mode: + # Locally-built apps are ad-hoc signed; make them relaunchable after + # an in-place self-update (otherwise macOS reports "Hermes is + # damaged"). No-op on non-macOS and on real-identity builds. + _desktop_macos_relaunchable_fixup(desktop_dir) # --build-only: produce the artifact but do NOT launch. The installer's # --update flow drives the rebuild headlessly and then launches the desktop diff --git a/scripts/install.sh b/scripts/install.sh index 1e38f4612..706ca36d3 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -2390,6 +2390,18 @@ install_desktop() { fi log_success "Desktop app built: $app" + # macOS: make the locally-built (ad-hoc) app relaunchable after an in-place + # self-update. An ad-hoc bundle has no stable Designated Requirement, so a + # later in-place rebuild (new cdhash) plus the inherited quarantine flag + # trips Gatekeeper's tamper check ("Hermes is damaged and can't be opened"). + # Strip quarantine + re-apply a clean deep ad-hoc signature (no + # hardened-runtime flag, which an ad-hoc build can't satisfy). Skipped when a + # real signing identity is configured so a signed build isn't clobbered. + if [ "$OS" = "macos" ] && [ -z "${CSC_LINK:-}" ] && [ -z "${APPLE_SIGNING_IDENTITY:-}" ] && command -v codesign >/dev/null 2>&1; then + xattr -cr "$app" 2>/dev/null || true + codesign --force --deep --sign - "$app" >/dev/null 2>&1 || true + fi + # `npm install` + `npm run pack` rewrite lockfiles; restore them so the # checkout stays clean for the next `hermes update`. restore_dirty_lockfiles "$INSTALL_DIR"