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:
committed by
brooklyn!
parent
53a785449f
commit
831c3d2441
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user