Skip to content

Commit 2903008

Browse files
committed
Self-heal the electron binary before dev launch
When npm blocks dependency install scripts (a common global allowScripts policy), electron's postinstall never extracts its binary and `npm run dev` dies with the opaque "Electron failed to install correctly" error, even though the downloaded zip is sitting in the cache. A predev guard now checks for the binary and, when missing, runs the official installer and falls back to extracting the cached zip — turning a confusing node_modules debugging session into a silent auto-repair. Constraint: must not assume install scripts are allowed to run Rejected: documenting a manual rebuild step | every contributor with a script-blocking npm would hit the same wall first Confidence: high Scope-risk: narrow (dev tooling only; ships nothing into the packaged app) Tested: broke the binary then ran the guard (extracts from cache), ran it healthy (silent no-op), and ran npm run dev end-to-end (electron launches via predev) Not-tested: a machine with no cached zip and no network
1 parent e1a825b commit 2903008

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

apps/macos/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"scripts": {
88
"dev:renderer": "vite",
99
"dev:electron": "wait-on -t 60000 http://127.0.0.1:${PASTE_DEV_PORT:-5174}/ && electron .",
10+
"predev": "node scripts/ensure-electron.cjs",
1011
"dev": "concurrently -k \"npm:dev:renderer\" \"npm:dev:electron\"",
1112
"build:renderer": "vite build",
1213
"build": "npm run build:renderer",
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
4+
// Self-heal the Electron binary before `npm run dev`.
5+
//
6+
// When npm is configured to block dependency install scripts (e.g. a global
7+
// allowScripts policy), electron's postinstall never extracts its binary, and
8+
// `electron .` then dies with the cryptic "Electron failed to install
9+
// correctly" error. The downloaded zip is still cached, so this guard extracts
10+
// it deterministically instead of leaving the developer to debug node_modules.
11+
12+
const fs = require("node:fs");
13+
const os = require("node:os");
14+
const path = require("node:path");
15+
const { execFileSync } = require("node:child_process");
16+
17+
const electronDir = path.join(__dirname, "..", "node_modules", "electron");
18+
19+
const log = (msg) => console.log(`[ensure-electron] ${msg}`);
20+
21+
const binaryFromPathFile = () => {
22+
try {
23+
const rel = fs.readFileSync(path.join(electronDir, "path.txt"), "utf8").trim();
24+
if (!rel) return "";
25+
return path.join(electronDir, "dist", rel);
26+
} catch {
27+
return "";
28+
}
29+
};
30+
31+
const isReady = () => {
32+
const bin = binaryFromPathFile();
33+
if (!bin) return false;
34+
try {
35+
fs.accessSync(bin, fs.constants.X_OK);
36+
return true;
37+
} catch {
38+
return false;
39+
}
40+
};
41+
42+
const readExpectedVersion = () => {
43+
try {
44+
const v = fs.readFileSync(path.join(electronDir, "package.json"), "utf8");
45+
return JSON.parse(v).version;
46+
} catch {
47+
return "";
48+
}
49+
};
50+
51+
const cachedZipFor = (version) => {
52+
// @electron/get stores artifacts under the platform cache dir.
53+
const candidates = [
54+
path.join(os.homedir(), "Library", "Caches", "electron"),
55+
path.join(os.homedir(), ".cache", "electron"),
56+
process.env.electron_config_cache || ""
57+
].filter(Boolean);
58+
const name = `electron-v${version}-${process.platform}-${process.arch}.zip`;
59+
for (const dir of candidates) {
60+
const zip = path.join(dir, name);
61+
if (fs.existsSync(zip)) return zip;
62+
}
63+
return "";
64+
};
65+
66+
const main = () => {
67+
if (isReady()) {
68+
return;
69+
}
70+
71+
log("electron binary missing — repairing (install scripts were likely skipped)");
72+
73+
// First try the official installer; it's a no-op when already present.
74+
try {
75+
execFileSync(process.execPath, [path.join(electronDir, "install.js")], {
76+
stdio: "inherit"
77+
});
78+
} catch {
79+
// fall through to manual extraction
80+
}
81+
if (isReady()) {
82+
log("repaired via electron install.js");
83+
return;
84+
}
85+
86+
const version = readExpectedVersion();
87+
const zip = version ? cachedZipFor(version) : "";
88+
if (!zip) {
89+
console.error(
90+
"[ensure-electron] could not find a cached electron zip; run `node node_modules/electron/install.js` with network access, then retry."
91+
);
92+
process.exit(1);
93+
}
94+
95+
const distDir = path.join(electronDir, "dist");
96+
fs.rmSync(distDir, { recursive: true, force: true });
97+
fs.mkdirSync(distDir, { recursive: true });
98+
log(`extracting ${path.basename(zip)}`);
99+
execFileSync("unzip", ["-q", "-o", zip, "-d", distDir], { stdio: "inherit" });
100+
fs.writeFileSync(
101+
path.join(electronDir, "path.txt"),
102+
"Electron.app/Contents/MacOS/Electron"
103+
);
104+
105+
if (!isReady()) {
106+
console.error("[ensure-electron] extraction completed but the binary is still missing");
107+
process.exit(1);
108+
}
109+
log(`repaired electron v${version} from cache`);
110+
};
111+
112+
main();

0 commit comments

Comments
 (0)