mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 12:42:21 +03:00
perf(electron): verify better-sqlite3 v13 Node-API prebuilds instead of source rebuild (#10367)
better-sqlite3 v13 ships Node-API prebuilds for every packaged platform (darwin/linux/linuxmusl/win32 x x64/arm64) inside the npm tarball, so the Electron-ABI node-gyp source rebuild in prepare-electron-standalone.mjs is obsolete. Replace it with a fail-fast prebuild verification that mirrors better-sqlite3 lib/binding.js selection, and strip build/deps/src so the packaged loader can only resolve the prebuild. Verified locally on darwin-arm64: the same darwin-arm64.node prebuild loads under both Node 24 (NODE_MODULE_VERSION 137) and Electron 43.3.0 under ELECTRON_RUN_AS_NODE (148); DB create/migrate/read/write/close/reopen pass in both runtimes and cross-runtime on each other's database files. Issue #10321 Stage 6.
This commit is contained in:
@@ -39,7 +39,7 @@
|
||||
* prune + validate (pack-artifact-policy) - Y - UNIQUE (prepublish)
|
||||
* data/ dir creation - Y - UNIQUE (prepublish)
|
||||
* --- electron-UNIQUE ---
|
||||
* better-sqlite3 native strip + Electron-ABI rebuild - - Y UNIQUE (electron)
|
||||
* better-sqlite3 prebuild verify + compile-input strip - - Y UNIQUE (electron)
|
||||
* Turbopack hashed-module symlink materialize (node_modules) - - Y SHARED (opt-in: materializeSymlinks)
|
||||
* symlink guard (assertBundleIsPackagable) - - Y UNIQUE (electron)
|
||||
* removeGeneratedElectronArtifacts - - Y UNIQUE (electron)
|
||||
|
||||
@@ -1,17 +1,73 @@
|
||||
/**
|
||||
* Spawn plan for the better-sqlite3 Electron-ABI rebuild (pure — import-safe for tests).
|
||||
* better-sqlite3 Node-API prebuild planning (pure — import-safe for tests).
|
||||
*
|
||||
* On Windows, `npx.cmd` MUST be spawned through a shell: since Node's
|
||||
* CVE-2024-27980 hardening, spawning `.cmd`/`.bat` shims without `shell: true`
|
||||
* fails outright (spawnSync returns `status: null`), which broke the v3.8.47
|
||||
* tag build ("better-sqlite3 rebuild against electron 43.1.0 failed (exit null)").
|
||||
* The args are a fixed literal list — no untrusted input reaches the shell.
|
||||
* Since better-sqlite3 v13 the packaged app no longer compiles the addon from
|
||||
* source against the Electron headers: v13 ships Node-API (NAPI_VERSION=10)
|
||||
* prebuilds for every platform we package, and Node-API addons are
|
||||
* ABI-independent, so the same prebuild runs under plain Node and under the
|
||||
* packaged app's ELECTRON_RUN_AS_NODE server (verified against electron 43 /
|
||||
* NODE_MODULE_VERSION 148 — issue #10321 Stage 6). The historical
|
||||
* `npx node-gyp rebuild` spawn plan existed because better-sqlite3@12 only
|
||||
* shipped prebuilds up to electron-v146; v13 makes it obsolete.
|
||||
*
|
||||
* This module mirrors better-sqlite3's own `lib/binding.js` selection logic so
|
||||
* the build fails fast when the prebuild the runtime loader would pick is
|
||||
* missing, instead of shipping an app that falls back to sql.js and OOMs on a
|
||||
* user machine.
|
||||
*/
|
||||
export function buildRebuildSpawnPlan(platform) {
|
||||
const win = platform === "win32";
|
||||
return {
|
||||
command: win ? "npx.cmd" : "npx",
|
||||
args: ["--yes", "node-gyp", "rebuild"],
|
||||
shell: win,
|
||||
};
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
export const SQLITE_PREBUILD_PLATFORMS = ["darwin", "linux", "linuxmusl", "win32"];
|
||||
export const SQLITE_PREBUILD_ARCHS = ["x64", "arm64"];
|
||||
|
||||
/**
|
||||
* Resolve the prebuild file name better-sqlite3's loader would pick for the
|
||||
* given platform/arch. Mirrors lib/binding.js: linux without a glibc runtime
|
||||
* version resolves to the linuxmusl prebuild.
|
||||
*
|
||||
* @param {string} platform - process.platform ("linux", "darwin", "win32")
|
||||
* @param {string} arch - process.arch ("x64", "arm64")
|
||||
* @param {{ glibcVersionRuntime?: string | null }} [reportHeader] - parsed
|
||||
* process.report.getReport().header (injectable for tests)
|
||||
*/
|
||||
export function sqlitePrebuildFileName(platform, arch, reportHeader) {
|
||||
const isMusl = platform === "linux" && !reportHeader?.glibcVersionRuntime;
|
||||
const target = `${isMusl ? "linuxmusl" : platform}-${arch}`;
|
||||
return `${target}.node`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a prebuild check applies for this platform/arch combination.
|
||||
* Unsupported combos (e.g. freebsd-ia32) are skipped rather than failed: the
|
||||
* runtime loader falls back to node-gyp build/ locations for those, which we
|
||||
* do not package.
|
||||
*/
|
||||
export function isSqlitePrebuildSupported(platform, arch) {
|
||||
return SQLITE_PREBUILD_PLATFORMS.includes(platform) && SQLITE_PREBUILD_ARCHS.includes(arch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the runtime-selected prebuild exists in a staged module.
|
||||
* Unsupported platform/arch combinations retain the historical fallback path.
|
||||
*
|
||||
* @returns {string | null} selected prebuild path, or null when unsupported
|
||||
*/
|
||||
export function assertSqlitePrebuildExists(moduleDir, platform, arch, reportHeader) {
|
||||
if (!isSqlitePrebuildSupported(platform, arch)) return null;
|
||||
|
||||
const expected = join(
|
||||
moduleDir,
|
||||
"prebuilds",
|
||||
sqlitePrebuildFileName(platform, arch, reportHeader)
|
||||
);
|
||||
if (!existsSync(expected)) {
|
||||
throw new Error(
|
||||
`[electron] better-sqlite3 prebuild missing for ${platform}-${arch} ` +
|
||||
`(${expected}). The packaged app would fall back to sql.js and OOM. ` +
|
||||
`Restore the prebuilds/ directory (npm cache / registry tarball) before packaging.`
|
||||
);
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { cpSync, existsSync, lstatSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||
import { existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
|
||||
import { basename, dirname, join, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { assembleStandalone } from "./assembleStandalone.mjs";
|
||||
import { buildRebuildSpawnPlan } from "./electronRebuildPlan.mjs";
|
||||
import { assertSqlitePrebuildExists } from "./electronRebuildPlan.mjs";
|
||||
import { pruneElectronRuntimeDocs } from "./electronRuntimeDocs.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -90,9 +89,7 @@ function removeNativeModules(baseDir, prefixes = ["keytar"]) {
|
||||
// user machine as "Internal Server Error" on every route.
|
||||
function assertNoStaleHashedNatives(baseDir, prefixes) {
|
||||
if (!existsSync(baseDir)) return;
|
||||
const leftovers = readdirSync(baseDir).filter((dir) =>
|
||||
prefixes.some((p) => dir.startsWith(p))
|
||||
);
|
||||
const leftovers = readdirSync(baseDir).filter((dir) => prefixes.some((p) => dir.startsWith(p)));
|
||||
if (leftovers.length > 0) {
|
||||
throw new Error(
|
||||
`[electron] stale native module copies survived cleanup in ${baseDir}: ` +
|
||||
@@ -102,77 +99,43 @@ function assertNoStaleHashedNatives(baseDir, prefixes) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Electron-UNIQUE: rebuild better-sqlite3 against the Electron ABI --------
|
||||
// --- Electron-UNIQUE: verify better-sqlite3 Node-API prebuilds ----------------
|
||||
//
|
||||
// The `npm ci` at the repo root compiles better-sqlite3 for the CI *Node* ABI
|
||||
// (e.g. 137 for Node 24). The packaged app runs its Next.js server via
|
||||
// ELECTRON_RUN_AS_NODE, so it needs the *Electron* ABI (146 for electron 42,
|
||||
// 148 for electron 43). We cannot rely on electron-builder's @electron/rebuild
|
||||
// here: it searches `electron/node_modules` (where better-sqlite3 does not live)
|
||||
// and, with the default prebuild path, tries to fetch a prebuilt binary — but
|
||||
// better-sqlite3@12.11.1 only ships prebuilds up to electron-v146, so electron
|
||||
// 43 (v148) silently gets no rebuild and the app dies with "Nenhum driver
|
||||
// SQLite disponível — better-sqlite3 (falhou)".
|
||||
// better-sqlite3 >= 13 ships Node-API (NAPI_VERSION=10) prebuilds for every
|
||||
// platform we package (darwin/linux/linuxmusl/win32 × x64/arm64) inside the
|
||||
// npm tarball. Node-API addons are ABI-independent, so the same prebuild runs
|
||||
// under plain Node (CI, CLI) and under the packaged app's ELECTRON_RUN_AS_NODE
|
||||
// server (verified against electron 43 / NODE_MODULE_VERSION 148 — issue
|
||||
// #10321 Stage 6). The historical source rebuild below existed because
|
||||
// better-sqlite3@12 only shipped prebuilds up to electron-v146 and electron 43
|
||||
// (v148) silently got no binary; v13 makes that obsolete.
|
||||
//
|
||||
// Instead we copy the *full* module (source + binding.gyp) from the root into
|
||||
// the standalone and compile it from source against the Electron headers, so
|
||||
// `bindings` finds a correct build/Release/better_sqlite3.node regardless of
|
||||
// prebuild availability. Robust to any current/future electron version.
|
||||
// Instead of compiling from source on every build (tens of seconds to minutes
|
||||
// per platform), we fail fast when the prebuild for the CURRENT build platform
|
||||
// is missing — a missing prebuild must kill the build here, not the app on a
|
||||
// user machine with "Nenhum driver SQLite disponível — better-sqlite3 (falhou)".
|
||||
|
||||
function readElectronVersion() {
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, "electron", "package.json"), "utf8"));
|
||||
const raw = pkg.devDependencies?.electron || pkg.dependencies?.electron || "";
|
||||
return String(raw).replace(/^[\^~]/, "");
|
||||
}
|
||||
|
||||
function rebuildBetterSqlite3ForElectron(standaloneNodeModules) {
|
||||
const srcMod = join(ROOT, "node_modules", "better-sqlite3");
|
||||
if (!existsSync(srcMod)) {
|
||||
console.warn("[electron] better-sqlite3 not found at repo root — skipping ABI rebuild.");
|
||||
function verifyBetterSqlite3Prebuilds(standaloneNodeModules) {
|
||||
const destMod = join(standaloneNodeModules, "better-sqlite3");
|
||||
if (!existsSync(destMod)) {
|
||||
console.warn("[electron] better-sqlite3 not found in standalone — skipping prebuild check.");
|
||||
return;
|
||||
}
|
||||
const electronVersion = readElectronVersion();
|
||||
if (!electronVersion) {
|
||||
throw new Error("[electron] could not resolve electron version for better-sqlite3 rebuild.");
|
||||
}
|
||||
const destMod = join(standaloneNodeModules, "better-sqlite3");
|
||||
// copyNatives only copies build/; we need the full module (src + binding.gyp)
|
||||
// to compile from source. Overwrite the copied Node-ABI build in the process.
|
||||
cpSync(srcMod, destMod, { recursive: true, force: true });
|
||||
rmSync(join(destMod, "build"), { recursive: true, force: true });
|
||||
|
||||
console.log(`[electron] rebuilding better-sqlite3 against electron ${electronVersion} ABI…`);
|
||||
const plan = buildRebuildSpawnPlan(process.platform);
|
||||
const result = spawnSync(
|
||||
plan.command,
|
||||
plan.args,
|
||||
{
|
||||
cwd: destMod,
|
||||
stdio: "inherit",
|
||||
// .cmd shims must go through a shell on Windows (CVE-2024-27980 hardening
|
||||
// makes a shell-less spawn fail with status null); args are fixed literals.
|
||||
shell: plan.shell,
|
||||
// Compile against the Electron headers (not Node's) so the .node lands in
|
||||
// build/Release with the Electron NODE_MODULE_VERSION. No shell interpolation.
|
||||
env: {
|
||||
...process.env,
|
||||
npm_config_runtime: "electron",
|
||||
npm_config_target: electronVersion,
|
||||
npm_config_disturl: "https://electronjs.org/headers",
|
||||
npm_config_arch: process.arch,
|
||||
npm_config_build_from_source: "true",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`[electron] better-sqlite3 rebuild against electron ${electronVersion} failed (exit ${result.status}).`
|
||||
);
|
||||
}
|
||||
// Drop the now-unneeded compile inputs to keep the packaged app lean.
|
||||
for (const dir of ["deps", "src", "build/Debug", "build/obj.target"]) {
|
||||
// Fail fast when the loader would find no prebuild for THIS build platform.
|
||||
// Mirrors better-sqlite3's own lib/binding.js selection logic.
|
||||
const reportHeader = process.report?.getReport?.().header;
|
||||
assertSqlitePrebuildExists(destMod, process.platform, process.arch, reportHeader);
|
||||
|
||||
// Drop compile inputs and stale Node-ABI build outputs to keep the packaged
|
||||
// app lean and to guarantee the loader resolves the prebuild, not a leftover
|
||||
// build/Release/better_sqlite3.node compiled for a different ABI.
|
||||
for (const dir of ["build", "deps", "src"]) {
|
||||
rmSync(join(destMod, dir), { recursive: true, force: true });
|
||||
}
|
||||
console.log(
|
||||
`[electron] better-sqlite3 Node-API prebuilds verified for ${process.platform}-${process.arch}.`
|
||||
);
|
||||
}
|
||||
|
||||
function logContextualError(error) {
|
||||
@@ -217,12 +180,12 @@ if (docsPrune.removedFiles > 0) {
|
||||
// Electron-UNIQUE post-assembly steps
|
||||
removeGeneratedElectronArtifacts();
|
||||
|
||||
// Rebuild better-sqlite3 from source against the Electron ABI in the primary
|
||||
// node_modules (where the standalone server resolves it). keytar is still
|
||||
// stripped so electron-builder's @electron/rebuild handles it (it has electron
|
||||
// prebuilds); also drop any stray Node-ABI better-sqlite3 under .next/node_modules
|
||||
// so it cannot shadow the rebuilt one.
|
||||
rebuildBetterSqlite3ForElectron(join(ELECTRON_STANDALONE_DIR, "node_modules"));
|
||||
// Verify better-sqlite3 Node-API prebuilds in the primary node_modules (where
|
||||
// the standalone server resolves it). keytar is still stripped so
|
||||
// electron-builder's @electron/rebuild handles it (it has electron prebuilds);
|
||||
// also drop any stray better-sqlite3 under .next/node_modules so it cannot
|
||||
// shadow the prebuild-backed one.
|
||||
verifyBetterSqlite3Prebuilds(join(ELECTRON_STANDALONE_DIR, "node_modules"));
|
||||
removeNativeModules(join(ELECTRON_STANDALONE_DIR, "node_modules"), ["keytar"]);
|
||||
removeNativeModules(join(ELECTRON_STANDALONE_DIR, NEXT_DIST_DIR, "node_modules"), [
|
||||
"better-sqlite3",
|
||||
|
||||
Reference in New Issue
Block a user