fix(desktop): ad-hoc sign macOS self-update rebuilds

The desktop self-updater rebuilds and re-signs the .app on each user's own
machine (`hermes desktop --build-only` -> electron-builder `--dir`). With
CSC_IDENTITY_AUTO_DISCOVERY on (its default), electron-builder signs the
type=distribution, hardened-runtime bundle with whatever identity is in that
user's keychain -- typically a personal "Apple Development" cert -- which
stalls/fails the sign step (no Developer ID, no provisioning profile) or
clobbers the original notarized signature with an unusable one, tripping
Gatekeeper on every post-update launch.

Force ad-hoc signing for the local packaged rebuild instead: deterministic,
and exactly what _desktop_macos_relaunchable_fixup already finishes off.
No-op for source runs, off-macOS, when a real identity is configured
(CSC_LINK / APPLE_SIGNING_IDENTITY), or when the caller already pinned the flag.
This commit is contained in:
Brooklyn Nicholson 2026-06-25 12:08:29 -05:00
parent a6a28ce3e2
commit 1d9ed7f48a
2 changed files with 68 additions and 0 deletions

View file

@ -5404,6 +5404,34 @@ def _desktop_macos_relaunchable_fixup(desktop_dir: Path) -> None:
print(f" (warning: macOS relaunch fixup skipped: {exc})")
def _force_adhoc_macos_signing(env: dict, *, source_mode: bool) -> bool:
"""Stop electron-builder grabbing a random keychain identity on self-update.
The desktop self-updater rebuilds *and re-signs the .app on the end user's
machine* (``hermes desktop --build-only`` electron-builder ``--dir``).
With ``CSC_IDENTITY_AUTO_DISCOVERY`` on (its default), electron-builder
signs the ``type=distribution``, hardened-runtime bundle with whatever it
finds in that user's keychain — typically a personal "Apple Development"
cert. That stalls/fails the sign step (no Developer ID + no provisioning
profile) or clobbers your real notarized signature with an unusable one, so
every post-update launch trips Gatekeeper.
Force ad-hoc signing for the local packaged rebuild instead: deterministic,
and exactly what ``_desktop_macos_relaunchable_fixup`` already finishes off.
No-op for source runs, off-macOS, when a real identity is configured
(``CSC_LINK`` / ``APPLE_SIGNING_IDENTITY``), or when the caller already
pinned the flag. Mutates ``env``; returns True when it set the flag.
"""
if sys.platform != "darwin" or source_mode:
return False
if env.get("CSC_LINK") or env.get("APPLE_SIGNING_IDENTITY"):
return False
if "CSC_IDENTITY_AUTO_DISCOVERY" in env:
return False
env["CSC_IDENTITY_AUTO_DISCOVERY"] = "false"
return True
def _desktop_linux_sandbox_fixup(packaged_executable: Path) -> bool:
"""Configure Electron's Linux SUID sandbox helper when required."""
if sys.platform != "linux":
@ -5535,6 +5563,9 @@ def cmd_gui(args: argparse.Namespace):
build_label = "source build" if source_mode else "packaged app"
print(f"→ Building desktop {build_label}...")
build_script = "build" if source_mode else "pack"
if _force_adhoc_macos_signing(env, source_mode=source_mode):
print(" → No Developer ID configured; ad-hoc signing this local rebuild "
"(CSC_IDENTITY_AUTO_DISCOVERY=false)")
if not source_mode:
# A running desktop instance launched from release/win-unpacked
# holds Hermes.exe locked on Windows, so the pack can't replace

View file

@ -877,3 +877,40 @@ def test_stop_desktop_build_lock_no_release_dir(tmp_path, monkeypatch):
with patch("psutil.process_iter") as it:
assert cli_main._stop_desktop_processes_locking_build(desktop_dir) == []
it.assert_not_called()
def test_force_adhoc_signing_disables_discovery_on_local_packaged_rebuild(monkeypatch):
monkeypatch.setattr(cli_main.sys, "platform", "darwin")
env = {}
assert cli_main._force_adhoc_macos_signing(env, source_mode=False) is True
assert env["CSC_IDENTITY_AUTO_DISCOVERY"] == "false"
@pytest.mark.parametrize("platform", ["linux", "win32"])
def test_force_adhoc_signing_noop_off_macos(monkeypatch, platform):
monkeypatch.setattr(cli_main.sys, "platform", platform)
env = {}
assert cli_main._force_adhoc_macos_signing(env, source_mode=False) is False
assert "CSC_IDENTITY_AUTO_DISCOVERY" not in env
def test_force_adhoc_signing_noop_for_source_mode(monkeypatch):
monkeypatch.setattr(cli_main.sys, "platform", "darwin")
env = {}
assert cli_main._force_adhoc_macos_signing(env, source_mode=True) is False
assert "CSC_IDENTITY_AUTO_DISCOVERY" not in env
@pytest.mark.parametrize("key", ["CSC_LINK", "APPLE_SIGNING_IDENTITY"])
def test_force_adhoc_signing_preserves_real_identity(monkeypatch, key):
monkeypatch.setattr(cli_main.sys, "platform", "darwin")
env = {key: "secret"}
assert cli_main._force_adhoc_macos_signing(env, source_mode=False) is False
assert "CSC_IDENTITY_AUTO_DISCOVERY" not in env
def test_force_adhoc_signing_respects_explicit_caller_flag(monkeypatch):
monkeypatch.setattr(cli_main.sys, "platform", "darwin")
env = {"CSC_IDENTITY_AUTO_DISCOVERY": "true"}
assert cli_main._force_adhoc_macos_signing(env, source_mode=False) is False
assert env["CSC_IDENTITY_AUTO_DISCOVERY"] == "true"