open-design/scripts/postinstall.mjs
Chris 9674f48f2f
fix(postinstall): auto-rebuild better-sqlite3 on Node.js ABI mismatch (#813)
* fix(postinstall): auto-rebuild better-sqlite3 on Node.js ABI mismatch

prebuild-install fetches a prebuilt binary for the Node.js version active
at install time. On systems where the Node ABI differs from Node 24 (e.g.
Arch Linux system Node, Node 22 LTS, Node 25), or after switching versions,
the addon fails to dlopen at daemon startup.

postinstall now tries to load the native addon after the workspace builds.
On failure it locates node-gyp from the pnpm virtual store (bundled with
better-sqlite3) and rebuilds from source — no external tooling beyond a
C++ compiler required. pnpm install becomes self-healing across Node versions.

Also adds a QUICKSTART troubleshooting entry for users with ignore-scripts=true
who need to run `node scripts/postinstall.mjs` manually.

* fix(postinstall): correct better-sqlite3 path and rebuild mechanism

Two bugs in the initial implementation caught in review:

- better-sqlite3 is declared by apps/daemon, not the workspace root.
  node_modules/better-sqlite3 at root does not exist in a normal pnpm
  install, so existsSync() was always false and the check never ran.
  Fix: resolve via createRequire from apps/daemon/package.json.

- better-sqlite3@12.9.0 depends only on bindings and prebuild-install,
  not node-gyp. The assumed sibling path in the pnpm store does not
  exist, so the rebuild branch was hitting the "not found" exit instead
  of rebuilding. Fix: use pnpm --filter @open-design/daemon rebuild
  better-sqlite3 so pnpm manages node-gyp through its own lifecycle.

Also expands the QUICKSTART troubleshooting entry with the manual
rebuild command, a verification step, and build tool prerequisites.

* fix(quickstart): scope better-sqlite3 verification to daemon package
2026-05-08 11:25:26 +08:00

88 lines
2.8 KiB
JavaScript

import { spawnSync } from "node:child_process";
import { createRequire } from "node:module";
import { dirname, extname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, "..");
const buildTargets = [
"packages/contracts",
"packages/sidecar-proto",
"packages/sidecar",
"packages/platform",
"tools/dev",
"tools/pack",
];
const jsExtensions = new Set([".js", ".cjs", ".mjs"]);
function resolvePackageManagerInvocation() {
const pnpmExecPath = process.env.npm_execpath;
if (pnpmExecPath != null && pnpmExecPath.length > 0) {
if (jsExtensions.has(extname(pnpmExecPath).toLowerCase())) {
return { argsPrefix: [pnpmExecPath], command: process.execPath };
}
return { argsPrefix: [], command: pnpmExecPath };
}
return { argsPrefix: [], command: process.platform === "win32" ? "pnpm.cmd" : "pnpm" };
}
const packageManager = resolvePackageManagerInvocation();
for (const target of buildTargets) {
const result = spawnSync(
packageManager.command,
[...packageManager.argsPrefix, "-C", target, "run", "build"],
{
cwd: repoRoot,
stdio: "inherit",
},
);
if (result.error != null) {
throw result.error;
}
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
// Verify the better-sqlite3 native addon loads under the current Node.js ABI.
// better-sqlite3 is a dep of apps/daemon (not the workspace root), so resolve
// it from the daemon package context. prebuild-install may have fetched a
// prebuilt binary for a different ABI (e.g. after switching between Node 22 /
// 24 / 25). When the addon fails to dlopen, pnpm rebuild handles the rebuild
// using its own node-gyp lifecycle — no assumptions about where node-gyp lives.
const req = createRequire(resolve(repoRoot, "apps/daemon/package.json"));
let needsRebuild = false;
try {
req("better-sqlite3");
} catch (e) {
// MODULE_NOT_FOUND means daemon deps aren't installed yet — not our problem.
// Any other error (ERR_DLOPEN_FAILED, ABI mismatch, etc.) warrants a rebuild.
if (e?.code !== "MODULE_NOT_FOUND") {
needsRebuild = true;
}
}
if (needsRebuild) {
process.stdout.write(
`postinstall: rebuilding better-sqlite3 for Node.js ${process.version}...\n`,
);
const rebuild = spawnSync(
packageManager.command,
[...packageManager.argsPrefix, "--filter", "@open-design/daemon", "rebuild", "better-sqlite3"],
{ cwd: repoRoot, stdio: "inherit" },
);
if (rebuild.error != null) throw rebuild.error;
if (rebuild.status !== 0) {
process.stderr.write(
"postinstall: better-sqlite3 rebuild failed.\n" +
"Install build tools (python3, make, g++ or clang++) then run: pnpm install\n",
);
process.exit(rebuild.status ?? 1);
}
}