fix(desktop): retry the Electron mirror only when the Electron distribution is missing
The pack-recovery mirror rung fired on "the pack failed and the staging output holds no packaged executable". Every failure before electron-builder is also true of that condition, and no mirror can repair a compile, bundler or native-link failure — the rung just re-ran the whole pack while printing that the Electron download from GitHub looked blocked. The retry now also requires _electron_dist_ok to be false, matching the gate the re-download rung already used. Late failures are unchanged: a packaged executable in place still means exactly one attempt (#40187).
This commit is contained in:
@@ -1454,6 +1454,11 @@ def _run_desktop_pack_with_recovery(
|
||||
A MISSING exe is the signature of the corrupt-download class; a late failure
|
||||
(e.g. macOS signing) leaves it in place and a redownload retry would only
|
||||
repeat the same slow failure.
|
||||
|
||||
Both rungs additionally require the Electron distributable to be MISSING.
|
||||
"No staged exe" is also true of every failure before electron-builder ever
|
||||
runs (compile, bundler, native link), and switching mirrors cannot repair
|
||||
those — it just re-runs the whole pack behind a message blaming GitHub.
|
||||
"""
|
||||
from hermes_cli.main import PROJECT_ROOT
|
||||
def _staged_exe() -> Optional[Path]:
|
||||
@@ -1487,13 +1492,13 @@ def _run_desktop_pack_with_recovery(
|
||||
build_result.returncode != 0
|
||||
and staging_dir is not None
|
||||
and not env.get("ELECTRON_MIRROR")
|
||||
and _staged_exe() is None):
|
||||
and _staged_exe() is None
|
||||
and not _electron_dist_ok(PROJECT_ROOT)):
|
||||
print(" ⚠ Desktop build still failing; the Electron download from "
|
||||
"GitHub looks blocked. Re-downloading via a public mirror "
|
||||
"(npmmirror.com)... (set ELECTRON_MIRROR to use another mirror)")
|
||||
mirror_env = {**npm_build_env, "ELECTRON_MIRROR": _ELECTRON_FALLBACK_MIRROR}
|
||||
if not _electron_dist_ok(PROJECT_ROOT):
|
||||
_redownload_electron_dist(PROJECT_ROOT, env, mirror=_ELECTRON_FALLBACK_MIRROR)
|
||||
_redownload_electron_dist(PROJECT_ROOT, env, mirror=_ELECTRON_FALLBACK_MIRROR)
|
||||
_stop_desktop_processes_locking_build(desktop_dir)
|
||||
build_result = _pack(mirror_env)
|
||||
return build_result
|
||||
|
||||
@@ -337,6 +337,115 @@ def test_gui_does_not_retry_after_packaged_executable_exists(tmp_path, monkeypat
|
||||
assert "Desktop GUI build failed" in capsys.readouterr().out
|
||||
|
||||
|
||||
def _make_electron_dist(root: Path) -> Path:
|
||||
"""Lay a complete Electron install where ``_electron_dist_ok`` looks on THIS host.
|
||||
|
||||
Resolved through ``_electron_dist_binary()`` rather than a hardcoded path so
|
||||
the fixture and the code agree by construction on every OS lane.
|
||||
"""
|
||||
binary = main_desktop._electron_dist_binary(root)
|
||||
binary.parent.mkdir(parents=True, exist_ok=True)
|
||||
binary.write_text("", encoding="utf-8")
|
||||
return binary
|
||||
|
||||
|
||||
def _pack_that_never_reaches_the_builder():
|
||||
"""A ``subprocess.run`` stand-in for a pack that dies before electron-builder.
|
||||
|
||||
It lays NOTHING down in the staging output — that is the signature of a
|
||||
compile, bundler or native-link failure — and records each attempt's env so
|
||||
a test can see whether the mirror rung was entered.
|
||||
"""
|
||||
attempts: list[dict] = []
|
||||
|
||||
def _run(cmd, **kwargs):
|
||||
if len(cmd) >= 3 and cmd[1:3] == ["run", "pack"]:
|
||||
attempts.append(dict(kwargs.get("env") or {}))
|
||||
return subprocess.CompletedProcess(cmd, 1)
|
||||
return subprocess.CompletedProcess(cmd, 0)
|
||||
|
||||
return attempts, _run
|
||||
|
||||
|
||||
def test_gui_skips_mirror_retry_when_electron_dist_is_intact(tmp_path, monkeypatch, capsys):
|
||||
"""A pack that never reached electron-builder has no mirror problem to repair.
|
||||
|
||||
"The pack failed and the staging output holds no exe" is ALSO true of a
|
||||
compile / bundler / native-link failure, so on its own it cannot select the
|
||||
mirror rung — yet that rung re-ran the whole pack while telling the user the
|
||||
Electron download from GitHub looked blocked. The mirror retry additionally
|
||||
requires the Electron distributable to be missing.
|
||||
"""
|
||||
root = _make_desktop_tree(tmp_path)
|
||||
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
|
||||
_make_electron_dist(root)
|
||||
live_exe = _make_packaged_executable(root, monkeypatch)
|
||||
live_exe.write_text("good build", encoding="utf-8")
|
||||
monkeypatch.delenv("ELECTRON_MIRROR", raising=False)
|
||||
|
||||
attempts, run_pack = _pack_that_never_reaches_the_builder()
|
||||
install_ok = subprocess.CompletedProcess(["npm", "ci"], 0)
|
||||
|
||||
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
|
||||
patch("hermes_cli.main_web_build._run_npm_install_deterministic", return_value=install_ok), \
|
||||
patch("hermes_cli.main_desktop._desktop_macos_relaunchable_fixup"), \
|
||||
patch("hermes_cli.main_desktop._purge_electron_build_cache", return_value=[]) as mock_purge, \
|
||||
patch("hermes_cli.main_desktop._redownload_electron_dist", return_value=True) as mock_dl, \
|
||||
patch("hermes_cli.main.subprocess.run", side_effect=run_pack), \
|
||||
pytest.raises(SystemExit) as exc:
|
||||
cli_main.cmd_gui(_ns())
|
||||
|
||||
assert exc.value.code == 1
|
||||
# One pack, no mirror env on it, no false accusation of a blocked download.
|
||||
assert len(attempts) == 1
|
||||
assert not any(a.get("ELECTRON_MIRROR") for a in attempts)
|
||||
assert "looks blocked" not in capsys.readouterr().out
|
||||
# Nothing recovery-shaped ran either.
|
||||
mock_purge.assert_not_called()
|
||||
mock_dl.assert_not_called()
|
||||
|
||||
|
||||
def test_gui_still_retries_via_mirror_when_electron_dist_is_missing(tmp_path, monkeypatch, capsys):
|
||||
"""The blocked-download recovery the mirror rung exists for still fires.
|
||||
|
||||
Guard so a later tightening of the gate cannot silently drop the last rung:
|
||||
Electron staged its package but ``dist`` was never populated, the pack
|
||||
produces no exe, and the user hasn't pinned a mirror — refresh via
|
||||
npmmirror.com and pack once more.
|
||||
"""
|
||||
root = _make_desktop_tree(tmp_path)
|
||||
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
|
||||
# electron present, dist half-populated (a blocked postinstall download).
|
||||
(root / "node_modules" / "electron" / "dist").mkdir(parents=True)
|
||||
live_exe = _make_packaged_executable(root, monkeypatch)
|
||||
live_exe.write_text("good build", encoding="utf-8")
|
||||
monkeypatch.delenv("ELECTRON_MIRROR", raising=False)
|
||||
|
||||
attempts, run_pack = _pack_that_never_reaches_the_builder()
|
||||
install_ok = subprocess.CompletedProcess(["npm", "ci"], 0)
|
||||
|
||||
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
|
||||
patch("hermes_cli.main_web_build._run_npm_install_deterministic", return_value=install_ok), \
|
||||
patch("hermes_cli.main_desktop._desktop_macos_relaunchable_fixup"), \
|
||||
patch("hermes_cli.main_desktop._purge_electron_build_cache", return_value=[]) as mock_purge, \
|
||||
patch("hermes_cli.main_desktop._redownload_electron_dist", return_value=True) as mock_dl, \
|
||||
patch("hermes_cli.main.subprocess.run", side_effect=run_pack), \
|
||||
pytest.raises(SystemExit) as exc:
|
||||
cli_main.cmd_gui(_ns())
|
||||
|
||||
assert exc.value.code == 1
|
||||
# The whole ladder still runs: pack, refreshed-download retry, mirror retry.
|
||||
assert len(attempts) == 3
|
||||
assert not attempts[0].get("ELECTRON_MIRROR")
|
||||
assert not attempts[1].get("ELECTRON_MIRROR")
|
||||
assert attempts[2].get("ELECTRON_MIRROR") == main_desktop._ELECTRON_FALLBACK_MIRROR
|
||||
assert "looks blocked" in capsys.readouterr().out
|
||||
# And the mirror run reached the dist re-download, mirror in hand.
|
||||
assert mock_purge.called
|
||||
assert any(a.kwargs.get("mirror") == main_desktop._ELECTRON_FALLBACK_MIRROR
|
||||
for a in mock_dl.call_args_list)
|
||||
|
||||
|
||||
|
||||
|
||||
# ── electronDist (re)download helper tests (#47266) ───────────────────
|
||||
|
||||
@@ -596,7 +596,7 @@ clearing the entry — the latch resets and the next boot dials fresh.
|
||||
|
||||
The build downloads the Electron runtime (~114 MB) from `github.com/electron/electron/releases`. If the installer hangs on the **Build desktop app** step with the live output repeating `retrying attempt=…`, GitHub is being blocked or throttled on your network (firewall, proxy, or region).
|
||||
|
||||
The installer self-heals this automatically: on a failed build it (1) clears a corrupt cached Electron zip and retries, then (2) if it still fails and you haven't set `ELECTRON_MIRROR`, retries once more through `npmmirror.com`, the de-facto Electron community mirror. `@electron/get` SHASUM-checks the download, but the checksums come from the same mirror — that catches a corrupt or partial download, not a compromised mirror. If you'd rather not trust a third-party host, pin your own `ELECTRON_MIRROR` (below); the build never overrides one you've set.
|
||||
The installer self-heals this automatically: on a failed build it (1) clears a corrupt cached Electron zip and retries, then (2) if it still fails, the Electron distributable is still missing, and you haven't set `ELECTRON_MIRROR`, retries once more through `npmmirror.com`, the de-facto Electron community mirror. `@electron/get` SHASUM-checks the download, but the checksums come from the same mirror — that catches a corrupt or partial download, not a compromised mirror. If you'd rather not trust a third-party host, pin your own `ELECTRON_MIRROR` (below); the build never overrides one you've set.
|
||||
|
||||
To **choose your own mirror** (e.g. a corporate/trusted one), set `ELECTRON_MIRROR` before installing or rebuild manually — the build honors it and won't override it:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user