diff --git a/electron-builder.yml b/electron-builder.yml index d5a8999..8a50def 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -18,6 +18,7 @@ extraResources: filter: - "**/*" - "!node_modules" + - "!release" - from: .next/standalone/node_modules/ to: standalone/node_modules/ filter: ["**/*"] diff --git a/electron/main.ts b/electron/main.ts index df2b0d3..a4fafc2 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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 = {}; 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(); diff --git a/scripts/after-pack.js b/scripts/after-pack.js index d5bb86c..46110f6 100644 --- a/scripts/after-pack.js +++ b/scripts/after-pack.js @@ -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: /CodePilot.app/Contents/Resources/standalone/... // Windows/Linux: /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}`); } } }