From a8be922ab8d643850ef0a98eee9fb300b46f47f2 Mon Sep 17 00:00:00 2001 From: roberto Date: Wed, 4 Mar 2026 07:47:19 +0100 Subject: [PATCH] fix: enhance cross-compilation process by replacing native binaries post-rebuild --- forge.config.ts | 140 +++++++++++++++++++++++++----------------------- 1 file changed, 74 insertions(+), 66 deletions(-) diff --git a/forge.config.ts b/forge.config.ts index a067eb0..abbd50d 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -68,73 +68,7 @@ const config: ForgeConfig = { env: { ...process.env, npm_config_nodedir: '' }, }); - // When cross-compiling, native addons (better-sqlite3) are built - // for the build machine (Linux). Use @electron/rebuild to get the - // correct binaries for the target platform. const targetKey = `${platform}-${arch}`; - const buildKey = `${process.platform}-${process.arch}`; - if (targetKey !== buildKey) { - console.log(`[forge] Cross-compiling: ${buildKey} → ${targetKey}`); - const electronVersion = JSON.parse( - fs.readFileSync(path.resolve(__dirname, 'node_modules', 'electron', 'package.json'), 'utf-8'), - ).version; - - // Rebuild native modules for target platform using prebuild-install. - // prebuild-install supports cross-platform via --platform/--arch, - // unlike @electron/rebuild which only supports --arch. - // This is FATAL — a Linux .node in a Windows package is always broken. - const nativeModules = ['better-sqlite3']; - for (const mod of nativeModules) { - const modDir = path.join(buildPath, 'node_modules', mod); - if (!fs.existsSync(modDir)) continue; - - // Clean stale build artifacts — a leftover build/Release/*.node - // for the host platform must not survive. - const buildRelease = path.join(modDir, 'build', 'Release'); - if (fs.existsSync(buildRelease)) { - fs.rmSync(buildRelease, { recursive: true, force: true }); - console.log(`[forge] Cleaned stale build/Release for ${mod}`); - } - - console.log(`[forge] Downloading ${mod} prebuilt for ${targetKey} (Electron ${electronVersion})...`); - execSync( - `npx --yes prebuild-install -r electron -t ${electronVersion} ` + - `--platform ${platform} --arch ${arch} --tag-prefix v --verbose`, - { cwd: modDir, stdio: 'inherit' }, - ); - - // prebuild-install writes the binary to build/Release/.node. - // Verify at least one .node file exists there and is NOT an ELF - // (Linux) binary — a Linux .node in a Windows package always crashes. - const releaseDir = path.join(modDir, 'build', 'Release'); - if (!fs.existsSync(releaseDir)) { - throw new Error( - `[forge] FATAL: build/Release/ not found for ${mod} after prebuild-install. ` + - `The native binary was not downloaded.`, - ); - } - const nodeFiles = fs.readdirSync(releaseDir).filter((f) => f.endsWith('.node')); - if (nodeFiles.length === 0) { - throw new Error( - `[forge] FATAL: No .node files in build/Release/ for ${mod} after prebuild-install.`, - ); - } - for (const f of nodeFiles) { - const buf = Buffer.alloc(4); - const fd = fs.openSync(path.join(releaseDir, f), 'r'); - fs.readSync(fd, buf, 0, 4, 0); - fs.closeSync(fd); - // ELF magic: 0x7f 'E' 'L' 'F' - if (buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46) { - throw new Error( - `[forge] FATAL: ${mod} build/Release/${f} is an ELF binary! ` + - `Cross-compilation failed — refusing to package a Linux .node for Windows.`, - ); - } - console.log(`[forge] Verified ${f} is not ELF ✓`); - } - } - } // vectordb uses platform-specific optional deps (@lancedb/vectordb---*). // npm install on Linux only pulls the Linux variant. Force-install the target's. @@ -224,6 +158,80 @@ const config: ForgeConfig = { } } }, + + // ── Post-rebuild: fix native binaries for cross-compilation ────── + // Forge runs @electron/rebuild AFTER packageAfterCopy, which + // recompiles native addons for the BUILD platform (Linux). + // packageAfterPrune runs AFTER rebuild+prune, so we can safely + // replace the Linux .node files with the correct target prebuilts. + packageAfterPrune: async (_forgeConfig, buildPath, _electronVersion, platform, arch) => { + const targetKey = `${platform}-${arch}`; + const buildKey = `${process.platform}-${process.arch}`; + if (targetKey === buildKey) return; // native build — nothing to fix + + console.log(`[forge:afterPrune] Cross-compile fixup: ${buildKey} → ${targetKey}`); + const electronVersion = JSON.parse( + fs.readFileSync(path.resolve(__dirname, 'node_modules', 'electron', 'package.json'), 'utf-8'), + ).version; + + // Replace native addons that @electron/rebuild compiled for the host. + const nativeModules = ['better-sqlite3']; + for (const mod of nativeModules) { + const modDir = path.join(buildPath, 'node_modules', mod); + if (!fs.existsSync(modDir)) continue; + + // Remove the host-platform binary left by @electron/rebuild + const buildRelease = path.join(modDir, 'build', 'Release'); + if (fs.existsSync(buildRelease)) { + fs.rmSync(buildRelease, { recursive: true, force: true }); + console.log(`[forge:afterPrune] Cleaned host-platform build/Release for ${mod}`); + } + + // Download the correct prebuilt for the TARGET platform + console.log(`[forge:afterPrune] Downloading ${mod} prebuilt for ${targetKey} (Electron ${electronVersion})...`); + execSync( + `npx --yes prebuild-install -r electron -t ${electronVersion} ` + + `--platform ${platform} --arch ${arch} --tag-prefix v --verbose`, + { cwd: modDir, stdio: 'inherit' }, + ); + + // Verify the binary exists and is for the correct platform. + const releaseDir = path.join(modDir, 'build', 'Release'); + if (!fs.existsSync(releaseDir)) { + throw new Error( + `[forge] FATAL: build/Release/ not found for ${mod} after prebuild-install. ` + + `The native binary was not downloaded.`, + ); + } + const nodeFiles = fs.readdirSync(releaseDir).filter((f) => f.endsWith('.node')); + if (nodeFiles.length === 0) { + throw new Error( + `[forge] FATAL: No .node files in build/Release/ for ${mod} after prebuild-install.`, + ); + } + for (const f of nodeFiles) { + const buf = Buffer.alloc(4); + const fd = fs.openSync(path.join(releaseDir, f), 'r'); + fs.readSync(fd, buf, 0, 4, 0); + fs.closeSync(fd); + // ELF magic: 0x7f 'E' 'L' 'F' + if (buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46) { + throw new Error( + `[forge] FATAL: ${mod} build/Release/${f} is an ELF binary! ` + + `Cross-compilation failed — refusing to package a Linux .node for Windows.`, + ); + } + // PE magic: 'M' 'Z' (0x4d 0x5a) — expected for win32 + if (platform === 'win32' && !(buf[0] === 0x4d && buf[1] === 0x5a)) { + throw new Error( + `[forge] FATAL: ${mod} build/Release/${f} is not a PE (Windows) binary! ` + + `Magic bytes: ${buf.toString('hex')}. Refusing to package.`, + ); + } + console.log(`[forge:afterPrune] Verified ${mod}/${f} — correct platform ✓`); + } + } + }, }, makers: [ new MakerSquirrel({}),