[eric] electron: fix the electron 42 windows build - electronLanguages en to en-US so the renderer gets a non-empty --lang (electron 42 renamed en.pak to en-US.pak; empty locale crashed blink LCIDFromLocaleInternal with 0xC0000005), bump electron-builder 25.1.8 to 26.8.1 + migrate win.sign to signtoolOptions, after-pack hook restores the 9router node_modules eb26 drops from extraResources so the subscription service stops hanging, and verify-locale-paks + verify-router-deps gate both in verify-all

This commit is contained in:
Eric
2026-05-28 21:41:01 -07:00
parent 0d90a469aa
commit f1a3ec2598
7 changed files with 1207 additions and 1893 deletions
+37
View File
@@ -0,0 +1,37 @@
'use strict';
// electron-builder 26 special-excludes node_modules from extraResources (25 did
// not), so the bundled 9Router - a Next.js standalone whose server.js does
// require('next') - ships WITHOUT its deps. The result: 9Router dies with
// "Cannot find module 'next'", never binds :20128, and the Models tab spins on
// "Starting subscription service..." forever. We copy router/node_modules into
// the packed app HERE rather than after electron-builder finishes, because
// afterPack runs BEFORE code-signing: on macOS the whole .app is sealed by the
// signature, so injecting files post-sign would invalidate it. The .next dotdir
// is handled by the package.json extraResources filter; only node_modules needs
// this rescue.
const fs = require('fs');
const path = require('path');
exports.default = async function afterPack(context) {
const { appOutDir, electronPlatformName, packager } = context;
const src = path.join(__dirname, '..', 'build-staging', 'router', 'node_modules');
if (!fs.existsSync(src)) return; // dev/no-router build; nothing to do
let routerDir;
if (electronPlatformName === 'darwin') {
const appName = packager.appInfo.productFilename; // "OpenSwarm"
routerDir = path.join(appOutDir, `${appName}.app`, 'Contents', 'Resources', 'router');
} else {
routerDir = path.join(appOutDir, 'resources', 'router');
}
if (!fs.existsSync(routerDir)) return; // router not staged into this target
const dest = path.join(routerDir, 'node_modules');
if (!fs.existsSync(dest)) {
fs.cpSync(src, dest, { recursive: true });
}
if (!fs.existsSync(path.join(dest, 'next'))) {
throw new Error(`afterPack: 9Router node_modules/next missing in ${routerDir} after copy`);
}
console.log(`[afterPack] staged 9Router node_modules into ${routerDir}`);
};
+1009 -1885
View File
File diff suppressed because it is too large Load Diff
+18 -8
View File
@@ -23,13 +23,16 @@
"devDependencies": {
"@electron/notarize": "3.1.1",
"cross-env": "7.0.3",
"electron": "castlabs/electron-releases#v42.0.0+wvcus",
"electron-builder": "25.1.8"
"electron": "github:castlabs/electron-releases#v42.0.0+wvcus",
"electron-builder": "^26.8.1"
},
"build": {
"appId": "com.clusterlabs.openswarm",
"productName": "OpenSwarm",
"electronLanguages": ["en"],
"afterPack": "./build/after-pack.js",
"electronLanguages": [
"en-US"
],
"electronDownload": {
"mirror": "https://github.com/castlabs/electron-releases/releases/download/v"
},
@@ -69,13 +72,18 @@
"target": [
{
"target": "nsis",
"arch": ["x64"]
"arch": [
"x64"
]
}
],
"artifactName": "OpenSwarm-Setup-${arch}.${ext}",
"sign": "./build/sign-windows.js",
"signingHashAlgorithms": ["sha256"],
"signDlls": false
"signtoolOptions": {
"sign": "./build/sign-windows.js",
"signingHashAlgorithms": [
"sha256"
]
}
},
"nsis": {
"oneClick": true,
@@ -123,7 +131,9 @@
"from": "build-staging/router",
"to": "router",
"filter": [
"**/*"
"**/*",
"**/.*",
"**/.*/**"
]
},
{
+5
View File
@@ -465,6 +465,11 @@ try {
if ($LASTEXITCODE -ne 0) { throw "electron-builder failed" }
} finally { Pop-Location }
# NOTE: the bundled 9Router's node_modules (which electron-builder 26 drops from
# extraResources) is restored by the build/after-pack.js afterPack hook, which
# runs inside electron-builder BEFORE code-signing so the copied files are sealed
# by the signature. See that file for the why.
Remove-Item -Recurse -Force $Staging -ErrorAction SilentlyContinue
Write-Host ""
+2
View File
@@ -35,6 +35,8 @@ function main() {
['packaging parity (Win+Mac stage the same dirs)', 'verify-packaging-parity.js', []],
['deps fully pinned (reproducible backend builds)', 'verify-deps-pinned.js', []],
['no build-host paths leaked into the artifact', 'verify-host-leakage.js', appArg],
['locale paks shipped (empty --lang -> Blink null-deref crash)', 'verify-locale-paks.js', appArg],
['9router deps shipped (else subscription service hangs)', 'verify-router-deps.js', appArg],
['bundled python runs (--version + import smoke)', 'verify-python-health.js', appArg],
['MCP bundles answer initialize over stdio', 'verify-mcp-bundles.js', appArg],
['update feed sha512 matches files on disk', 'verify-update-feed.js', []],
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env node
// Guards the empty-locale renderer crash. A packaged build MUST ship Chromium's
// locale .pak files (locales/en-US.pak + the full ~50). If they are missing,
// Electron launches the renderer with an EMPTY --lang, and Blink's
// LCIDFromLocaleInternal (third_party/blink/.../text/locale_win.cc) null-derefs
// (STATUS_ACCESS_VIOLATION 0xC0000005, read of 0x8) the instant a text/agent/
// webview surface mounts - a hard crash on the first real interaction, with no
// JS error to localize it. electron-builder has been observed to drop these on a
// --dir repack, so we assert them explicitly rather than trust the packager.
'use strict';
const fs = require('fs');
const path = require('path');
const h = require('./lib/app-harness');
// The build intentionally trims locales to en-US via electronLanguages, so the
// count is small by design - the only thing that matters is that the locale the
// renderer resolves to (en-US.pak) is actually present. Zero paks / a missing
// en-US.pak is the crash condition.
const REQUIRED = 'en-US.pak';
function parseArgs(argv) {
const out = { app: null };
for (let i = 0; i < argv.length; i++) if (argv[i] === '--app') out.app = argv[++i];
return out;
}
function pakCount(dir) {
try { return fs.readdirSync(dir).filter((f) => f.toLowerCase().endsWith('.pak')); }
catch { return null; }
}
function checkWinLinux(exe) {
const dir = path.join(path.dirname(exe), 'locales');
const paks = pakCount(dir);
if (paks === null) return { ok: false, msg: `locales/ dir missing at ${dir}` };
const hasReq = paks.some((p) => p.toLowerCase() === REQUIRED.toLowerCase());
process.stdout.write(` ${dir}: ${paks.length} paks, en-US.pak=${hasReq}\n`);
if (!hasReq) return { ok: false, msg: `${REQUIRED} missing from ${dir} (${paks.length} paks present)` };
return { ok: true, msg: `${paks.length} paks incl ${REQUIRED}` };
}
function checkMac(exe) {
// mac stores locale paks inside the Electron Framework; layout varies by version,
// and this crash is Windows-specific, so be informational rather than blocking.
const appRoot = exe.slice(0, exe.indexOf('.app') + 4);
const found = [];
(function walk(d, depth) {
if (depth > 6) return;
let ents = [];
try { ents = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
for (const e of ents) {
const full = path.join(d, e.name);
if (e.isDirectory()) walk(full, depth + 1);
else if (e.isFile() && e.name.toLowerCase().endsWith('.pak')) found.push(full);
}
})(appRoot, 0);
process.stdout.write(` mac: found ${found.length} .pak file(s) under the app bundle\n`);
return { ok: found.length > 0, msg: `${found.length} paks (mac is informational)` , soft: true };
}
function main() {
const args = parseArgs(process.argv.slice(2));
const exe = h.packagedAppPath(args.app);
const res = process.platform === 'darwin' ? checkMac(exe) : checkWinLinux(exe);
if (res.ok) { process.stdout.write(`PASS locale paks present (${res.msg})\n`); process.exit(0); }
if (res.soft) { process.stdout.write(`WARN ${res.msg} - could not confirm mac paks; not blocking\n`); process.exit(0); }
process.stderr.write(
`FAIL packaged build is MISSING Chromium locale paks: ${res.msg}\n` +
` The renderer would launch with an empty --lang and crash in Blink's\n` +
` LCIDFromLocaleInternal (0xC0000005) on the first text/agent/webview mount.\n` +
` Fix: ensure electron-builder copies node_modules/electron/dist/locales/*.pak\n` +
` into the packaged output (this regressed on --dir repacks of the v42 build).\n`);
process.exit(1);
}
main();
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env node
// Guards the "subscription service hangs forever" failure. The bundled 9Router is
// a Next.js standalone server whose server.js does require('next'), so it needs
// router/node_modules (and router/.next) present in the packaged app. electron-
// builder 26 special-excludes node_modules from extraResources (25 did not), and
// drops the .next dotdir unless the filter opts dotfiles in - either way the
// router dies with "Cannot find module 'next'", 9Router never binds :20128, and
// the Models tab spins on "Starting subscription service..." indefinitely. Assert
// the deps are actually in the package.
'use strict';
const fs = require('fs');
const path = require('path');
const h = require('./lib/app-harness');
function parseArgs(argv) {
const out = { app: null };
for (let i = 0; i < argv.length; i++) if (argv[i] === '--app') out.app = argv[++i];
return out;
}
function routerDir(exe) {
if (process.platform === 'darwin') {
const appRoot = exe.slice(0, exe.indexOf('.app') + 4);
return path.join(appRoot, 'Contents', 'Resources', 'router');
}
return path.join(path.dirname(exe), 'resources', 'router');
}
function main() {
const args = parseArgs(process.argv.slice(2));
const exe = h.packagedAppPath(args.app);
const dir = routerDir(exe);
const checks = {
'server.js': path.join(dir, 'server.js'),
'node_modules/next': path.join(dir, 'node_modules', 'next'),
'.next': path.join(dir, '.next'),
};
const missing = [];
for (const [label, p] of Object.entries(checks)) {
const ok = fs.existsSync(p);
process.stdout.write(` ${ok ? 'ok ' : 'MISSING'} ${label}\n`);
if (!ok) missing.push(label);
}
if (missing.length === 0) {
process.stdout.write(`PASS 9Router deps present in ${dir}\n`);
process.exit(0);
}
process.stderr.write(
`FAIL bundled 9Router is missing ${missing.join(', ')} in ${dir}\n` +
` server.js does require('next'); without node_modules/.next the 9Router\n` +
` never starts, port 20128 stays dead, and the subscription service hangs.\n` +
` Fix: ensure the build copies router/node_modules + .next into the package\n` +
` (electron-builder 26 drops node_modules from extraResources; build-app-win.ps1\n` +
` copies it back post-build, and the package.json filter opts the .next dotdir in).\n`);
process.exit(1);
}
main();