Repository navigation
fix(cli): fix postinstall native binary rebuild regression (#321) #327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c859665
69d28be
5a244aa
a22f0a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
|
||
| if (platformMatch) { | ||
| try { | ||
| process.dlopen({ exports: {} }, appBinary); | ||
| process.exit(0); | ||
| } catch (err) { | ||
| console.warn(` ⚠️ Bundled binary incompatible despite platform match: ${err.message}`); | ||
| } | ||
| } | ||
|
|
||
| console.log(`\n 🔧 Fixing better-sqlite3 binary for ${process.platform}-${process.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; | ||
| // 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}`); | ||
| } | ||
|
|
||
| if (!needsRebuild) { | ||
| try { | ||
| process.dlopen({ exports: {} }, buildInfoPath); | ||
| process.dlopen({ exports: {} }, appBinary); | ||
| console.log(" ✅ Native module fixed successfully!\n"); | ||
| process.exit(0); | ||
| } catch { | ||
| // Same platform but binary still incompatible (e.g. Node.js ABI mismatch) — rebuild | ||
| } catch (err) { | ||
| console.warn(` ⚠️ Copied binary failed to load: ${err.message}`); | ||
| } | ||
| } | ||
|
|
||
| console.log(`\n 🔧 Rebuilding better-sqlite3 for ${process.platform}-${process.arch}...`); | ||
| // 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"); | ||
| } | ||
| console.error(""); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| // Verify the rebuilt binary actually loads | ||
| try { | ||
| process.dlopen({ exports: {} }, buildInfoPath); | ||
| process.dlopen({ exports: {} }, appBinary); | ||
| 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"); | ||
| 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); | ||
| } | ||
|
Comment on lines
86
to
104
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the copy logic, this try {
const { execSync } = await import("node:child_process");
execSync("npm rebuild better-sqlite3", {
cwd: join(ROOT, "app"),
stdio: "inherit",
timeout: 120_000,
});
try {
process.dlopen({ exports: {} }, appBinary);
console.log(" ✅ Native module rebuilt successfully!\n");
process.exit(0);
} catch (err) {
console.warn(` ⚠️ Rebuilt binary failed to load: ${err.message}`);
}
} 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 | ||
| // 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(""); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current structure with two separate
try...catchblocks can lead to confusing log messages. IfcopyFileSyncfails, the script will still attempt todlopenthe (likely incompatible or non-existent) binary inappBinaryand log 'Copied binary failed to load', which is misleading. Nesting thedlopencheck inside thetryblock for the file copy would make the logic more robust and the error reporting clearer.