mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-10-20 05:40:27 +08:00
feat(cli): postinstall ABI sanity check for better-sqlite3
npm install @openrig/cli now runs a postinstall check that fails
loudly if the Node runtime can't load better-sqlite3's native
binary. Layered on top of the engines.node constraint from 587206d:
engines warns at npm level; this hard-blocks at install time with
a copy-paste fix command.
Two-phase check (Option C hybrid):
1. Version-range fast path: rejects odd-numbered Node majors
(21/23/25) and versions below 20 before attempting any native
load. Short-circuits with clear error + nvm install fix.
2. Native addon load: on supported versions, requires
better-sqlite3 and catches ABI mismatch (wrong
NODE_MODULE_VERSION). Reports npm rebuild fix command.
Error messages follow honest-errors principle: what failed + why
it matters + copy-paste fix (nvm install 22 or npm rebuild).
Files:
- packages/cli/scripts/check-abi.mjs (postinstall script)
- packages/cli/package.json scripts.postinstall wiring
- packages/cli/test/check-abi.test.ts (8 unit tests covering
supported versions, odd versions, old versions, ABI mismatch,
and fast-path short-circuit verification)
Gates: check-abi 8/8; CLI 444/445 (1 preexisting chatroom fail);
daemon 1792/1792; tsc + build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
587206db6d
commit
23e77a3acf
@@ -35,6 +35,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"postinstall": "node scripts/check-abi.mjs",
|
||||
"prepublishOnly": "bash ../../scripts/build-package.sh",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Postinstall ABI sanity check for @openrig/cli.
|
||||
// Fails loudly if better-sqlite3 native binary does not match the active
|
||||
// Node runtime. Layered on top of the engines.node constraint in
|
||||
// package.json — engines warns at npm level; this hard-blocks at install
|
||||
// time with a copy-paste fix.
|
||||
|
||||
/**
|
||||
* @param {{ nodeVersion: string, loadNativeAddon: () => void }} deps
|
||||
* @returns {{ ok: true } | { ok: false, message: string }}
|
||||
*/
|
||||
export function checkAbi({ nodeVersion, loadNativeAddon }) {
|
||||
// Phase 1: version-range check (fast path).
|
||||
// Even-numbered Node majors (20, 22, 24) are LTS lines with native addon
|
||||
// prebuilds. Odd-numbered majors (21, 23, 25) generally lack them.
|
||||
const match = nodeVersion.match(/^v?(\d+)/);
|
||||
const major = match ? parseInt(match[1], 10) : 0;
|
||||
|
||||
if (major < 20) {
|
||||
return {
|
||||
ok: false,
|
||||
message: [
|
||||
"",
|
||||
" ╔══════════════════════════════════════════════════════════════╗",
|
||||
` ║ @openrig/cli requires Node.js 20, 22, or 24 (LTS). ║`,
|
||||
` ║ Current: ${nodeVersion.padEnd(49)}║`,
|
||||
" ║ ║",
|
||||
" ║ Fix: nvm install 22 && npm install -g @openrig/cli ║",
|
||||
" ╚══════════════════════════════════════════════════════════════╝",
|
||||
"",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
if (major % 2 !== 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message: [
|
||||
"",
|
||||
" ╔══════════════════════════════════════════════════════════════╗",
|
||||
` ║ @openrig/cli does not support odd-numbered Node releases. ║`,
|
||||
` ║ Current: ${nodeVersion.padEnd(49)}║`,
|
||||
" ║ ║",
|
||||
" ║ Odd Node versions (21, 23, 25, …) lack native addon ║",
|
||||
" ║ prebuilds for better-sqlite3. The daemon will fail. ║",
|
||||
" ║ ║",
|
||||
" ║ Fix: nvm install 22 && npm install -g @openrig/cli ║",
|
||||
" ╚══════════════════════════════════════════════════════════════╝",
|
||||
"",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
// Phase 2: ABI load check (catches edge cases like local Node builds
|
||||
// or prebuild packaging gaps on supported versions).
|
||||
try {
|
||||
loadNativeAddon();
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
ok: false,
|
||||
message: [
|
||||
"",
|
||||
" ╔══════════════════════════════════════════════════════════════╗",
|
||||
" ║ better-sqlite3 native binary does not match this Node. ║",
|
||||
` ║ Current: ${nodeVersion.padEnd(49)}║`,
|
||||
" ║ ║",
|
||||
" ║ Fix: npm rebuild better-sqlite3 ║",
|
||||
" ║ or: nvm install 22 && npm install -g @openrig/cli ║",
|
||||
" ╚══════════════════════════════════════════════════════════════╝",
|
||||
"",
|
||||
` Detail: ${detail}`,
|
||||
"",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// --- Run when executed as postinstall script ---
|
||||
const isMain =
|
||||
process.argv[1] &&
|
||||
(import.meta.url === `file://${process.argv[1]}` ||
|
||||
process.argv[1].endsWith("check-abi.mjs"));
|
||||
|
||||
if (isMain) {
|
||||
const { createRequire } = await import("node:module");
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const result = checkAbi({
|
||||
nodeVersion: process.version,
|
||||
loadNativeAddon: () => require("better-sqlite3"),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
console.error(result.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { checkAbi } from "../scripts/check-abi.mjs";
|
||||
|
||||
describe("postinstall ABI check", () => {
|
||||
it("passes on supported even-numbered LTS with working native addon", () => {
|
||||
const result = checkAbi({
|
||||
nodeVersion: "v22.22.1",
|
||||
loadNativeAddon: () => {},
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("passes on Node 20 LTS", () => {
|
||||
const result = checkAbi({
|
||||
nodeVersion: "v20.18.0",
|
||||
loadNativeAddon: () => {},
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("passes on Node 24 LTS", () => {
|
||||
const result = checkAbi({
|
||||
nodeVersion: "v24.0.0",
|
||||
loadNativeAddon: () => {},
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("fails on odd-numbered Node 25 with honest error + fix command", () => {
|
||||
const result = checkAbi({
|
||||
nodeVersion: "v25.8.0",
|
||||
loadNativeAddon: () => {},
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.message).toContain("odd-numbered");
|
||||
expect(result.message).toContain("v25.8.0");
|
||||
expect(result.message).toContain("nvm install 22");
|
||||
});
|
||||
|
||||
it("fails on odd-numbered Node 23 with honest error", () => {
|
||||
const result = checkAbi({
|
||||
nodeVersion: "v23.5.0",
|
||||
loadNativeAddon: () => {},
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.message).toContain("odd-numbered");
|
||||
});
|
||||
|
||||
it("fails on Node below 20 with version-too-low error", () => {
|
||||
const result = checkAbi({
|
||||
nodeVersion: "v18.20.0",
|
||||
loadNativeAddon: () => {},
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.message).toContain("requires Node.js 20, 22, or 24");
|
||||
expect(result.message).toContain("nvm install 22");
|
||||
});
|
||||
|
||||
it("fails with ABI mismatch error when native addon throws on supported version", () => {
|
||||
const result = checkAbi({
|
||||
nodeVersion: "v22.22.1",
|
||||
loadNativeAddon: () => {
|
||||
throw new Error(
|
||||
"The module was compiled against a different Node.js version using NODE_MODULE_VERSION 127. " +
|
||||
"This version of Node.js requires NODE_MODULE_VERSION 131."
|
||||
);
|
||||
},
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.message).toContain("native binary does not match");
|
||||
expect(result.message).toContain("npm rebuild better-sqlite3");
|
||||
expect(result.message).toContain("NODE_MODULE_VERSION");
|
||||
});
|
||||
|
||||
it("skips native addon check entirely for odd versions (fast path)", () => {
|
||||
let addonCalled = false;
|
||||
const result = checkAbi({
|
||||
nodeVersion: "v25.8.0",
|
||||
loadNativeAddon: () => { addonCalled = true; },
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
// Version check short-circuits before trying to load the addon
|
||||
expect(addonCalled).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user