fix(desktop): launch the installed Hermes.app from hermes desktop

On macOS `hermes desktop` launched the release/ bundle even when an
installed Hermes.app existed, so the app ran from two paths (two Dock
icons, TCC grants keyed to a different bundle) while the one Finder opens
stayed stale. Refresh the installed copies first and launch the one that
now matches the checkout build; release/ stays the fallback.
This commit is contained in:
Brooklyn Nicholson
2026-09-25 00:04:46 -05:00
committed by brooklyn!
parent 53a785449f
commit 831c3d2441
3 changed files with 68 additions and 7 deletions

View File

@@ -953,6 +953,15 @@ def _install_rebuilt_desktop_app(desktop_dir: Path) -> tuple[list[Path], list[st
rebuilt_exe.parents[2], _installed_desktop_apps(), running=_running_macos_app_bundles())
def _refresh_installed_desktop_apps(desktop_dir: Path) -> None:
"""Install the rebuilt bundle over stale installed copies and report each outcome."""
installed, problems = _install_rebuilt_desktop_app(desktop_dir)
for app in installed:
print(f" ✓ Installed the rebuilt Desktop app at {app}")
for problem in problems:
print(f" ⚠ {problem}")
def _update_owned_macos_bundles(candidates: list[Path]) -> list[Path]:
"""The existing bundles in *candidates* that only ``hermes update`` keeps current (#52339).
@@ -988,6 +997,25 @@ def _installed_desktop_apps() -> list[Path]:
return _update_owned_macos_bundles(packaged_gui_app_paths())
def _installed_desktop_launch_target(desktop_dir: Path, packaged_executable: Path) -> Path:
"""The executable ``hermes desktop`` launches: the installed app once it IS the checkout build.
Finder, the Dock and Spotlight open the installed ``Hermes.app``; launching the ``release/``
bundle beside it ran the same app from a second path while the installed copy went stale
(#52339). Refresh the installed copies, then launch the first one that matches the checkout
build. The checkout bundle stays the fallback: nothing installed, or a copy that is running
or could not be replaced.
"""
if sys.platform != "darwin":
return packaged_executable
_refresh_installed_desktop_apps(desktop_dir)
rebuilt_hash = _app_asar_hash(packaged_executable.parents[2])
for app in _installed_desktop_apps():
if rebuilt_hash is not None and _app_asar_hash(app) == rebuilt_hash:
return app / "Contents" / "MacOS" / "Hermes"
return packaged_executable
def _install_rebuilt_macos_bundles(
rebuilt_app: Path, candidates: list[Path], *, running: set[Path]) -> tuple[list[Path], list[str]]:
"""Stage-and-swap ``rebuilt_app`` over each existing bundle in ``candidates`` whose ``app.asar``
@@ -1574,7 +1602,8 @@ def cmd_gui(args: argparse.Namespace):
print(f"✗ Desktop package build completed but no launchable app was found at: {desktop_dir / 'release'}")
print(" Expected an unpacked Electron app for the current OS.")
sys.exit(1)
launch_command = _packaged_desktop_launch_command(packaged_executable)
launch_command = _packaged_desktop_launch_command(
_installed_desktop_launch_target(desktop_dir, packaged_executable))
launch_command.extend(config_electron_flags)
if getattr(args, "local", False):
launch_command.append("--local")

View File

@@ -121,7 +121,7 @@ def build_update_products(project_root: Path, *, desktop: bool) -> None:
publish_stage("Building the web UI")
build_source_web(project_root, env=env)
if desktop:
from hermes_cli.main_desktop import _install_rebuilt_desktop_app, build_prepared_desktop
from hermes_cli.main_desktop import _refresh_installed_desktop_apps, build_prepared_desktop
publish_stage("Building the desktop app")
build_prepared_desktop(
@@ -130,11 +130,7 @@ def build_update_products(project_root: Path, *, desktop: bool) -> None:
)
# A current release/ can still sit beside a stale installed copy (an earlier
# update rebuilt but never installed); healing must not wait for the next build.
installed, problems = _install_rebuilt_desktop_app(project_root / "apps/desktop")
for app in installed:
print(f" ✓ Installed the rebuilt Desktop app at {app}")
for problem in problems:
print(f" ⚠ {problem}")
_refresh_installed_desktop_apps(project_root / "apps/desktop")
# A configured memory provider that no longer ships in core is installed from the
# catalog for every profile home sharing this venv (config, data and tool names
# unchanged). The update must finish even if the migration blows up.

View File

@@ -176,6 +176,42 @@ def test_source_launch_reads_bom_electron_path_without_provisioning(tmp_path, mo
assert len(calls) == 1
def _stamped_macos_bundle(app: Path, asar: bytes) -> Path:
(app / "Contents" / "MacOS").mkdir(parents=True)
(app / "Contents" / "MacOS" / "Hermes").write_bytes(b"\xcf\xfa\xed\xfe")
(app / "Contents" / "Resources").mkdir()
(app / "Contents" / "Resources" / "app.asar").write_bytes(asar)
(app / "Contents" / "Resources" / "install-stamp.json").write_text('{"updateMechanism": "self"}')
return app
@pytest.mark.platforms("macos")
def test_packaged_launch_opens_the_refreshed_installed_app(tmp_path, monkeypatch):
"""#52339: Finder and the Dock open the installed Hermes.app, so ``hermes desktop`` must launch
that copy (brought up to the checkout build) instead of a second bundle under release/."""
import shutil
root = _make_desktop_tree(tmp_path)
_stamped_macos_bundle(root / "apps" / "desktop" / "release" / "mac-arm64" / "Hermes.app", b"checkout build")
installed = _stamped_macos_bundle(tmp_path / "Applications" / "Hermes.app", b"stale build")
monkeypatch.setattr("hermes_cli.gui_uninstall.packaged_gui_app_paths", lambda: [installed])
monkeypatch.setattr("hermes_constants.get_default_hermes_root", lambda **kw: tmp_path) # root is its hermes-agent
monkeypatch.setattr(main_desktop, "_stage_macos_bundle_copy", lambda src, dst: shutil.copytree(src, dst, symlinks=True))
monkeypatch.setattr(main_desktop, "_running_macos_app_bundles", lambda: set())
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
monkeypatch.setattr(main_desktop, "_desktop_launch_env", lambda args: ({}, []))
calls = []
monkeypatch.setattr(main_desktop.subprocess, "run",
lambda cmd, **kw: calls.append(cmd) or subprocess.CompletedProcess(cmd, 0))
with pytest.raises(SystemExit) as exit_info:
main_desktop.cmd_gui(_ns(skip_build=True))
assert exit_info.value.code == 0
assert calls == [[str(installed / "Contents" / "MacOS" / "Hermes")]]
assert (installed / "Contents" / "Resources" / "app.asar").read_bytes() == b"checkout build"
def test_packaged_renderer_bom_does_not_bypass_entry_validation(tmp_path):
import json
import struct