From c859665c6b35dbf2f475e02b7ac0b1ee7a896709 Mon Sep 17 00:00:00 2001 From: ardaaltinors Date: Thu, 12 Mar 2026 10:00:32 +0300 Subject: [PATCH 1/4] fix(cli): copy native binary from root node_modules instead of rebuilding (#321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standalone app/ directory created by Next.js only contains runtime files for better-sqlite3 (no binding.gyp, no source, no prebuild-install), so `npm rebuild` inside app/ is a no-op. The previous fix (#312) added exit(1) on rebuild failure, which caused npm to rollback the entire package installation — leaving users with nothing to fix manually. New approach: 1. Check if existing binary is already compatible (dlopen) 2. Copy the correctly-built binary from root node_modules/ (npm already compiles it for the correct platform during install) 3. Fall back to npm rebuild if root binary is unavailable 4. Warn but don't fail the install if nothing works — the package stays installed and the CLI pre-flight check gives a clear error at startup --- scripts/postinstall.mjs | 114 +++++++++++++++++++++++++--------------- 1 file changed, 72 insertions(+), 42 deletions(-) diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs index 449947b2bc..d8379f6b43 100644 --- a/scripts/postinstall.mjs +++ b/scripts/postinstall.mjs @@ -1,18 +1,22 @@ #!/usr/bin/env node /** - * OmniRoute — Postinstall Native Module Rebuild + * OmniRoute — Postinstall Native Module Fix * * The npm package ships with a Next.js standalone build that includes - * better-sqlite3 compiled for the build platform (Linux x64). - * This script detects platform mismatches and rebuilds the native - * module for the user's actual OS/architecture. + * better-sqlite3 compiled for the build platform (Linux x64) inside + * app/node_modules/. However, npm also installs better-sqlite3 as a + * top-level dependency (in the root node_modules/), correctly compiled + * for the user's platform. + * + * This script copies the correctly-built native binary from the root + * into the standalone app directory — no rebuild or build tools needed. * * Fixes: https://github.com/diegosouzapw/OmniRoute/issues/129 + * Fixes: https://github.com/diegosouzapw/OmniRoute/issues/321 */ -import { execSync } from "node:child_process"; -import { existsSync } from "node:fs"; +import { existsSync, copyFileSync, mkdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; @@ -20,62 +24,88 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ROOT = join(__dirname, ".."); -// The standalone build bundles better-sqlite3 inside app/node_modules -const appNodeModules = join(ROOT, "app", "node_modules", "better-sqlite3"); +const appBinary = join( + ROOT, + "app", + "node_modules", + "better-sqlite3", + "build", + "Release", + "better_sqlite3.node" +); +const rootBinary = join( + ROOT, + "node_modules", + "better-sqlite3", + "build", + "Release", + "better_sqlite3.node" +); -if (!existsSync(appNodeModules)) { - // No bundled better-sqlite3 — nothing to do (dev install, not npm global) +if (!existsSync(join(ROOT, "app", "node_modules", "better-sqlite3"))) { + // No standalone app directory — nothing to do (dev install, not npm global) process.exit(0); } -const buildInfoPath = join(appNodeModules, "build", "Release", "better_sqlite3.node"); - -// The published binary is compiled for linux-x64. -// On any other platform/arch, we must rebuild — dlopen alone is unreliable -// because macOS may load an incompatible binary without throwing. +// The published binary is compiled for linux-x64. On any other platform/arch, +// always replace it — dlopen alone is unreliable because macOS can load an +// incompatible binary without throwing (the exact bug fixed in #312). const BUILD_PLATFORM = "linux"; const BUILD_ARCH = "x64"; -const needsRebuild = process.platform !== BUILD_PLATFORM || process.arch !== BUILD_ARCH; +const platformMatch = process.platform === BUILD_PLATFORM && process.arch === BUILD_ARCH; -if (!needsRebuild) { +if (platformMatch) { try { - process.dlopen({ exports: {} }, buildInfoPath); + process.dlopen({ exports: {} }, appBinary); process.exit(0); } catch { - // Same platform but binary still incompatible (e.g. Node.js ABI mismatch) — rebuild + // Same platform but binary still incompatible (e.g. Node.js ABI mismatch) } } -console.log(`\n 🔧 Rebuilding better-sqlite3 for ${process.platform}-${process.arch}...`); +console.log(`\n 🔧 Fixing better-sqlite3 binary for ${process.platform}-${process.arch}...`); + +// Strategy 1: Copy the correctly-built binary from root node_modules +if (existsSync(rootBinary)) { + try { + mkdirSync(dirname(appBinary), { recursive: true }); + copyFileSync(rootBinary, appBinary); + + // Verify the copied binary loads + process.dlopen({ exports: {} }, appBinary); + console.log(" ✅ Native module fixed successfully!\n"); + process.exit(0); + } catch { + // Copy succeeded but binary still doesn't load — fall through + } +} + +// Strategy 2: Fall back to npm rebuild (may work if build tools are available) +console.log(" ⚠️ Root binary not available, attempting npm rebuild..."); try { + const { execSync } = await import("node:child_process"); execSync("npm rebuild better-sqlite3", { cwd: join(ROOT, "app"), stdio: "inherit", timeout: 120_000, }); -} catch (error) { - console.error(" ❌ Failed to rebuild better-sqlite3 automatically."); - console.error(" You can fix this manually by running:"); - console.error(` cd ${join(ROOT, "app")} && npm rebuild better-sqlite3`); - if (process.platform === "darwin") { - console.error(" If build tools are missing: xcode-select --install"); - } - console.error(""); - process.exit(1); + + // Verify rebuild worked + process.dlopen({ exports: {} }, appBinary); + console.log(" ✅ Native module rebuilt successfully!\n"); + process.exit(0); +} catch { + // Rebuild failed or binary still incompatible } -// Verify the rebuilt binary actually loads -try { - process.dlopen({ exports: {} }, buildInfoPath); - console.log(" ✅ Native module rebuilt successfully!\n"); -} catch { - console.error(" ❌ Rebuild completed but binary is still incompatible."); - console.error(" Try manually:"); - console.error(` cd ${join(ROOT, "app")} && npm rebuild better-sqlite3`); - if (process.platform === "darwin") { - console.error(" If build tools are missing: xcode-select --install"); - } - console.error(""); - process.exit(1); +// If nothing worked, warn but don't fail the install — let the package stay +// installed so users can fix manually or use the pre-flight check in the CLI +console.warn(" ⚠️ Could not fix better-sqlite3 native module automatically."); +console.warn(" The server may not start correctly."); +console.warn(" Try manually:"); +console.warn(` cd ${join(ROOT, "app")} && npm rebuild better-sqlite3`); +if (process.platform === "darwin") { + console.warn(" If build tools are missing: xcode-select --install"); } +console.warn(""); From 69d28bec4d39dd18b5994540f958c7fde82835dc Mon Sep 17 00:00:00 2001 From: ardaaltinors Date: Thu, 12 Mar 2026 10:20:08 +0300 Subject: [PATCH 2/4] feat(cli): detect native binary platform from file header instead of dlopen Add native-binary-compat module that reads ELF/Mach-O/PE headers to determine the actual target platform/arch of the .node binary. This eliminates the macOS false-positive where dlopen loads a linux-x64 binary without throwing. - Parse ELF (linux), Mach-O (darwin), and PE (win32) binary formats - Use header-based check as primary signal, dlopen as secondary - Update pre-flight check in CLI to use the new module - Add unit tests for all binary formats and cross-platform scenarios --- bin/omniroute.mjs | 33 ++--- scripts/native-binary-compat.mjs | 158 +++++++++++++++++++++++ tests/unit/native-binary-compat.test.mjs | 143 ++++++++++++++++++++ 3 files changed, 313 insertions(+), 21 deletions(-) create mode 100644 scripts/native-binary-compat.mjs create mode 100644 tests/unit/native-binary-compat.test.mjs diff --git a/bin/omniroute.mjs b/bin/omniroute.mjs index be0b624134..c2c11c0194 100755 --- a/bin/omniroute.mjs +++ b/bin/omniroute.mjs @@ -17,6 +17,7 @@ import { existsSync, readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { homedir, platform } from "node:os"; +import { isNativeBinaryCompatible } from "../scripts/native-binary-compat.mjs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -194,9 +195,9 @@ if (!existsSync(serverJs)) { } // ── Pre-flight: verify better-sqlite3 native binary ─────── -// The published binary targets linux-x64. Check both platform match AND -// dlopen — on macOS, dlopen alone may succeed on an incompatible binary -// (false positive), so we check platform first as the primary signal. +// Verify the binary's actual target platform/arch before trusting dlopen. +// This avoids the macOS false positive where a bundled linux-x64 addon can +// appear to load even though the runtime will fail when better-sqlite3 starts. const sqliteBinary = join( APP_DIR, "node_modules", @@ -205,25 +206,15 @@ const sqliteBinary = join( "Release", "better_sqlite3.node" ); -if (existsSync(sqliteBinary)) { - let compatible = false; - try { - process.dlopen({ exports: {} }, sqliteBinary); - compatible = true; - } catch { - // dlopen failed — definitely incompatible - } - - if (!compatible) { - console.error( - "\x1b[31m✖ better-sqlite3 native module is incompatible with this platform.\x1b[0m" - ); - console.error(` Run: cd ${APP_DIR} && npm rebuild better-sqlite3`); - if (platform() === "darwin") { - console.error(" If build tools are missing: xcode-select --install"); - } - process.exit(1); +if (existsSync(sqliteBinary) && !isNativeBinaryCompatible(sqliteBinary)) { + console.error( + "\x1b[31m✖ better-sqlite3 native module is incompatible with this platform.\x1b[0m" + ); + console.error(` Run: cd ${APP_DIR} && npm rebuild better-sqlite3`); + if (platform() === "darwin") { + console.error(" If build tools are missing: xcode-select --install"); } + process.exit(1); } // ── Start server ─────────────────────────────────────────── diff --git a/scripts/native-binary-compat.mjs b/scripts/native-binary-compat.mjs new file mode 100644 index 0000000000..74fa21adbc --- /dev/null +++ b/scripts/native-binary-compat.mjs @@ -0,0 +1,158 @@ +import { existsSync, readFileSync } from "node:fs"; + +export const PUBLISHED_BUILD_PLATFORM = "linux"; +export const PUBLISHED_BUILD_ARCH = "x64"; + +function mapElfMachine(machine) { + switch (machine) { + case 62: + return "x64"; + case 183: + return "arm64"; + default: + return null; + } +} + +function mapMachCpuType(cpuType) { + switch (cpuType) { + case 0x01000007: + return "x64"; + case 0x0100000c: + return "arm64"; + default: + return null; + } +} + +function mapPeMachine(machine) { + switch (machine) { + case 0x8664: + return "x64"; + case 0xaa64: + return "arm64"; + default: + return null; + } +} + +function readUInt16(buffer, offset, littleEndian) { + return littleEndian ? buffer.readUInt16LE(offset) : buffer.readUInt16BE(offset); +} + +function readUInt32(buffer, offset, littleEndian) { + return littleEndian ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset); +} + +function detectElfTarget(buffer) { + if (buffer.length < 20) return null; + if (buffer[0] !== 0x7f || buffer[1] !== 0x45 || buffer[2] !== 0x4c || buffer[3] !== 0x46) { + return null; + } + + const littleEndian = buffer[5] !== 2; + const arch = mapElfMachine(readUInt16(buffer, 18, littleEndian)); + if (!arch) return null; + + return { platform: "linux", architectures: [arch] }; +} + +function detectMachTarget(buffer) { + if (buffer.length < 8) return null; + + const magic = buffer.readUInt32BE(0); + const thinMagic = new Map([ + [0xfeedface, false], + [0xfeedfacf, false], + [0xcefaedfe, true], + [0xcffaedfe, true], + ]); + const fatMagic = new Map([ + [0xcafebabe, false], + [0xcafebabf, false], + [0xbebafeca, true], + [0xbfbafeca, true], + ]); + + if (thinMagic.has(magic)) { + const littleEndian = thinMagic.get(magic); + const arch = mapMachCpuType(readUInt32(buffer, 4, littleEndian)); + if (!arch) return null; + return { platform: "darwin", architectures: [arch] }; + } + + if (!fatMagic.has(magic)) return null; + + const littleEndian = fatMagic.get(magic); + const isFat64 = magic === 0xcafebabf || magic === 0xbfbafeca; + const archCount = readUInt32(buffer, 4, littleEndian); + const entrySize = isFat64 ? 32 : 20; + const architectures = new Set(); + + for (let index = 0; index < archCount; index += 1) { + const offset = 8 + index * entrySize; + if (offset + 4 > buffer.length) break; + const arch = mapMachCpuType(readUInt32(buffer, offset, littleEndian)); + if (arch) architectures.add(arch); + } + + if (architectures.size === 0) return null; + return { platform: "darwin", architectures: [...architectures] }; +} + +function detectPeTarget(buffer) { + if (buffer.length < 0x40) return null; + if (buffer[0] !== 0x4d || buffer[1] !== 0x5a) return null; + + const peHeaderOffset = buffer.readUInt32LE(0x3c); + if (peHeaderOffset + 6 > buffer.length) return null; + if ( + buffer[peHeaderOffset] !== 0x50 || + buffer[peHeaderOffset + 1] !== 0x45 || + buffer[peHeaderOffset + 2] !== 0x00 || + buffer[peHeaderOffset + 3] !== 0x00 + ) { + return null; + } + + const arch = mapPeMachine(buffer.readUInt16LE(peHeaderOffset + 4)); + if (!arch) return null; + return { platform: "win32", architectures: [arch] }; +} + +export function detectNativeBinaryTarget(buffer) { + return detectElfTarget(buffer) ?? detectMachTarget(buffer) ?? detectPeTarget(buffer) ?? null; +} + +export function readNativeBinaryTarget(binaryPath) { + if (!existsSync(binaryPath)) return null; + + try { + return detectNativeBinaryTarget(readFileSync(binaryPath)); + } catch { + return null; + } +} + +export function isNativeBinaryCompatible( + binaryPath, + { runtimePlatform = process.platform, runtimeArch = process.arch, dlopen = process.dlopen } = {} +) { + const target = readNativeBinaryTarget(binaryPath); + + if (target) { + if (target.platform !== runtimePlatform || !target.architectures.includes(runtimeArch)) { + return false; + } + } else if (runtimePlatform !== PUBLISHED_BUILD_PLATFORM || runtimeArch !== PUBLISHED_BUILD_ARCH) { + // Unknown binary layout on a non-build platform is too risky to treat as compatible. + return false; + } + + try { + dlopen({ exports: {} }, binaryPath); + return true; + } catch { + return false; + } +} diff --git a/tests/unit/native-binary-compat.test.mjs b/tests/unit/native-binary-compat.test.mjs new file mode 100644 index 0000000000..73d0a86bd7 --- /dev/null +++ b/tests/unit/native-binary-compat.test.mjs @@ -0,0 +1,143 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +import { + detectNativeBinaryTarget, + isNativeBinaryCompatible, +} from "../../scripts/native-binary-compat.mjs"; + +function makeElfBinary(machine) { + const buffer = Buffer.alloc(64); + buffer[0] = 0x7f; + buffer[1] = 0x45; + buffer[2] = 0x4c; + buffer[3] = 0x46; + buffer[4] = 2; + buffer[5] = 1; + buffer.writeUInt16LE(machine, 18); + return buffer; +} + +function makeMachBinary(cpuType) { + const buffer = Buffer.alloc(32); + buffer.writeUInt32BE(0xcffaedfe, 0); + buffer.writeUInt32LE(cpuType, 4); + return buffer; +} + +function makePeBinary(machine) { + const buffer = Buffer.alloc(160); + buffer[0] = 0x4d; + buffer[1] = 0x5a; + buffer.writeUInt32LE(0x80, 0x3c); + buffer.write("PE\0\0", 0x80, "ascii"); + buffer.writeUInt16LE(machine, 0x84); + return buffer; +} + +describe("detectNativeBinaryTarget", () => { + it("detects linux x64 ELF binaries", () => { + assert.deepEqual(detectNativeBinaryTarget(makeElfBinary(62)), { + platform: "linux", + architectures: ["x64"], + }); + }); + + it("detects darwin arm64 Mach-O binaries", () => { + assert.deepEqual(detectNativeBinaryTarget(makeMachBinary(0x0100000c)), { + platform: "darwin", + architectures: ["arm64"], + }); + }); + + it("detects win32 x64 PE binaries", () => { + assert.deepEqual(detectNativeBinaryTarget(makePeBinary(0x8664)), { + platform: "win32", + architectures: ["x64"], + }); + }); +}); + +describe("isNativeBinaryCompatible", () => { + function withTempBinary(buffer, callback) { + const dir = mkdtempSync(join(tmpdir(), "omniroute-native-")); + const file = join(dir, "better_sqlite3.node"); + writeFileSync(file, buffer); + + try { + callback(file); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + } + + it("accepts linux-x64 binaries when the target matches and dlopen succeeds", () => { + withTempBinary(makeElfBinary(62), (binaryPath) => { + assert.equal( + isNativeBinaryCompatible(binaryPath, { + runtimePlatform: "linux", + runtimeArch: "x64", + dlopen() {}, + }), + true + ); + }); + }); + + it("rejects linux-x64 binaries when dlopen fails on the same platform", () => { + withTempBinary(makeElfBinary(62), (binaryPath) => { + assert.equal( + isNativeBinaryCompatible(binaryPath, { + runtimePlatform: "linux", + runtimeArch: "x64", + dlopen() { + throw new Error("abi mismatch"); + }, + }), + false + ); + }); + }); + + it("rejects macOS false positives for bundled linux binaries", () => { + withTempBinary(makeElfBinary(62), (binaryPath) => { + assert.equal( + isNativeBinaryCompatible(binaryPath, { + runtimePlatform: "darwin", + runtimeArch: "arm64", + dlopen() {}, + }), + false + ); + }); + }); + + it("rejects Windows false positives for bundled linux binaries", () => { + withTempBinary(makeElfBinary(62), (binaryPath) => { + assert.equal( + isNativeBinaryCompatible(binaryPath, { + runtimePlatform: "win32", + runtimeArch: "x64", + dlopen() {}, + }), + false + ); + }); + }); + + it("accepts copied darwin binaries after postinstall replacement", () => { + withTempBinary(makeMachBinary(0x0100000c), (binaryPath) => { + assert.equal( + isNativeBinaryCompatible(binaryPath, { + runtimePlatform: "darwin", + runtimeArch: "arm64", + dlopen() {}, + }), + true + ); + }); + }); +}); From 5a244aa12aa0f5ecd763eff62aad4c90e59a78d5 Mon Sep 17 00:00:00 2001 From: ardaaltinors Date: Thu, 12 Mar 2026 10:26:16 +0300 Subject: [PATCH 3/4] fix(cli): include native-binary-compat.mjs in published package files The module is imported by bin/omniroute.mjs but was missing from the files array in package.json, causing ERR_MODULE_NOT_FOUND on global installs. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 994e827649..c07f7503f4 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "open-sse/mcp-server/", "src/shared/contracts/", "scripts/postinstall.mjs", + "scripts/native-binary-compat.mjs", "README.md", "LICENSE" ], From a22f0a4e7bc5c1156b25792099033516d8a82005 Mon Sep 17 00:00:00 2001 From: ardaaltinors Date: Thu, 12 Mar 2026 10:34:56 +0300 Subject: [PATCH 4/4] fix(cli): address review feedback on native binary detection and postinstall - Read only first 4096 bytes of binary header instead of entire file - Add error logging to all catch blocks with specific failure messages - Separate copy vs dlopen catch blocks in postinstall Strategy 1 - Add archCount sanity cap (max 30) for fat Mach-O parsing - Distinguish timeout vs rebuild failure in Strategy 2 --- scripts/native-binary-compat.mjs | 73 +++++++++++++++++--------------- scripts/postinstall.mjs | 36 +++++++++------- 2 files changed, 59 insertions(+), 50 deletions(-) diff --git a/scripts/native-binary-compat.mjs b/scripts/native-binary-compat.mjs index 74fa21adbc..1b5ab353e7 100644 --- a/scripts/native-binary-compat.mjs +++ b/scripts/native-binary-compat.mjs @@ -1,8 +1,11 @@ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, openSync, readSync, closeSync } from "node:fs"; export const PUBLISHED_BUILD_PLATFORM = "linux"; export const PUBLISHED_BUILD_ARCH = "x64"; +const HEADER_SIZE = 4096; +const MAX_FAT_ARCH_COUNT = 30; + function mapElfMachine(machine) { switch (machine) { case 62: @@ -44,11 +47,11 @@ function readUInt32(buffer, offset, littleEndian) { return littleEndian ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset); } +const ELF_MAGIC = 0x7f454c46; + function detectElfTarget(buffer) { if (buffer.length < 20) return null; - if (buffer[0] !== 0x7f || buffer[1] !== 0x45 || buffer[2] !== 0x4c || buffer[3] !== 0x46) { - return null; - } + if (buffer.readUInt32BE(0) !== ELF_MAGIC) return null; const littleEndian = buffer[5] !== 2; const arch = mapElfMachine(readUInt16(buffer, 18, littleEndian)); @@ -57,35 +60,37 @@ function detectElfTarget(buffer) { return { platform: "linux", architectures: [arch] }; } +const THIN_MACH_MAGIC = new Map([ + [0xfeedface, false], + [0xfeedfacf, false], + [0xcefaedfe, true], + [0xcffaedfe, true], +]); +const FAT_MACH_MAGIC = new Map([ + [0xcafebabe, false], + [0xcafebabf, false], + [0xbebafeca, true], + [0xbfbafeca, true], +]); + function detectMachTarget(buffer) { if (buffer.length < 8) return null; const magic = buffer.readUInt32BE(0); - const thinMagic = new Map([ - [0xfeedface, false], - [0xfeedfacf, false], - [0xcefaedfe, true], - [0xcffaedfe, true], - ]); - const fatMagic = new Map([ - [0xcafebabe, false], - [0xcafebabf, false], - [0xbebafeca, true], - [0xbfbafeca, true], - ]); - if (thinMagic.has(magic)) { - const littleEndian = thinMagic.get(magic); + if (THIN_MACH_MAGIC.has(magic)) { + const littleEndian = THIN_MACH_MAGIC.get(magic); const arch = mapMachCpuType(readUInt32(buffer, 4, littleEndian)); if (!arch) return null; return { platform: "darwin", architectures: [arch] }; } - if (!fatMagic.has(magic)) return null; + if (!FAT_MACH_MAGIC.has(magic)) return null; - const littleEndian = fatMagic.get(magic); + const littleEndian = FAT_MACH_MAGIC.get(magic); const isFat64 = magic === 0xcafebabf || magic === 0xbfbafeca; const archCount = readUInt32(buffer, 4, littleEndian); + if (archCount > MAX_FAT_ARCH_COUNT) return null; const entrySize = isFat64 ? 32 : 20; const architectures = new Set(); @@ -102,18 +107,11 @@ function detectMachTarget(buffer) { function detectPeTarget(buffer) { if (buffer.length < 0x40) return null; - if (buffer[0] !== 0x4d || buffer[1] !== 0x5a) return null; + if (buffer.readUInt16LE(0) !== 0x5a4d) return null; const peHeaderOffset = buffer.readUInt32LE(0x3c); if (peHeaderOffset + 6 > buffer.length) return null; - if ( - buffer[peHeaderOffset] !== 0x50 || - buffer[peHeaderOffset + 1] !== 0x45 || - buffer[peHeaderOffset + 2] !== 0x00 || - buffer[peHeaderOffset + 3] !== 0x00 - ) { - return null; - } + if (buffer.readUInt32LE(peHeaderOffset) !== 0x00004550) return null; const arch = mapPeMachine(buffer.readUInt16LE(peHeaderOffset + 4)); if (!arch) return null; @@ -121,16 +119,23 @@ function detectPeTarget(buffer) { } export function detectNativeBinaryTarget(buffer) { - return detectElfTarget(buffer) ?? detectMachTarget(buffer) ?? detectPeTarget(buffer) ?? null; + return detectElfTarget(buffer) ?? detectMachTarget(buffer) ?? detectPeTarget(buffer); } export function readNativeBinaryTarget(binaryPath) { if (!existsSync(binaryPath)) return null; + let fd; try { - return detectNativeBinaryTarget(readFileSync(binaryPath)); - } catch { + fd = openSync(binaryPath, "r"); + const buffer = Buffer.alloc(HEADER_SIZE); + const bytesRead = readSync(fd, buffer, 0, HEADER_SIZE, 0); + return detectNativeBinaryTarget(buffer.subarray(0, bytesRead)); + } catch (err) { + console.warn(` ⚠️ Could not read native binary at ${binaryPath}: ${err.message}`); return null; + } finally { + if (fd !== undefined) closeSync(fd); } } @@ -145,14 +150,14 @@ export function isNativeBinaryCompatible( return false; } } else if (runtimePlatform !== PUBLISHED_BUILD_PLATFORM || runtimeArch !== PUBLISHED_BUILD_ARCH) { - // Unknown binary layout on a non-build platform is too risky to treat as compatible. return false; } try { dlopen({ exports: {} }, binaryPath); return true; - } catch { + } catch (err) { + console.warn(` ⚠️ Native binary dlopen failed: ${err.message}`); return false; } } diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs index d8379f6b43..12fd4436e5 100644 --- a/scripts/postinstall.mjs +++ b/scripts/postinstall.mjs @@ -20,6 +20,8 @@ import { existsSync, copyFileSync, mkdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; +import { PUBLISHED_BUILD_PLATFORM, PUBLISHED_BUILD_ARCH } from "./native-binary-compat.mjs"; + const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ROOT = join(__dirname, ".."); @@ -43,23 +45,18 @@ const rootBinary = join( ); if (!existsSync(join(ROOT, "app", "node_modules", "better-sqlite3"))) { - // No standalone app directory — nothing to do (dev install, not npm global) process.exit(0); } -// The published binary is compiled for linux-x64. On any other platform/arch, -// always replace it — dlopen alone is unreliable because macOS can load an -// incompatible binary without throwing (the exact bug fixed in #312). -const BUILD_PLATFORM = "linux"; -const BUILD_ARCH = "x64"; -const platformMatch = process.platform === BUILD_PLATFORM && process.arch === BUILD_ARCH; +const platformMatch = + process.platform === PUBLISHED_BUILD_PLATFORM && process.arch === PUBLISHED_BUILD_ARCH; if (platformMatch) { try { process.dlopen({ exports: {} }, appBinary); process.exit(0); - } catch { - // Same platform but binary still incompatible (e.g. Node.js ABI mismatch) + } catch (err) { + console.warn(` ⚠️ Bundled binary incompatible despite platform match: ${err.message}`); } } @@ -70,18 +67,21 @@ if (existsSync(rootBinary)) { try { mkdirSync(dirname(appBinary), { recursive: true }); copyFileSync(rootBinary, appBinary); + } catch (err) { + console.warn(` ⚠️ Failed to copy binary: ${err.message}`); + } - // Verify the copied binary loads + try { process.dlopen({ exports: {} }, appBinary); console.log(" ✅ Native module fixed successfully!\n"); process.exit(0); - } catch { - // Copy succeeded but binary still doesn't load — fall through + } catch (err) { + console.warn(` ⚠️ Copied binary failed to load: ${err.message}`); } } // Strategy 2: Fall back to npm rebuild (may work if build tools are available) -console.log(" ⚠️ Root binary not available, attempting npm rebuild..."); +console.log(" ⚠️ Root binary not available or incompatible, attempting npm rebuild..."); try { const { execSync } = await import("node:child_process"); @@ -91,12 +91,16 @@ try { timeout: 120_000, }); - // Verify rebuild worked process.dlopen({ exports: {} }, appBinary); console.log(" ✅ Native module rebuilt successfully!\n"); process.exit(0); -} catch { - // Rebuild failed or binary still incompatible +} catch (err) { + const isTimeout = err.killed || err.signal === "SIGTERM"; + if (isTimeout) { + console.warn(" ⚠️ npm rebuild timed out after 120s."); + } else { + console.warn(` ⚠️ npm rebuild failed: ${err.message}`); + } } // If nothing worked, warn but don't fail the install — let the package stay