Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5461,6 +5461,25 @@ def cmd_gui(args: argparse.Namespace):
print(" ⚠ Dependency install failed with a missing Electron dist; "
"continuing to the build so electron-builder can attempt "
"the Electron fetch itself.")
elif not source_mode and _electron_pkg_staged_missing_dist(PROJECT_ROOT):
# `npm ci` succeeded but left ``node_modules/electron/dist`` empty.
# This is the steady-state cost of ``hermes update`` reinstalling
# repo-root deps with ``--workspaces=false`` (it does NOT reinstall
# the desktop workspace, so electron's postinstall — which fetches
# dist/ — never runs on update; #47917). electron-builder would
# then fall through to a fresh ``@electron/get`` download on every
# single update ("no local electron dist; …will fetch"), turning a
# near-no-op rebuild into a slow network round-trip — or a hard
# failure where GitHub's release host is blocked. Repopulate dist/
# proactively (canonical host, then mirror) so the first pack reuses
# the local binary instead of racing the network.
if _try_redownload_electron_dist(PROJECT_ROOT, env):
print(" ⚠ Electron dist was missing after install (update reinstalls "
"repo-root deps only); repopulated it before building.")
else:
print(" ⚠ Electron dist missing after install and could not be "
"pre-fetched; continuing so electron-builder can attempt "
"the Electron fetch itself.")

build_label = "source build" if source_mode else "packaged app"
print(f"→ Building desktop {build_label}...")
Expand Down
77 changes: 77 additions & 0 deletions tests/hermes_cli/test_gui_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,83 @@ def test_gui_source_mode_uses_renderer_build_and_electron(tmp_path, monkeypatch)
assert mock_run.call_args_list[1].kwargs["cwd"] == desktop_dir


def test_gui_prefetches_electron_dist_after_clean_install_before_pack(tmp_path, monkeypatch):
"""`npm ci` succeeds but leaves dist/ empty -> repopulate BEFORE the first pack.

This is the #47917 residual: `hermes update` reinstalls repo-root deps with
``--workspaces=false``, so electron's dist-fetching postinstall never runs on
update and ``node_modules/electron/dist`` is empty even though the install
succeeded. Without a proactive repair, electron-builder silently falls
through to a fresh ``@electron/get`` download on every update. The first pack
must only run after dist/ has been repopulated.
"""
root = _make_desktop_tree(tmp_path)
desktop_dir = root / "apps" / "desktop"
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
packaged_exe = _make_packaged_executable(root, monkeypatch)

install_ok = subprocess.CompletedProcess(["npm", "ci"], 0)
pack_ok = subprocess.CompletedProcess(["npm", "run", "pack"], 0)
launch_ok = subprocess.CompletedProcess([str(packaged_exe)], 0)

call_order: list[str] = []

def _record_prefetch(*_a, **_kw):
call_order.append("prefetch")
return True

def _record_run(cmd, *_a, **_kw):
if cmd[:3] == ["/usr/bin/npm", "run", "pack"]:
call_order.append("pack")
return pack_ok
return launch_ok

with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
patch("hermes_cli.main._run_npm_install_deterministic", return_value=install_ok), \
patch("hermes_cli.main._desktop_build_needed", return_value=True), \
patch("hermes_cli.main._electron_pkg_staged_missing_dist", return_value=True), \
patch("hermes_cli.main._try_redownload_electron_dist", side_effect=_record_prefetch) as mock_prefetch, \
patch("hermes_cli.main._stop_desktop_processes_locking_build", return_value=[]), \
patch("hermes_cli.main._write_desktop_build_stamp"), \
patch("hermes_cli.main._desktop_macos_relaunchable_fixup"), \
patch("hermes_cli.main.subprocess.run", side_effect=_record_run), \
pytest.raises(SystemExit) as exc:
cli_main.cmd_gui(_ns())

assert exc.value.code == 0
mock_prefetch.assert_called_once()
# The repair must happen BEFORE the first pack, not as after-failure recovery.
assert call_order[:2] == ["prefetch", "pack"]


def test_gui_skips_electron_prefetch_when_dist_present(tmp_path, monkeypatch):
"""Healthy dist/ after install -> no redundant pre-fetch, straight to pack."""
root = _make_desktop_tree(tmp_path)
desktop_dir = root / "apps" / "desktop"
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
packaged_exe = _make_packaged_executable(root, monkeypatch)

install_ok = subprocess.CompletedProcess(["npm", "ci"], 0)
pack_ok = subprocess.CompletedProcess(["npm", "run", "pack"], 0)
launch_ok = subprocess.CompletedProcess([str(packaged_exe)], 0)

with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
patch("hermes_cli.main._run_npm_install_deterministic", return_value=install_ok), \
patch("hermes_cli.main._desktop_build_needed", return_value=True), \
patch("hermes_cli.main._electron_pkg_staged_missing_dist", return_value=False), \
patch("hermes_cli.main._try_redownload_electron_dist") as mock_prefetch, \
patch("hermes_cli.main._stop_desktop_processes_locking_build", return_value=[]), \
patch("hermes_cli.main._write_desktop_build_stamp"), \
patch("hermes_cli.main._desktop_macos_relaunchable_fixup"), \
patch("hermes_cli.main.subprocess.run", side_effect=[pack_ok, launch_ok]) as mock_run, \
pytest.raises(SystemExit) as exc:
cli_main.cmd_gui(_ns())

assert exc.value.code == 0
mock_prefetch.assert_not_called()
assert mock_run.call_args_list[0].args[0] == ["/usr/bin/npm", "run", "pack"]


@pytest.mark.parametrize(
"argv",
[
Expand Down
Loading