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

@ -5110,6 +5110,90 @@ def _purge_electron_build_cache(desktop_dir: Path) -> list[Path]:
return removed
def _electron_dist_binary(project_root: Path) -> Path:
"""Return the path to the Electron main binary inside ``node_modules``.
electron-builder reads the binary from ``build.electronDist``
(``node_modules/electron/dist``) since #38673, so this is the exact file
whose absence makes a pack fail with "The specified electronDist does not
exist". The basename differs per OS (the platform Electron is named for the
host the build runs on).
"""
dist = project_root / "node_modules" / "electron" / "dist"
if sys.platform == "darwin":
return dist / "Electron.app" / "Contents" / "MacOS" / "Electron"
if sys.platform == "win32":
return dist / "electron.exe"
return dist / "electron"
def _electron_dist_ok(project_root: Path) -> bool:
"""True when ``node_modules/electron/dist`` holds a usable Electron binary.
A directory that exists but is missing the binary (a partial extraction from
a corrupt cached zip, or an interrupted postinstall) counts as NOT ok, since
that is exactly the shape that makes electron-builder throw on the pinned
electronDist.
"""
try:
return _electron_dist_binary(project_root).exists()
except OSError:
return False
def _redownload_electron_dist(
project_root: Path,
env: dict,
*,
mirror: Optional[str] = None,
) -> bool:
"""(Re)populate ``node_modules/electron/dist`` via electron's own downloader.
Since #38673 the desktop build pins ``build.electronDist`` to
``node_modules/electron/dist``, so electron-builder reads the Electron binary
straight from there and never downloads it during ``npm run pack``. That dist
tree is 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 #47266), the dist is missing
and re-running ``pack`` only re-throws "The specified electronDist does not
exist". The mirror fallback therefore has to drive *this* downloader, not
another ``pack``.
No-op (returns True) when the dist binary is already present, so an unrelated
build failure doesn't trigger a needless ~200 MB re-download. Otherwise drops
any partial dist + version marker (electron's install.js short-circuits when
``path.txt`` already matches) and runs the downloader once, optionally via a
mirror. Best-effort: never raises. Returns True iff the dist binary exists
afterward.
"""
if _electron_dist_ok(project_root):
return True
electron_dir = project_root / "node_modules" / "electron"
installer = electron_dir / "install.js"
if not installer.is_file():
return False
node = shutil.which("node")
if not node:
return False
dist_dir = electron_dir / "dist"
shutil.rmtree(dist_dir, ignore_errors=True)
try:
(electron_dir / "path.txt").unlink()
except OSError:
pass
dl_env = dict(env)
if mirror:
dl_env["ELECTRON_MIRROR"] = mirror
try:
subprocess.run([node, str(installer)], cwd=str(electron_dir), env=dl_env, check=False)
except OSError:
return False
return _electron_dist_ok(project_root)
def _stop_desktop_processes_locking_build(desktop_dir: Path) -> list[int]:
"""Terminate any running desktop app executing from this build's ``release``
dir so a rebuild can replace its (otherwise locked) executable.
@ -5364,8 +5448,18 @@ def cmd_gui(args: argparse.Namespace):
# failure was something else, the clean re-download is harmless
# and the retry fails the same way.
purged = _purge_electron_build_cache(desktop_dir)
if purged:
print(" ⚠ Desktop build failed; cleared cached Electron download and retrying once...")
# electronDist is pinned to node_modules/electron/dist (#38673):
# electron-builder reads the Electron binary from there and `pack`
# never downloads it, so purging the cache + re-running pack can't
# by itself repopulate a missing/partial dist. When the dist is
# actually gone, re-run electron's own downloader so the retry has
# a binary to read. Gated on the dist check so an unrelated build
# failure (tsc/vite) doesn't trigger a pointless ~200 MB refetch.
restored = False
if not _electron_dist_ok(PROJECT_ROOT):
restored = _redownload_electron_dist(PROJECT_ROOT, env)
if purged or restored:
print(" ⚠ Desktop build failed; refreshed the Electron download and retrying once...")
for p in purged:
print(f" - {p}")
# The purge can't remove a win-unpacked tree whose Hermes.exe
@ -5383,12 +5477,25 @@ def cmd_gui(args: argparse.Namespace):
# trade-off we only make AFTER the canonical GitHub download has
# failed, and we never override a user-pinned ELECTRON_MIRROR.
print(" ⚠ Desktop build still failing; the Electron download from "
"GitHub looks blocked. Retrying once via a public mirror "
"GitHub looks blocked. Re-downloading via a public mirror "
"(npmmirror.com)... (set ELECTRON_MIRROR to use another mirror)")
mirror = "https://npmmirror.com/mirrors/electron/"
mirror_env = dict(env)
mirror_env["ELECTRON_MIRROR"] = "https://npmmirror.com/mirrors/electron/"
_stop_desktop_processes_locking_build(desktop_dir)
build_result = subprocess.run([npm, "run", build_script], cwd=desktop_dir, env=mirror_env, check=False)
mirror_env["ELECTRON_MIRROR"] = mirror
# electronDist is pinned (#38673), so `npm run pack` never
# downloads Electron — the mirror only helps if it drives
# electron's own downloader. Re-fetch the binary through the
# mirror first; otherwise the retry just re-reads the same missing
# dist and re-throws "electronDist does not exist" (#47266).
have_dist = _electron_dist_ok(PROJECT_ROOT)
if not have_dist:
have_dist = _redownload_electron_dist(PROJECT_ROOT, env, mirror=mirror)
if have_dist:
_stop_desktop_processes_locking_build(desktop_dir)
build_result = subprocess.run([npm, "run", build_script], cwd=desktop_dir, env=mirror_env, check=False)
else:
print(" ✗ Could not re-download Electron from the mirror "
"(node_modules/electron/dist still missing)")
if build_result.returncode != 0:
print("✗ Desktop GUI build failed")
print(f" Run manually: cd apps/desktop && npm run {build_script}")

View file

@ -2161,6 +2161,66 @@ function Clear-ElectronBuildCache {
return $removed
}
# True when node_modules\electron\dist holds a usable Electron binary.
# electron-builder reads the binary from build.electronDist
# (node_modules\electron\dist) since #38673, so this is the exact file whose
# absence makes a pack fail with "The specified electronDist does not exist". A
# dist dir that exists but is missing electron.exe (partial extraction / aborted
# postinstall) is NOT ok.
function Test-ElectronDist {
param([string]$InstallDir)
$distExe = Join-Path $InstallDir 'node_modules\electron\dist\electron.exe'
return (Test-Path -LiteralPath $distExe)
}
# (Re)populate node_modules\electron\dist via electron's own downloader.
#
# Since #38673 the desktop build pins build.electronDist to
# node_modules\electron\dist, so electron-builder reads the Electron binary
# straight from there and never downloads it during `npm run pack`. That dist
# tree is produced by the electron package's postinstall (install.js) during
# `npm ci`. When that download is blocked/throttled (GitHub's release host is
# unreachable in some regions - #47266), dist is missing and re-running pack only
# re-throws "The specified electronDist does not exist". The mirror fallback
# therefore has to drive THIS downloader, not another pack.
#
# No-op (returns $true) when the dist binary is already present. Otherwise drops a
# partial dist + version marker (electron's install.js short-circuits when
# path.txt already matches) and runs the downloader once, optionally via a
# mirror. Best-effort: never throws. Returns $true iff the dist binary exists
# afterward.
function Restore-ElectronDist {
param([string]$InstallDir, [string]$Mirror)
if (Test-ElectronDist -InstallDir $InstallDir) { return $true }
$electronDir = Join-Path $InstallDir 'node_modules\electron'
$distExe = Join-Path $electronDir 'dist\electron.exe'
$installer = Join-Path $electronDir 'install.js'
if (-not (Test-Path -LiteralPath $installer)) { return $false }
$node = Get-Command node -ErrorAction SilentlyContinue
if (-not $node) { return $false }
$distDir = Join-Path $electronDir 'dist'
if (Test-Path -LiteralPath $distDir) {
Remove-Item -LiteralPath $distDir -Recurse -Force -ErrorAction SilentlyContinue
}
Remove-Item -LiteralPath (Join-Path $electronDir 'path.txt') -Force -ErrorAction SilentlyContinue
$prevMirror = $env:ELECTRON_MIRROR
if ($Mirror) { $env:ELECTRON_MIRROR = $Mirror }
try {
# Out-Host so the downloader's progress shows on the console WITHOUT
# leaking into this function's return value (PowerShell returns every
# object left on the output stream, so a bare pipe here would make the
# boolean below ambiguous).
& $node.Source $installer 2>&1 | ForEach-Object { "$_" } | Out-Host
} catch {
} finally {
$env:ELECTRON_MIRROR = $prevMirror
}
return (Test-Path -LiteralPath $distExe)
}
function Install-Desktop {
# Build apps/desktop into a launchable Hermes.exe. Only called from
# Stage-Desktop, which is itself only included in the manifest when
@ -2310,8 +2370,19 @@ function Install-Desktop {
# once; @electron/get re-downloads with its own SHASUM check. Without
# this a corrupt download hard-fails the whole installer.
$purged = @(Clear-ElectronBuildCache -DesktopDir $desktopDir)
if ($purged.Count -gt 0) {
Write-Warn "Desktop build failed - cleared cached Electron download, retrying once:"
# electronDist is pinned to node_modules\electron\dist (#38673):
# electron-builder reads the Electron binary from there and `pack`
# never downloads it, so purging the cache + re-running pack can't by
# itself repopulate a missing/partial dist. When the dist is actually
# gone, re-run electron's own downloader so the retry has a binary to
# read. Gated on the dist check so an unrelated build failure
# (tsc/vite) doesn't trigger a pointless ~200MB refetch.
$restored = $false
if (-not (Test-ElectronDist -InstallDir $InstallDir)) {
$restored = Restore-ElectronDist -InstallDir $InstallDir
}
if ($purged.Count -gt 0 -or $restored) {
Write-Warn "Desktop build failed - refreshed the Electron download, retrying once:"
foreach ($p in $purged) { Write-Info " - $p" }
& $npmExe run pack 2>&1 | ForEach-Object { "$_" } | Tee-Object -FilePath $buildLog
$code = $LASTEXITCODE
@ -2326,14 +2397,23 @@ function Install-Desktop {
# trade-off we only make AFTER the canonical GitHub download has failed,
# and we never override a user-pinned ELECTRON_MIRROR.
if ($code -ne 0 -and -not $env:ELECTRON_MIRROR) {
$prevMirror = $env:ELECTRON_MIRROR
$env:ELECTRON_MIRROR = "https://npmmirror.com/mirrors/electron/"
$mirror = "https://npmmirror.com/mirrors/electron/"
Write-Warn "Desktop build still failing - the Electron download from GitHub looks blocked."
Write-Warn "Retrying once via a public Electron mirror ($($env:ELECTRON_MIRROR)):"
Write-Warn "Re-downloading Electron via a public mirror ($mirror), then rebuilding:"
Write-Info " (set ELECTRON_MIRROR yourself to use a different/trusted mirror)"
& $npmExe run pack 2>&1 | ForEach-Object { "$_" } | Tee-Object -FilePath $buildLog
$code = $LASTEXITCODE
$env:ELECTRON_MIRROR = $prevMirror
# electronDist is pinned (#38673), so `npm run pack` never downloads
# Electron - the mirror only helps if it drives electron's own
# downloader. Re-fetch the binary through the mirror first; otherwise
# the retry just re-reads the same missing dist and re-throws
# "The specified electronDist does not exist" (#47266).
$haveDist = Test-ElectronDist -InstallDir $InstallDir
if (-not $haveDist) { $haveDist = Restore-ElectronDist -InstallDir $InstallDir -Mirror $mirror }
if ($haveDist) {
& $npmExe run pack 2>&1 | ForEach-Object { "$_" } | Tee-Object -FilePath $buildLog
$code = $LASTEXITCODE
} else {
Write-Warn "Could not re-download Electron from the mirror (node_modules\electron\dist still missing)"
}
}
$ErrorActionPreference = $prevEAP
if ($code -ne 0) {

View file

@ -2407,6 +2407,58 @@ _desktop_pack() {
# failed, and we never override a user-pinned ELECTRON_MIRROR.
DESKTOP_ELECTRON_FALLBACK_MIRROR="https://npmmirror.com/mirrors/electron/"
# True (returns 0) when node_modules/electron/dist holds a usable Electron
# binary. electron-builder reads the binary from build.electronDist
# (node_modules/electron/dist) since #38673, so this is the exact file whose
# absence makes a pack fail with "The specified electronDist does not exist". A
# dist dir that exists but is missing the binary (partial extraction / aborted
# postinstall) is NOT ok. $1 = the workspace root holding node_modules.
_electron_dist_ok() {
local install_dir="$1"
local electron_dir="$install_dir/node_modules/electron"
if [ "$OS" = "macos" ]; then
[ -e "$electron_dir/dist/Electron.app/Contents/MacOS/Electron" ]
else
[ -e "$electron_dir/dist/electron" ]
fi
}
# (Re)populate node_modules/electron/dist via electron's own downloader.
#
# Since #38673 the desktop build pins build.electronDist to
# node_modules/electron/dist, so electron-builder reads the Electron binary
# straight from there and never downloads it during `npm run pack`. That dist
# tree is produced by the electron package's postinstall (install.js) during
# `npm ci`. When that download is blocked/throttled (GitHub's release host is
# unreachable in some regions - #47266), dist is missing and re-running pack only
# re-throws "The specified electronDist does not exist". The mirror fallback
# therefore has to drive THIS downloader, not another pack.
#
# No-op (returns 0) when the dist binary is already present. Otherwise drops a
# partial dist + version marker (electron's install.js short-circuits when
# path.txt already matches) and runs the downloader once. $1 = the workspace root
# holding node_modules; optional $2 = an ELECTRON_MIRROR base URL. Best-effort:
# returns 0 iff the dist binary exists afterward.
_restore_electron_dist() {
local install_dir="$1"
local mirror="${2:-}"
local electron_dir="$install_dir/node_modules/electron"
_electron_dist_ok "$install_dir" && return 0
[ -f "$electron_dir/install.js" ] || return 1
command -v node >/dev/null 2>&1 || return 1
rm -rf "$electron_dir/dist" 2>/dev/null || true
rm -f "$electron_dir/path.txt" 2>/dev/null || true
if [ -n "$mirror" ]; then
( cd "$electron_dir" && ELECTRON_MIRROR="$mirror" node install.js ) || true
else
( cd "$electron_dir" && node install.js ) || true
fi
_electron_dist_ok "$install_dir"
}
# Build apps/desktop into a launchable native app. Mirrors install.ps1's
# Install-Desktop: a root-level npm install so the apps/* workspace resolves
# the desktop's own deps (Electron ~150MB), then `npm run pack`
@ -2479,8 +2531,19 @@ install_desktop() {
# (b) Corrupt cached Electron zip is the most common self-healable cause.
local purged
purged="$(clear_electron_build_cache "$desktop_dir")"
if [ -n "$purged" ]; then
log_warn "Desktop build failed; cleared cached Electron download and retrying once..."
# electronDist is pinned to node_modules/electron/dist (#38673):
# electron-builder reads the binary from there and `pack` never downloads
# it, so purging the cache + re-running pack can't by itself repopulate a
# missing/partial dist. When the dist is actually gone, re-run electron's
# own downloader so the retry has a binary to read. Gated on the dist
# check so an unrelated build failure (tsc/vite) doesn't trigger a
# pointless ~200MB refetch.
local restored=false
if ! _electron_dist_ok "$INSTALL_DIR"; then
if _restore_electron_dist "$INSTALL_DIR"; then restored=true; fi
fi
if [ -n "$purged" ] || [ "$restored" = true ]; then
log_warn "Desktop build failed; refreshed the Electron download and retrying once..."
if _desktop_pack "$desktop_dir"; then
pack_ok=true
fi
@ -2488,14 +2551,26 @@ install_desktop() {
fi
# (c) Still failing and the user hasn't pinned their own mirror: the GitHub
# release host is likely blocked/throttled. Retry once via a public
# Electron mirror (@electron/get still SHASUM-verifies the download).
# release host is likely blocked/throttled. Re-download the Electron
# binary via a public mirror, then retry. The mirror MUST drive
# electron's own downloader — `npm run pack` reads the pinned electronDist
# and never downloads, so a mirror passed only to pack is a no-op (#47266).
if [ "$pack_ok" = false ] && [ -z "${ELECTRON_MIRROR:-}" ]; then
log_warn "Desktop build still failing — the Electron download from GitHub looks blocked."
log_warn "Retrying once via a public Electron mirror ($DESKTOP_ELECTRON_FALLBACK_MIRROR)..."
log_warn "Re-downloading Electron via a public mirror ($DESKTOP_ELECTRON_FALLBACK_MIRROR), then rebuilding..."
log_warn " (set ELECTRON_MIRROR yourself to use a different/trusted mirror)"
if _desktop_pack "$desktop_dir" "$DESKTOP_ELECTRON_FALLBACK_MIRROR"; then
pack_ok=true
local have_dist=false
if _electron_dist_ok "$INSTALL_DIR"; then
have_dist=true
elif _restore_electron_dist "$INSTALL_DIR" "$DESKTOP_ELECTRON_FALLBACK_MIRROR"; then
have_dist=true
fi
if [ "$have_dist" = true ]; then
if _desktop_pack "$desktop_dir" "$DESKTOP_ELECTRON_FALLBACK_MIRROR"; then
pack_ok=true
fi
else
log_warn "Could not re-download Electron from the mirror (node_modules/electron/dist still missing)"
fi
fi

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."""