fix: explicitly rebuild better-sqlite3 for Electron ABI in afterPack
Root cause: electron-builder's @electron/rebuild only rebuilds native modules found in the `files` config. Since better-sqlite3 enters the app through extraResources (via .next/standalone/), it was skipped. The afterPack hook was copying the system Node.js ABI (127) instead of the required Electron ABI (143). Fix: - afterPack now explicitly calls `npx electron-rebuild` targeting the exact Electron version and architecture from the build context - Falls back to @electron/rebuild programmatic API if CLI fails - Fails the build if rebuild cannot complete (no silent failures) - Adds file size logging for verification - Excludes `release/` from extraResources to prevent recursive nesting - Adds startup ABI compatibility check in Electron main process with friendly error dialog instead of cryptic crash Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0bf97a3795
commit
b42d3d2ad8
3 files changed
+128
-19
No files matched your search
@@ -18,6 +18,7 @@ extraResources:
|
||||
filter:
|
||||
- "**/*"
|
||||
- "!node_modules"
|
||||
- "!release"
|
||||
- from: .next/standalone/node_modules/
|
||||
to: standalone/node_modules/
|
||||
filter: ["**/*"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { app, BrowserWindow, nativeImage, dialog } from 'electron';
|
||||
import path from 'path';
|
||||
import { spawn, execFileSync, ChildProcess } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import net from 'net';
|
||||
import os from 'os';
|
||||
|
||||
@@ -12,6 +13,62 @@ let userShellEnv: Record<string, string> = {};
|
||||
|
||||
const isDev = !app.isPackaged;
|
||||
|
||||
/**
|
||||
* Verify that better_sqlite3.node in standalone resources is compatible
|
||||
* with this Electron runtime's ABI. If it was built for a different
|
||||
* Node.js ABI (e.g. system Node v22 ABI 127 vs Electron's ABI 143),
|
||||
* show a clear error instead of a cryptic MODULE_NOT_FOUND crash.
|
||||
*/
|
||||
function checkNativeModuleABI(): void {
|
||||
if (isDev) return; // Skip in dev mode
|
||||
|
||||
const standaloneDir = path.join(process.resourcesPath, 'standalone');
|
||||
|
||||
// Find better_sqlite3.node recursively
|
||||
function findNodeFile(dir: string): string | null {
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const found = findNodeFile(fullPath);
|
||||
if (found) return found;
|
||||
} else if (entry.name === 'better_sqlite3.node') {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeFile = findNodeFile(path.join(standaloneDir, 'node_modules'));
|
||||
if (!nodeFile) {
|
||||
console.warn('[ABI check] better_sqlite3.node not found in standalone resources');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Attempt to load the native module to verify ABI compatibility
|
||||
process.dlopen({ exports: {} } as NodeModule, nodeFile);
|
||||
console.log(`[ABI check] better_sqlite3.node ABI is compatible (${nodeFile})`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes('NODE_MODULE_VERSION')) {
|
||||
console.error(`[ABI check] ABI mismatch detected: ${msg}`);
|
||||
dialog.showErrorBox(
|
||||
'CodePilot - Native Module ABI Mismatch',
|
||||
`The bundled better-sqlite3 native module was compiled for a different Node.js version.\n\n` +
|
||||
`${msg}\n\n` +
|
||||
`This usually means the build process did not correctly recompile native modules for Electron.\n` +
|
||||
`Please rebuild the application or report this issue.`
|
||||
);
|
||||
app.quit();
|
||||
} else {
|
||||
// Other load errors (missing dependencies, etc.) -- log but don't block
|
||||
console.warn(`[ABI check] Could not verify better_sqlite3.node: ${msg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the user's full shell environment by running a login shell.
|
||||
* When Electron is launched from Dock/Finder, process.env is very limited
|
||||
@@ -238,6 +295,9 @@ app.whenReady().then(async () => {
|
||||
// Load user's full shell environment (API keys, PATH, etc.)
|
||||
userShellEnv = loadUserShellEnv();
|
||||
|
||||
// Verify native module ABI compatibility before starting the server
|
||||
checkNativeModuleABI();
|
||||
|
||||
// Set macOS Dock icon
|
||||
if (process.platform === 'darwin' && app.dock) {
|
||||
const iconPath = getIconPath();
|
||||
|
||||
+67
-19
@@ -1,39 +1,84 @@
|
||||
/**
|
||||
* electron-builder afterPack hook.
|
||||
*
|
||||
* After @electron/rebuild recompiles native modules for Electron's ABI,
|
||||
* copy the rebuilt better_sqlite3.node into the standalone resources
|
||||
* (which still has the system-Node version from `next build`).
|
||||
* The standard @electron/rebuild step only rebuilds native modules found
|
||||
* in the `files` config. Since better-sqlite3 enters the app through
|
||||
* extraResources (via .next/standalone/), it gets skipped.
|
||||
*
|
||||
* This hook:
|
||||
* 1. Explicitly rebuilds better-sqlite3 for the target Electron ABI
|
||||
* 2. Copies the rebuilt .node into all locations within standalone resources
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
module.exports = async function afterPack(context) {
|
||||
const appOutDir = context.appOutDir;
|
||||
const arch = context.arch;
|
||||
// electron-builder arch enum: 1=x64, 3=arm64, etc.
|
||||
const archName = arch === 3 ? 'arm64' : arch === 1 ? 'x64' : arch === 0 ? 'ia32' : String(arch);
|
||||
const platform = context.packager.platform.name; // 'mac', 'windows', 'linux'
|
||||
|
||||
// Source: the rebuilt .node file in the project's node_modules
|
||||
// Get Electron version from packager config or from installed package
|
||||
const electronVersion =
|
||||
context.electronVersion ||
|
||||
context.packager?.config?.electronVersion ||
|
||||
require(path.join(process.cwd(), 'node_modules', 'electron', 'package.json')).version;
|
||||
|
||||
console.log(`[afterPack] Electron ${electronVersion}, arch=${archName}, platform=${platform}`);
|
||||
|
||||
// Step 1: Explicitly rebuild better-sqlite3 for the target Electron version
|
||||
const projectDir = process.cwd();
|
||||
console.log('[afterPack] Rebuilding better-sqlite3 for Electron ABI...');
|
||||
|
||||
try {
|
||||
// Use @electron/rebuild via npx (it's a dependency of electron-builder)
|
||||
const rebuildCmd = `npx electron-rebuild -f -o better-sqlite3 -v ${electronVersion} -a ${archName}`;
|
||||
console.log(`[afterPack] Running: ${rebuildCmd}`);
|
||||
execSync(rebuildCmd, {
|
||||
cwd: projectDir,
|
||||
stdio: 'inherit',
|
||||
timeout: 120000,
|
||||
});
|
||||
console.log('[afterPack] Rebuild completed successfully');
|
||||
} catch (err) {
|
||||
console.error('[afterPack] Failed to rebuild better-sqlite3:', err.message);
|
||||
// Try alternative: use @electron/rebuild programmatically
|
||||
try {
|
||||
const { rebuild } = require('@electron/rebuild');
|
||||
await rebuild({
|
||||
buildPath: projectDir,
|
||||
electronVersion: electronVersion,
|
||||
arch: archName,
|
||||
onlyModules: ['better-sqlite3'],
|
||||
force: true,
|
||||
});
|
||||
console.log('[afterPack] Rebuild via @electron/rebuild API succeeded');
|
||||
} catch (err2) {
|
||||
console.error('[afterPack] @electron/rebuild API also failed:', err2.message);
|
||||
throw new Error('Cannot rebuild better-sqlite3 for Electron ABI');
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Verify the rebuilt .node file
|
||||
const rebuiltSource = path.join(
|
||||
process.cwd(),
|
||||
'node_modules',
|
||||
'better-sqlite3',
|
||||
'build',
|
||||
'Release',
|
||||
'better_sqlite3.node'
|
||||
projectDir, 'node_modules', 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node'
|
||||
);
|
||||
|
||||
if (!fs.existsSync(rebuiltSource)) {
|
||||
console.warn('[afterPack] Rebuilt better_sqlite3.node not found at', rebuiltSource);
|
||||
return;
|
||||
throw new Error(`[afterPack] Rebuilt better_sqlite3.node not found at ${rebuiltSource}`);
|
||||
}
|
||||
|
||||
// Find all better_sqlite3.node files inside the standalone resources
|
||||
const sourceStats = fs.statSync(rebuiltSource);
|
||||
console.log(`[afterPack] Rebuilt .node file: ${rebuiltSource} (${sourceStats.size} bytes, mtime: ${sourceStats.mtime.toISOString()})`);
|
||||
|
||||
// Step 3: Find and replace all better_sqlite3.node in standalone resources
|
||||
// macOS: <appOutDir>/CodePilot.app/Contents/Resources/standalone/...
|
||||
// Windows/Linux: <appOutDir>/resources/standalone/...
|
||||
const searchRoots = [
|
||||
// macOS paths
|
||||
path.join(appOutDir, 'CodePilot.app', 'Contents', 'Resources', 'standalone'),
|
||||
path.join(appOutDir, 'Contents', 'Resources', 'standalone'),
|
||||
// Windows/Linux paths
|
||||
path.join(appOutDir, 'resources', 'standalone'),
|
||||
];
|
||||
|
||||
@@ -47,8 +92,10 @@ module.exports = async function afterPack(context) {
|
||||
if (entry.isDirectory()) {
|
||||
walkAndReplace(fullPath);
|
||||
} else if (entry.name === 'better_sqlite3.node') {
|
||||
console.log(`[afterPack] Replacing ${fullPath}`);
|
||||
const beforeSize = fs.statSync(fullPath).size;
|
||||
fs.copyFileSync(rebuiltSource, fullPath);
|
||||
const afterSize = fs.statSync(fullPath).size;
|
||||
console.log(`[afterPack] Replaced ${fullPath} (${beforeSize} -> ${afterSize} bytes)`);
|
||||
replaced++;
|
||||
}
|
||||
}
|
||||
@@ -59,13 +106,14 @@ module.exports = async function afterPack(context) {
|
||||
}
|
||||
|
||||
if (replaced > 0) {
|
||||
console.log(`[afterPack] Replaced ${replaced} better_sqlite3.node file(s) with Electron-compatible build`);
|
||||
console.log(`[afterPack] Successfully replaced ${replaced} better_sqlite3.node file(s) with Electron ABI build`);
|
||||
} else {
|
||||
console.warn('[afterPack] No better_sqlite3.node files found in standalone resources');
|
||||
// List the resources dir for debugging
|
||||
console.warn('[afterPack] WARNING: No better_sqlite3.node files found in standalone resources!');
|
||||
for (const root of searchRoots) {
|
||||
if (fs.existsSync(root)) {
|
||||
console.log(`[afterPack] Contents of ${root}:`, fs.readdirSync(root).slice(0, 20));
|
||||
} else {
|
||||
console.log(`[afterPack] Path does not exist: ${root}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user