Skip to content

fix(desktop): recover from corrupt cached Electron download on build - #39032

Merged
teknium1 merged 2 commits into
mainfrom
hermes/hermes-0295e50b
Jun 4, 2026
Merged

fix(desktop): recover from corrupt cached Electron download on build#39032
teknium1 merged 2 commits into
mainfrom
hermes/hermes-0295e50b

Conversation

@teknium1

@teknium1 teknium1 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

hermes desktop now 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's app-builder unpack-electron extracts the distribution from that cached zip (not node_modules); a bad zip yields a partial tree missing the 193 MB electron binary, so the final electronHermes rename dies with ENOENT. 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, honoring electron_config_cache / ELECTRON_CACHE. (contributor)
    • _purge_electron_build_cache(desktop_dir) — on a packaged-build failure, unconditionally remove the cached electron-*.zip plus the half-written *-unpacked/ dir, then the caller retries once. @electron/get re-downloads with its own SHASUM verification. (Teknium, replacing the contributor's testzip()-gated _purge_corrupt_electron_cache)
    • Wire purge → single retry into cmd_gui's pack-failure path (skips --source mode).
  • apps/desktop/scripts/before-pack.cjs (new) — electron-builder beforePack hook wipes the target unpacked dir before staging, making packaging idempotent across interrupted runs. (contributor)
  • apps/desktop/package.json — register the beforePack hook.
  • Tests: pytest for the purge helper (incl. the prepend-junk case) + retry/no-retry cmd_gui paths; node --test for 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 stdlib zipfile reads 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() returns None (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

Before After
hermes desktop, corrupt cache ENOENT … rename 'electron' -> 'Hermes', wedged forever purge + retry → clean re-download → launches
prepend/concat corruption (reported case) testzip() passes → never recovers purged unconditionally → recovers
tests/hermes_cli/test_gui_command.py 23 passed
node --test before-pack.test.cjs 4 passed

Closes #37544. Supersedes #37545 (contributor authorship preserved via cherry-pick).


cc @emozilla @OutThisLife for review.

Infographic

desktop-build-self-heal

0xharryriddle and others added 2 commits June 4, 2026 05:41
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.
@teknium1
teknium1 requested a review from a team June 4, 2026 12:49
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-0295e50b vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9793 on HEAD, 9792 on base (🆕 +1)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5085 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard labels Jun 4, 2026
@teknium1
teknium1 merged commit fef04a1 into main Jun 4, 2026
23 checks passed
@teknium1
teknium1 deleted the hermes/hermes-0295e50b branch June 4, 2026 14:17
@youngstar-eth

Copy link
Copy Markdown
Contributor

Confirming this root cause from a real-world macOS hit, plus two gaps I noticed reading the diff.

Repro (macOS 15, arm64) — corroborates the diagnosis

The corrupt cache file was ~/Library/Caches/electron/electron-v40.9.3-darwin-arm64.zip at 228,590,759 B — ~2× the valid 114,295,383 B. unzip -t:

bad zipfile offset (local header sig): 114295376
error: invalid compressed data to inflate

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 @electron/get hash subdir alongside it; app-builder used the corrupt loose top-level zip, and extraction dropped only Contents/MacOS/Electron (CRC failure on that one member) while every other entry extracted fine → the ElectronHermes rename ENOENT. Manually replacing the zip + clearing release/ fixed it — exactly what this PR automates.

_electron_download_cache_dirs()'s macOS path (~/Library/Caches/electron) and rglob("electron-*.zip") (catches both the loose top-level and the hash-subdir copies) are both spot on. 👍

Gap 1 — the first-install / bootstrap path isn't covered, only hermes desktop

The purge+retry is wired into cmd_gui only. The initial install path is:

scripts/install.sh  →  run_stage_body() "desktop)"  →  install_desktop()

install_desktop() (install.sh ~L2295) runs npm run pack once and returns 1 on failure with no purge/retry, and this PR doesn't touch install.sh. That's exactly where a corrupt cached zip first bites — my own failure was the bootstrap desktop stage, not hermes desktop.

The new beforePack hook does fire for install.sh's npm run pack too, so stale-unpacked-dir corruption is handled in both paths — but a corrupt zip at first install still hard-fails the installer; the user only self-heals if they later run hermes desktop.

Was leaving install.sh out intentional, or should install_desktop() get the same recovery (shell out to the Python helper, or mirror the purge+retry)?

Gap 2 — minor: *-unpacked glob misses macOS output dirs

In _purge_electron_build_cache, release_dir.glob("*-unpacked") matches linux-unpacked / win-unpacked but not the macOS dirs (mac, mac-arm64, mac-universal). The docstring says this clears the unpacked dir "even if the hook is somehow skipped", but on macOS that fallback is a no-op — beforePack still covers the normal path, and the zip purge (the real fix) works via rglob, so impact is low. The test also only exercises linux-unpacked. Could add glob("mac*") alongside, plus a macOS/Windows case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: hermes desktop build fails on Linux with ENOENT: rename 'electron' -> 'Hermes' when the cached Electron download is corrupt

5 participants