mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
* 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:
parent
db44af004c
commit
d1ecebcbfd
4 changed files with 431 additions and 28 deletions
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue