fix(desktop): recover from corrupt cached Electron download on build - #39032
Conversation
hermes desktop failed on Linux with an ENOENT renaming release/linux-unpacked/electron -> Hermes. Root cause is a corrupt cached Electron zip (~/.cache/electron/electron-*.zip): app-builder unpack-electron extracts a partial tree from the bad zip that is missing the electron binary, so electron-builder dies on the final rename. Re-running repeats the broken extraction, leaving the desktop app permanently unlaunchable until the cache is manually purged. - Add _electron_download_cache_dirs() + _purge_corrupt_electron_cache() to hermes_cli/main.py: validate every electron-*.zip via zipfile.testzip() and delete corrupt ones; honor electron_config_cache / ELECTRON_CACHE overrides with per-OS defaults. - Wire purge + single retry into cmd_gui packaged-build failure path so a poisoned download self-heals (electron re-downloads clean). - Add beforePack hook (apps/desktop/scripts/before-pack.cjs) to wipe the target unpacked dir before staging, making packaging idempotent across interrupted runs. Cross-platform, best-effort. - Tests: corrupt-zip detector, cmd_gui purge/retry/launch path, no-retry-when-clean path, and node --test for the cleanup helper.
…pfile gate
The salvaged detector validated each cached electron-*.zip with
zipfile.testzip() and only purged ones it judged corrupt. But stdlib
zipfile reads from the end-of-central-directory backward, so it silently
tolerates prepended/concatenated junk — which is exactly the corruption
the bug report names ('86257938 extra bytes at beginning or within
zipfile', a partial download resumed into the same file). testzip()
returns clean on those zips, so the self-heal never fired for the
reported failure mode.
Drop the self-rolled validator: on any packaged-build failure, purge the
version's cached zips AND the half-written unpacked dir, then retry once.
@electron/get re-downloads with its own SHASUM verification — the real
source of truth, which catches prepend/concat/truncate alike. An
unrelated failure just costs one clean re-download and fails the same way.
Verified empirically: zipfile.testzip() returns None (clean) on a
prepended-junk zip; the unconditional purge removes it correctly.
🔎 Lint report:
|
|
Confirming this root cause from a real-world macOS hit, plus two gaps I noticed reading the diff. Repro (macOS 15, arm64) — corroborates the diagnosisThe corrupt cache file was i.e. a valid 114 MB zip with ~114 MB of junk concatenated onto it — a partial download resumed into the same file, exactly as the description says. A valid copy was sitting in the
Gap 1 — the first-install / bootstrap path isn't covered, only
|
Summary
hermes desktopnow self-heals on Linux (and any platform) when the cached Electron download is corrupt, instead of failing the same way forever.Root cause: a corrupt zip in the per-user Electron download cache (
~/.cache/electron/electron-v*.zip) — typically a partial download resumed into the same file, leaving prepended/concatenated junk. electron-builder'sapp-builder unpack-electronextracts the distribution from that cached zip (notnode_modules); a bad zip yields a partial tree missing the 193 MBelectronbinary, so the finalelectron→Hermesrename dies withENOENT. Re-running repeats the broken extraction forever.Salvage of #37545 by @0xharryriddle, with one design change on top (see below).
Changes
hermes_cli/main.py:_electron_download_cache_dirs()— resolve per-OS Electron download cache dirs, honoringelectron_config_cache/ELECTRON_CACHE. (contributor)_purge_electron_build_cache(desktop_dir)— on a packaged-build failure, unconditionally remove the cachedelectron-*.zipplus the half-written*-unpacked/dir, then the caller retries once.@electron/getre-downloads with its own SHASUM verification. (Teknium, replacing the contributor'stestzip()-gated_purge_corrupt_electron_cache)cmd_gui's pack-failure path (skips--sourcemode).apps/desktop/scripts/before-pack.cjs(new) — electron-builderbeforePackhook wipes the target unpacked dir before staging, making packaging idempotent across interrupted runs. (contributor)apps/desktop/package.json— register thebeforePackhook.cmd_guipaths;node --testfor the cleanup hook.Why the design change
The contributor's detector validated each zip with
zipfile.testzip()and only purged ones it judged corrupt. But stdlibzipfilereads from the end-of-central-directory backward, so it silently tolerates the prepended/concatenated junk that is the exact corruption the issue names ("86257938 extra bytes at beginning or within zipfile"). Verified empirically:testzip()returnsNone(clean) on a prepended-junk zip, so the original self-heal would never fire for the reported user.Dropping the self-rolled validator is simpler and more robust: don't try to prove corruption, just clear the cache on failure and let
@electron/get's SHASUM check (the real source of truth) re-validate on re-download. Catches prepend/concat/truncate alike. An unrelated failure costs one clean re-download and fails the same way.Validation
hermes desktop, corrupt cacheENOENT … rename 'electron' -> 'Hermes', wedged forevertestzip()passes → never recoverstests/hermes_cli/test_gui_command.pynode --test before-pack.test.cjsCloses #37544. Supersedes #37545 (contributor authorship preserved via cherry-pick).
cc @emozilla @OutThisLife for review.
Infographic