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.
79 lines
3.3 KiB
JavaScript
79 lines
3.3 KiB
JavaScript
'use strict'
|
|
|
|
/**
|
|
* before-pack.cjs — electron-builder beforePack hook.
|
|
*
|
|
* Removes any stale unpacked app directory (`appOutDir`) before
|
|
* electron-builder stages the Electron binaries into it.
|
|
*
|
|
* WHY THIS EXISTS
|
|
* ---------------
|
|
* electron-builder's final packaging step copies the stock `electron`
|
|
* binary into `release/<platform>-unpacked/` and then renames it to the
|
|
* product name (`Hermes`). If a PREVIOUS `npm run pack` was interrupted
|
|
* (Ctrl-C, OOM kill, crash, full disk) the unpacked directory is left in a
|
|
* corrupted partial state: it keeps the already-renamed `LICENSE.electron.txt`
|
|
* and the Chromium payload (.pak/.so/icudtl.dat/chrome-sandbox) but is MISSING
|
|
* the `electron` binary itself.
|
|
*
|
|
* On the next run, electron-builder sees the destination directory already
|
|
* populated, skips re-copying the binary it thinks is present, then tries to
|
|
* rename a `electron` file that no longer exists. The build dies with:
|
|
*
|
|
* ENOENT: no such file or directory, rename
|
|
* '.../release/linux-unpacked/electron' -> '.../release/linux-unpacked/Hermes'
|
|
*
|
|
* This is a hard failure with no obvious cause for the user — `hermes desktop`
|
|
* just prints "Desktop GUI build failed" and the only fix is to manually
|
|
* `rm -rf` the release directory, which a normal user has no way to know.
|
|
*
|
|
* The packaging step is not idempotent across an interrupted run, so we make
|
|
* it idempotent ourselves: wipe the target unpacked directory up front so
|
|
* electron-builder always stages into a clean tree. This is safe — the
|
|
* directory is a pure build artifact that electron-builder fully recreates
|
|
* on every pack; nothing else depends on its prior contents.
|
|
*
|
|
* Cross-platform: the same partial-state trap exists on macOS
|
|
* (the mac-unpacked Hermes.app bundle) and Windows (win-unpacked), so we
|
|
* clean whatever `appOutDir` electron-builder hands us regardless of platform.
|
|
*
|
|
* Best-effort: a cleanup failure must never mask the real build. We log and
|
|
* resolve rather than throw — worst case electron-builder hits the original
|
|
* ENOENT, which is no worse than not having this hook at all.
|
|
*
|
|
* electron-builder passes a context with:
|
|
* - appOutDir: the unpacked app directory about to be staged
|
|
* - electronPlatformName: 'win32' | 'darwin' | 'linux'
|
|
*/
|
|
|
|
const fs = require('node:fs')
|
|
|
|
function cleanStaleAppOutDir(appOutDir) {
|
|
if (!appOutDir || typeof appOutDir !== 'string') {
|
|
return false
|
|
}
|
|
if (!fs.existsSync(appOutDir)) {
|
|
return false
|
|
}
|
|
// Recursive + force so a half-written tree (read-only bits, partial files)
|
|
// can't block the wipe. retry/maxRetries rides out transient EBUSY on
|
|
// Windows where an AV/indexer may briefly hold a handle.
|
|
fs.rmSync(appOutDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
|
return true
|
|
}
|
|
|
|
exports.cleanStaleAppOutDir = cleanStaleAppOutDir
|
|
|
|
exports.default = async function beforePack(context) {
|
|
const appOutDir = context && context.appOutDir
|
|
try {
|
|
if (cleanStaleAppOutDir(appOutDir)) {
|
|
console.log(`[before-pack] removed stale unpacked dir before staging: ${appOutDir}`)
|
|
}
|
|
} catch (err) {
|
|
// Never fail the build over cleanup; surface why so a genuinely stuck
|
|
// directory (permissions, mount) is still diagnosable.
|
|
console.warn(`[before-pack] could not clean ${appOutDir} (${err.message}); continuing`)
|
|
}
|
|
}
|