fix(desktop): make locally-built macOS app relaunchable after in-place self-update (#36198)

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.
This commit is contained in:
brooklyn!
2026-05-31 21:27:23 -05:00
committed by GitHub
parent a8526a4159
commit 79f7e7a1e9
2 changed files with 54 additions and 0 deletions

View File

@ -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

View File

@ -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"