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:
+12
-21
@@ -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 ───────────────────────────────────────────
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"open-sse/mcp-server/",
|
||||
"src/shared/contracts/",
|
||||
"scripts/postinstall.mjs",
|
||||
"scripts/native-binary-compat.mjs",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
|
||||
@@ -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
@@ -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("");
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user