Skip to content

fix(cli): fix postinstall native binary rebuild regression (#321) - #327

Merged
diegosouzapw merged 4 commits into
cheaper-inference:mainfrom
ardaaltinors:fix/postinstall-copy-native-binary
Mar 12, 2026
Merged

diegosouzapw merged 4 commits into
cheaper-inference:mainfrom
ardaaltinors:fix/postinstall-copy-native-binary

Conversation

@ardaaltinors

@ardaaltinors ardaaltinors commented Mar 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes issue #321

Summary

PR #313 introduced process.exit(1) on postinstall rebuild failure, which caused npm to rollback the entire package — leaving users with nothing installed. The root cause was that npm rebuild better-sqlite3 inside the standalone app/ directory is a no-op (Next.js file tracing only copies runtime files, not binding.gyp, source, or prebuild-install).

New approach

Instead of rebuilding, copy the correctly-built binary from root node_modules/. npm already compiles better sqlite3 for the correct platform during npm install -g (it's a top-level dependency). The postinstall now simply copies that binary into the standalone app/ directory.

Changes

  • scripts/postinstall.mjs — 3-tier strategy:
    1. Platform/arch guard: if build platform matches, dlopen check and exit
    2. Copy binary from root node_modules/ to app/node_modules/ (no rebuild
      needed)
    3. Fall back to npm rebuild if root binary unavailable
    4. Warn but don't exit(1) if nothing works — package stays installed
  • scripts/native-binary-compat.mjs (new) — Reads ELF/Mach-O/PE binary headers
    to detect actual target platform/arch, avoiding macOS dlopen false positives. Only
    reads first 4KB of the file.
  • bin/omniroute.mjs — Pre-flight binary compatibility check using header
    detection before starting server
  • package.json — Added native-binary-compat.mjs to files array

Docker test results (4/4 pass)

Environment Binary dlopen Result
Node 22 / Debian / linux-arm64 ELF aarch64 SUCCESS PASS
Node 22 / Debian / linux-amd64 ELF x86-64 SUCCESS PASS
Node 20 / Debian / linux-arm64 ELF aarch64 SUCCESS PASS
Node 22 / Alpine / linux-arm64 ELF aarch64 (musl) SUCCESS PASS

Test plan

  • npm test — 602/602 pass
  • npm run lint — 0 errors
  • Docker: Node 22 Debian arm64
  • Docker: Node 22 Debian amd64
  • Docker: Node 20 Debian arm64
  • Docker: Node 22 Alpine arm64
  • Local macOS ARM: postinstall detects mismatch and copies binary

…ding (cheaper-inference#321)

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 (cheaper-inference#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
…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
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.
…install

- 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
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a significant installation regression by overhauling how the better-sqlite3 native module is handled during postinstall. Instead of relying on a potentially failing rebuild process within the standalone application directory, the system now intelligently copies an already compatible binary from the main node_modules. Furthermore, a new, more accurate native binary compatibility check has been introduced and integrated into the application's startup, providing a robust mechanism to verify module integrity and prevent runtime issues, ultimately leading to a more stable and user-friendly installation experience.

Highlights

  • Postinstall Regression Fix: Addressed a critical regression where process.exit(1) on better-sqlite3 rebuild failure during postinstall caused npm to rollback the entire package, leaving users with nothing installed.
  • New Binary Handling Strategy: Implemented a new 3-tier strategy in scripts/postinstall.mjs that prioritizes copying the correctly-built better-sqlite3 binary from the root node_modules/ to the standalone app/ directory, avoiding the need for a rebuild.
  • Robust Native Binary Compatibility Check: Introduced a new utility script, scripts/native-binary-compat.mjs, which reads ELF/Mach-O/PE binary headers to accurately detect the actual target platform and architecture of native modules, preventing false positives from dlopen.
  • Pre-flight Compatibility Verification: Integrated the new native binary compatibility check into bin/omniroute.mjs to perform a pre-flight verification of the better-sqlite3 binary before the server starts, enhancing reliability.
  • Graceful Failure for Postinstall: Modified the postinstall script to warn instead of exiting with an error if all strategies for fixing the native module fail, ensuring the package remains installed and allowing manual intervention.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • bin/omniroute.mjs
    • Imported isNativeBinaryCompatible for pre-flight checks.
    • Replaced the dlopen based compatibility check with the new isNativeBinaryCompatible function.
  • package.json
    • Added scripts/native-binary-compat.mjs to the files array.
  • scripts/native-binary-compat.mjs
    • Added a new script to detect the target platform and architecture of native binaries (ELF, Mach-O, PE) by reading their headers.
    • Exported detectNativeBinaryTarget, readNativeBinaryTarget, and isNativeBinaryCompatible functions.
  • scripts/postinstall.mjs
    • Refactored the postinstall script to implement a 3-tier strategy for better-sqlite3 binary: check platform match, copy from root node_modules, then fallback to npm rebuild.
    • Changed the script to warn instead of process.exit(1) on failure, preventing full package rollback.
    • Imported PUBLISHED_BUILD_PLATFORM and PUBLISHED_BUILD_ARCH from native-binary-compat.mjs.
  • tests/unit/native-binary-compat.test.mjs
    • Added comprehensive unit tests for detectNativeBinaryTarget and isNativeBinaryCompatible functions, covering ELF, Mach-O, and PE binary detection and compatibility scenarios.
Activity
  • The author performed extensive testing, including npm test, npm run lint, and Docker tests across various Node.js versions, Debian, and Alpine Linux environments for both arm64 and amd64 architectures.
  • Local macOS ARM testing was also conducted to verify postinstall binary detection and copying.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request provides a robust fix for a postinstall script regression that caused installation rollbacks. The new approach of copying a pre-compiled binary before attempting a rebuild is a significant improvement. The introduction of a native binary compatibility checker that inspects file headers is an excellent and well-implemented solution to avoid platform-related issues. The changes greatly enhance the installation experience for users on different operating systems. I've added a couple of suggestions to refine the error handling in the postinstall script to provide clearer feedback if any step fails.

Comment thread scripts/postinstall.mjs
Comment on lines +67 to 80
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}`);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current structure with two separate try...catch blocks can lead to confusing log messages. If copyFileSync fails, the script will still attempt to dlopen the (likely incompatible or non-existent) binary in appBinary and log 'Copied binary failed to load', which is misleading. Nesting the dlopen check inside the try block for the file copy would make the logic more robust and the error reporting clearer.

  try {
    mkdirSync(dirname(appBinary), { recursive: true });
    copyFileSync(rootBinary, appBinary);
    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}`);
    }
  } catch (err) {
    console.warn(`  ⚠️  Failed to copy binary: ${err.message}`);
  }

Comment thread scripts/postinstall.mjs
Comment on lines 86 to 104
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the copy logic, this try...catch block combines the execSync call for rebuilding and the dlopen verification. If the rebuild succeeds but dlopen fails, the error message 'npm rebuild failed' is inaccurate. Separating these into nested try...catch blocks will provide more precise feedback to the user about which step actually failed.

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}`);
  }
}

@kilo-code-bot

kilo-code-bot Bot commented Mar 12, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (4 files)
  • bin/omniroute.mjs - Uses new binary compatibility check
  • package.json - Added native-binary-compat.mjs to files array
  • scripts/native-binary-compat.mjs - New binary detection library
  • scripts/postinstall.mjs - Updated to copy binaries from root node_modules
  • tests/unit/native-binary-compat.test.mjs - Comprehensive test coverage

The PR implements a robust solution for native binary compatibility:

  1. Header-based detection - Reads ELF/Mach-O/PE binary headers to detect actual target platform/arch before attempting dlopen, avoiding macOS false positives
  2. Copy-based fix - Copies correctly-built binary from root node_modules instead of requiring rebuild (no build tools needed)
  3. Graceful degradation - Warns but doesn't fail installation if binary can't be fixed
  4. Good test coverage - Tests cover Linux x64, macOS arm64, and Windows x64 detection

@diegosouzapw
diegosouzapw merged commit e53c760 into cheaper-inference:main Mar 12, 2026
10 checks passed
diegosouzapw added a commit that referenced this pull request Mar 12, 2026
feat(claude): [1m] suffix for 1M extended context (PR #311 @DavyMassoneto)
feat(registry): new models for iFlow, Qwen, Kimi (PR #326 @nyatoru)
fix(cli): postinstall binary copy instead of rebuild (PR #327 @ardaaltinors, fixes #321)
docs: English Remote OAuth guide in README (PR #329, fixes #318)
test: 3 unit tests for parseModel [1m] suffix
@diegosouzapw

Copy link
Copy Markdown
Collaborator

✅ Merged in v2.3.2! Fantastic work @ardaaltinors — the ELF/Mach-O/PE header detection approach is exactly the right fix. Those 4 Docker test results seal it. Thank you! 🚀

Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…stall-copy-native-binary

fix(cli): fix postinstall native binary rebuild regression (cheaper-inference#321)
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
feat(claude): [1m] suffix for 1M extended context (PR cheaper-inference#311 @DavyMassoneto)
feat(registry): new models for iFlow, Qwen, Kimi (PR cheaper-inference#326 @nyatoru)
fix(cli): postinstall binary copy instead of rebuild (PR cheaper-inference#327 @ardaaltinors, fixes cheaper-inference#321)
docs: English Remote OAuth guide in README (PR cheaper-inference#329, fixes cheaper-inference#318)
test: 3 unit tests for parseModel [1m] suffix
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…stall-copy-native-binary

fix(cli): fix postinstall native binary rebuild regression (cheaper-inference#321)
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
feat(claude): [1m] suffix for 1M extended context (PR cheaper-inference#311 @DavyMassoneto)
feat(registry): new models for iFlow, Qwen, Kimi (PR cheaper-inference#326 @nyatoru)
fix(cli): postinstall binary copy instead of rebuild (PR cheaper-inference#327 @ardaaltinors, fixes cheaper-inference#321)
docs: English Remote OAuth guide in README (PR cheaper-inference#329, fixes cheaper-inference#318)
test: 3 unit tests for parseModel [1m] suffix
diegosouzapw added a commit that referenced this pull request Oct 24, 2026
Dependabot alerts #327 (high), #328 and #333 (critical) flag next
>= 16.0.0, <= 16.4.0. Same version and lockfile delta as the Dependabot PR #17969
(next + @next/env + the @next/swc-* binaries 16.3.8 -> 16.4.2), whose Build,
E2E 1-9/9, Electron package smoke and Ecosystem jobs passed with it.

Landing it next to the dompurify bump clears all three critical npm audit
findings on the base (dompurify, monaco-editor via dompurify, next), which the
Lint job's `npm audit --audit-level=critical` blocks on; on separate PRs each one
still carries the other's critical finding.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants