diff --git a/scripts/build-desktop.mjs b/scripts/build-desktop.mjs index e36f8042..e384667d 100644 --- a/scripts/build-desktop.mjs +++ b/scripts/build-desktop.mjs @@ -1,4 +1,4 @@ -import { mkdir, rm } from "node:fs/promises"; +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { prepareBuildEnv, resolvePublishMode } from "./build/prepare-env.mjs"; @@ -42,9 +42,52 @@ const builderArgs = buildElectronBuilderArgs({ prepared, }); -const result = await runElectronBuilder(builderArgs, prepared.env); -if (result.signal) { - process.kill(process.pid, result.signal); -} else { - process.exit(result.code); +// electron-builder with `extraMetadata` mutates the source package.json in +// place during packaging and restores it when it finishes. If the process is +// killed mid-flight (CI timeout, Ctrl-C, OOM, parent task cancellation) the +// restore step never fires and the source tree is left with a stripped +// scripts block — every subsequent `pnpm` command then fails until someone +// notices the drift and runs `git checkout`. The snapshot + signal-safe +// restore below is idempotent: on a clean exit electron-builder's own +// restore already wrote the original bytes back and this is a no-op; on +// any abnormal termination we guarantee the working tree is clean. +const packageJsonSnapshot = await readFile(packageJsonPath); +let packageJsonRestored = false; +async function restorePackageJsonIfNeeded() { + if (packageJsonRestored) return; + packageJsonRestored = true; + try { + const current = await readFile(packageJsonPath); + if (current.equals(packageJsonSnapshot)) return; + await writeFile(packageJsonPath, packageJsonSnapshot); + console.log("[build-desktop] Restored package.json from snapshot after builder left it mutated."); + } catch (error) { + console.error(`[build-desktop] Failed to restore package.json snapshot: ${error instanceof Error ? error.message : String(error)}`); + } +} + +const signals = ["SIGINT", "SIGTERM", "SIGHUP"]; +const signalHandlers = new Map(); +for (const signal of signals) { + const handler = () => { + void restorePackageJsonIfNeeded().finally(() => { + process.kill(process.pid, signal); + }); + }; + signalHandlers.set(signal, handler); + process.once(signal, handler); +} + +try { + const result = await runElectronBuilder(builderArgs, prepared.env); + await restorePackageJsonIfNeeded(); + for (const [signal, handler] of signalHandlers) process.off(signal, handler); + if (result.signal) { + process.kill(process.pid, result.signal); + } else { + process.exit(result.code); + } +} catch (error) { + await restorePackageJsonIfNeeded(); + throw error; }