fix(desktop): re-download Electron binary via mirror when pack fails (#47266) (#47276)

* fix(desktop): re-download Electron binary via mirror when pack fails (#47266)

Since #38673 pinned build.electronDist to node_modules/electron/dist,
electron-builder reads the Electron binary straight from there and never
downloads it during `npm run pack`. That dist tree is only produced by the
electron package's postinstall (install.js) during `npm ci`. When that
download is blocked or throttled (GitHub's release host is unreachable in
some regions), the dist is missing and the build dies with:

    The specified electronDist does not exist: .../node_modules/electron/dist

The existing ELECTRON_MIRROR fallback in all three desktop-build paths
(scripts/install.ps1, scripts/install.sh, and `hermes desktop` in
hermes_cli/main.py) re-ran `npm run pack` with ELECTRON_MIRROR set — but
pack never downloads Electron anymore, so the mirror was never used and the
retry re-read the same missing dist. The fallback was effectively dead.

Drive the mirror through electron's own downloader instead:

- Add a dist-presence check + a downloader helper (Test-ElectronDist /
  Restore-ElectronDist, _electron_dist_ok / _restore_electron_dist,
  _electron_dist_ok / _redownload_electron_dist) that wipes a partial dist
  + the path.txt version marker (electron's install.js short-circuits on it)
  and re-runs `node install.js`, optionally via a mirror.
- On the first retry, repopulate a missing dist from the canonical source;
  on the mirror retry, re-fetch through npmmirror.com, then pack.
- Gate the re-download on the dist check so an unrelated build failure
  (tsc/vite) doesn't trigger a pointless ~200 MB refetch, and skip the final
  pack when the binary still can't be fetched instead of failing the same way.

* test(desktop): cover Electron dist re-download mirror fallback (#47266)

Add behavior coverage for the electronDist re-download fix:

- _electron_dist_ok across linux/win32/darwin, including the partial-dist
  case (dir present but binary missing) that makes the pinned electronDist
  fail.
- _redownload_electron_dist: no-op when the binary is present, bail when
  install.js is absent, wipe a stale dist + path.txt marker and run
  electron's downloader with ELECTRON_MIRROR injected, and report failure
  when the download still produces no binary.
- `hermes desktop`: the mirror fallback now drives electron's own downloader
  before re-running pack, and skips the final pack entirely when the binary
  can't be fetched.

Replaces the old mirror test that asserted the (now-fixed) dead behavior of
re-running `npm run pack` with ELECTRON_MIRROR set — pack never downloads
Electron under the pinned electronDist, so that retry could never help.
This commit is contained in:
xxxigm 2026-06-17 03:40:55 +07:00 committed by GitHub
parent db44af004c
commit d1ecebcbfd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 431 additions and 28 deletions

View file

@ -498,9 +498,10 @@ def test_gui_retries_pack_once_after_purging_build_cache(tmp_path, monkeypatch):
assert mock_run.call_args_list[2].args[0] == [str(packaged_exe)]
def test_gui_falls_back_to_mirror_when_purge_finds_nothing(tmp_path, monkeypatch, capsys):
"""Purge clears nothing (not a cache problem) → fall back to an Electron
mirror once before failing, so a GitHub-blocked download self-heals."""
def test_gui_redownloads_electron_via_mirror_then_repacks(tmp_path, monkeypatch, capsys):
"""Purge clears nothing and the pinned electronDist (#38673) is missing →
the mirror fallback must drive electron's own downloader (NOT another pack,
which never downloads Electron) and only then retry pack (#47266)."""
root = _make_desktop_tree(tmp_path)
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
_make_packaged_executable(root, monkeypatch, platform="linux")
@ -512,21 +513,59 @@ def test_gui_falls_back_to_mirror_when_purge_finds_nothing(tmp_path, monkeypatch
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_macos_relaunchable_fixup"), \
patch("hermes_cli.main._purge_electron_build_cache", return_value=[]) as mock_purge, \
patch("hermes_cli.main._purge_electron_build_cache", return_value=[]), \
patch("hermes_cli.main._electron_dist_ok", return_value=False), \
patch("hermes_cli.main._redownload_electron_dist", side_effect=[False, True]) as mock_dl, \
patch("hermes_cli.main.subprocess.run", side_effect=[pack_fail, pack_fail]) as mock_run, \
pytest.raises(SystemExit) as exc:
cli_main.cmd_gui(_ns())
assert exc.value.code == 1
mock_purge.assert_called_once()
# pack(fail) → purge(nothing) → pack via mirror(fail) = 2 subprocess.run calls
# initial pack + mirror pack = 2 npm calls. The first-retry pack is skipped
# because the canonical-source re-download (no mirror) failed, so there was
# never a binary to build against.
assert mock_run.call_count == 2
# The retry runs the same build but with ELECTRON_MIRROR injected.
# First re-download attempt is canonical (no mirror); the second drives the
# public mirror.
assert mock_dl.call_args_list[0].kwargs.get("mirror") is None
assert mock_dl.call_args_list[1].kwargs["mirror"]
# Only the mirror-driven pack carries ELECTRON_MIRROR.
assert "ELECTRON_MIRROR" not in (mock_run.call_args_list[0].kwargs.get("env") or {})
assert mock_run.call_args_list[1].kwargs["env"]["ELECTRON_MIRROR"]
assert "Desktop GUI build failed" in capsys.readouterr().out
def test_gui_skips_pack_when_electron_redownload_unrecoverable(tmp_path, monkeypatch, capsys):
"""When the Electron binary can't be fetched at all (mirror also blocked),
skip the pointless final pack it would just re-throw the same missing
electronDist and fail with a clear message instead."""
root = _make_desktop_tree(tmp_path)
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
_make_packaged_executable(root, monkeypatch, platform="linux")
monkeypatch.delenv("ELECTRON_MIRROR", raising=False)
install_ok = subprocess.CompletedProcess(["npm", "ci"], 0)
pack_fail = subprocess.CompletedProcess(["npm", "run", "pack"], 1)
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_macos_relaunchable_fixup"), \
patch("hermes_cli.main._purge_electron_build_cache", return_value=[]), \
patch("hermes_cli.main._electron_dist_ok", return_value=False), \
patch("hermes_cli.main._redownload_electron_dist", return_value=False), \
patch("hermes_cli.main.subprocess.run", side_effect=[pack_fail]) as mock_run, \
pytest.raises(SystemExit) as exc:
cli_main.cmd_gui(_ns())
assert exc.value.code == 1
# Only the initial pack ran; both retries were skipped because no binary
# could be produced.
assert mock_run.call_count == 1
out = capsys.readouterr().out
assert "Could not re-download Electron from the mirror" in out
assert "Desktop GUI build failed" in out
def test_gui_does_not_override_user_electron_mirror(tmp_path, monkeypatch, capsys):
"""A user-pinned ELECTRON_MIRROR is respected: no extra mirror fallback
attempt (and we never swap in our default mirror)."""
@ -553,6 +592,108 @@ def test_gui_does_not_override_user_electron_mirror(tmp_path, monkeypatch, capsy
assert "Desktop GUI build failed" in capsys.readouterr().out
# ── electronDist (re)download helper tests (#47266) ───────────────────
@pytest.mark.parametrize(
"platform,rel",
[
("linux", "dist/electron"),
("win32", "dist/electron.exe"),
("darwin", "dist/Electron.app/Contents/MacOS/Electron"),
],
)
def test_electron_dist_ok_per_platform(tmp_path, monkeypatch, platform, rel):
monkeypatch.setattr(cli_main.sys, "platform", platform)
electron = tmp_path / "node_modules" / "electron"
# A dist dir that exists but lacks the binary is NOT ok (partial extraction).
(electron / "dist").mkdir(parents=True)
assert cli_main._electron_dist_ok(tmp_path) is False
binp = electron / rel
binp.parent.mkdir(parents=True, exist_ok=True)
binp.write_text("", encoding="utf-8")
assert cli_main._electron_dist_ok(tmp_path) is True
def test_redownload_electron_dist_noop_when_present(tmp_path, monkeypatch):
"""Already-healthy dist → no download, so an unrelated build failure can't
trigger a needless ~200 MB refetch."""
monkeypatch.setattr(cli_main.sys, "platform", "linux")
binp = tmp_path / "node_modules" / "electron" / "dist" / "electron"
binp.parent.mkdir(parents=True)
binp.write_text("", encoding="utf-8")
with patch("hermes_cli.main.subprocess.run") as mock_run:
assert cli_main._redownload_electron_dist(tmp_path, {}) is True
mock_run.assert_not_called()
def test_redownload_electron_dist_missing_installer(tmp_path, monkeypatch):
"""No electron/install.js (deps never installed) → nothing to run."""
monkeypatch.setattr(cli_main.sys, "platform", "linux")
(tmp_path / "node_modules" / "electron").mkdir(parents=True)
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/node"), \
patch("hermes_cli.main.subprocess.run") as mock_run:
assert cli_main._redownload_electron_dist(tmp_path, {}) is False
mock_run.assert_not_called()
def test_redownload_electron_dist_runs_installer_with_mirror(tmp_path, monkeypatch):
"""Missing dist → wipe any partial dist + version marker, run electron's own
install.js with ELECTRON_MIRROR injected, and report success on the binary."""
monkeypatch.setattr(cli_main.sys, "platform", "linux")
electron = tmp_path / "node_modules" / "electron"
electron.mkdir(parents=True)
(electron / "install.js").write_text("// stub", encoding="utf-8")
# A stale partial dist + version marker that MUST be cleared first, otherwise
# electron's install.js short-circuits on path.txt and never re-downloads.
(electron / "dist").mkdir()
(electron / "dist" / "leftover").write_text("junk", encoding="utf-8")
(electron / "path.txt").write_text("electron", encoding="utf-8")
captured = {}
def fake_run(cmd, **kwargs):
captured["cmd"] = cmd
captured["env"] = kwargs.get("env")
captured["cwd"] = kwargs.get("cwd")
# simulate electron's install.js producing the dist binary
binp = electron / "dist" / "electron"
binp.parent.mkdir(parents=True, exist_ok=True)
binp.write_text("", encoding="utf-8")
return subprocess.CompletedProcess(cmd, 0)
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/node"), \
patch("hermes_cli.main.subprocess.run", side_effect=fake_run):
ok = cli_main._redownload_electron_dist(
tmp_path, {"PATH": "/x"}, mirror="https://mirror.example/electron/"
)
assert ok is True
assert captured["cmd"] == ["/usr/bin/node", str(electron / "install.js")]
assert captured["cwd"] == str(electron)
assert captured["env"]["ELECTRON_MIRROR"] == "https://mirror.example/electron/"
# The partial dir + marker were dropped before the re-download.
assert not (electron / "dist" / "leftover").exists()
assert not (electron / "path.txt").exists()
def test_redownload_electron_dist_returns_false_when_download_fails(tmp_path, monkeypatch):
"""install.js ran but produced no binary (still blocked) → False, so the
caller skips a doomed pack."""
monkeypatch.setattr(cli_main.sys, "platform", "linux")
electron = tmp_path / "node_modules" / "electron"
electron.mkdir(parents=True)
(electron / "install.js").write_text("// stub", encoding="utf-8")
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/node"), \
patch("hermes_cli.main.subprocess.run",
return_value=subprocess.CompletedProcess(["node"], 1)):
assert cli_main._redownload_electron_dist(tmp_path, {}) is False
class _FakeProc:
"""Minimal psutil.Process stand-in for the lock-breaker tests."""