Merge pull request #327 from ardaaltinors/fix/postinstall-copy-native-binary

fix(cli): fix postinstall native binary rebuild regression (#321)
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-03-12 06:58:10 -03:00
committed by GitHub
5 changed files with 397 additions and 65 deletions
+12 -21
View File
@@ -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 ───────────────────────────────────────────
+1
View File
@@ -13,6 +13,7 @@
"open-sse/mcp-server/",
"src/shared/contracts/",
"scripts/postinstall.mjs",
"scripts/native-binary-compat.mjs",
"README.md",
"LICENSE"
],
+163
View File
@@ -0,0 +1,163 @@
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:
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);
}
const ELF_MAGIC = 0x7f454c46;
function detectElfTarget(buffer) {
if (buffer.length < 20) return null;
if (buffer.readUInt32BE(0) !== ELF_MAGIC) return null;
const littleEndian = buffer[5] !== 2;
const arch = mapElfMachine(readUInt16(buffer, 18, littleEndian));
if (!arch) return null;
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);
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 (!FAT_MACH_MAGIC.has(magic)) return null;
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();
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.readUInt16LE(0) !== 0x5a4d) return null;
const peHeaderOffset = buffer.readUInt32LE(0x3c);
if (peHeaderOffset + 6 > buffer.length) return null;
if (buffer.readUInt32LE(peHeaderOffset) !== 0x00004550) 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);
}
export function readNativeBinaryTarget(binaryPath) {
if (!existsSync(binaryPath)) return null;
let fd;
try {
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);
}
}
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) {
return false;
}
try {
dlopen({ exports: {} }, binaryPath);
return true;
} catch (err) {
console.warn(` ⚠️ Native binary dlopen failed: ${err.message}`);
return false;
}
}
+78 -44
View File
@@ -1,81 +1,115 @@
#!/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";
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, "..");
// 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"))) {
process.exit(0);
}
const buildInfoPath = join(appNodeModules, "build", "Release", "better_sqlite3.node");
const platformMatch =
process.platform === PUBLISHED_BUILD_PLATFORM && process.arch === PUBLISHED_BUILD_ARCH;
// 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.
const BUILD_PLATFORM = "linux";
const BUILD_ARCH = "x64";
const needsRebuild = 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
} catch (err) {
console.warn(` ⚠️ Bundled binary incompatible despite platform match: ${err.message}`);
}
}
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);
} catch (err) {
console.warn(` ⚠️ Failed to copy binary: ${err.message}`);
}
try {
process.dlopen({ exports: {} }, appBinary);
console.log(" ✅ Native module fixed successfully!\n");
process.exit(0);
} 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 or incompatible, 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");
process.dlopen({ exports: {} }, appBinary);
console.log(" ✅ Native module rebuilt successfully!\n");
process.exit(0);
} 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}`);
}
console.error("");
process.exit(1);
}
// 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("");
+143
View File
@@ -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
);
});
});
});