From 740d0812eefa4f5f6329f9587cf84ab528861eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 21 Apr 2026 01:29:50 -0400 Subject: [PATCH] fix(build): restore package.json if electron-builder leaves it mutated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit electron-builder's `extraMetadata` feature mutates the source package.json in place during packaging and restores it on clean exit. When the builder is killed mid-flight (parent task timeout, Ctrl-C, OOM) the restore never fires and the source tree is left with a stripped scripts block — every subsequent pnpm command then fails with "Missing script" until someone notices the drift and runs `git checkout`. Observed once in this session when a `build:dir` task was cancelled mid-packaging. Wrap the builder invocation in a bytes-level snapshot + idempotent restore, driven by both normal-exit and SIGINT/SIGTERM/SIGHUP handlers. On a clean run the builder's own restore wrote the original bytes back, our final compare is byte-equal, and the restore is a no-op. On an abnormal termination the snapshot overwrites whatever electron-builder left behind so the working tree is guaranteed clean regardless of exit path. The snapshot approach is intentional over a "reject if tree dirty before build" guard: this is a recovery mechanism, not a precondition, and the repo's own `package.json` is the truth — anything else in there at the moment the builder took its snapshot is the desired state. --- scripts/build-desktop.mjs | 55 ++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 6 deletions(-) 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; }