mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Adds bin/cli/runtime/sqliteRuntime.mjs that resolves better-sqlite3 from: (1) bundled optionalDependency, (2) ~/.omniroute/runtime/ install, (3) lazy npm install into runtime dir, (4) node:sqlite stdlib (Node >=22.5), (5) bundled sql.js WASM. Each native binary is validated against expected platform magic bytes (ELF/Mach-O/PE) before load. Adds bin/cli/runtime/magicBytes.mjs with validateBinaryMagic() helper (9 tests). Adds bin/cli/runtime/index.mjs as warmUpRuntimes() orchestrator. Adds scripts/postinstall.mjs warm-up hook (non-fatal, skipped in CI). Integrates it as the last step of scripts/build/postinstall.mjs. Extends src/lib/db/core.ts with ensureDbInitialized() (async, idempotent) and getDriverInfo() so the startup orchestrator can await the resolver before any DB access, enabling graceful degradation without crashing the process on missing better-sqlite3. Solves Windows EBUSY error on 'npm install -g omniroute@latest' while the previous version is still running, and works in environments without C++ build tools or with unreachable npm registry. Documents OMNIROUTE_SKIP_POSTINSTALL in .env.example and ENVIRONMENT.md. Ref: 9router/cli/hooks/sqliteRuntime.js (pattern origin).
325 lines
11 KiB
JavaScript
325 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* OmniRoute — Postinstall Native Module Fix
|
|
*
|
|
* The npm package ships with a Next.js standalone build that includes
|
|
* native modules compiled for the build platform (Linux x64) inside
|
|
* app/node_modules/. However, npm also installs these as top-level
|
|
* dependencies (in the root node_modules/), correctly compiled for
|
|
* the user's platform.
|
|
*
|
|
* This script copies the correctly-built native binaries from the root
|
|
* into the standalone app directory — no rebuild or build tools needed.
|
|
*
|
|
* Modules repaired:
|
|
* - better-sqlite3 (SQLite bindings)
|
|
* - wreq-js (TLS client for OAuth providers)
|
|
*
|
|
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/129
|
|
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/321
|
|
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/426
|
|
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/1634
|
|
*/
|
|
|
|
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { PUBLISHED_BUILD_ARCH, PUBLISHED_BUILD_PLATFORM } from "./native-binary-compat.mjs";
|
|
import { hasStandaloneAppBundle } from "./postinstallSupport.mjs";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
const ROOT = join(__dirname, "..", "..");
|
|
|
|
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"
|
|
);
|
|
|
|
async function fixBetterSqliteBinary() {
|
|
if (!existsSync(join(ROOT, "app", "node_modules", "better-sqlite3"))) {
|
|
return;
|
|
}
|
|
|
|
const platformMatch =
|
|
process.platform === PUBLISHED_BUILD_PLATFORM && process.arch === PUBLISHED_BUILD_ARCH;
|
|
|
|
if (platformMatch) {
|
|
try {
|
|
process.dlopen({ exports: {} }, appBinary);
|
|
return;
|
|
} catch (err) {
|
|
console.warn(` ⚠️ Bundled binary incompatible despite platform match: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
console.log(`\n 🔧 Fixing better-sqlite3 binary for ${process.platform}-${process.arch}...`);
|
|
|
|
if (existsSync(rootBinary)) {
|
|
try {
|
|
mkdirSync(dirname(appBinary), { recursive: true });
|
|
copyFileSync(rootBinary, appBinary);
|
|
} catch (err) {
|
|
console.warn(` ⚠️ Failed to copy binary: ${err.message}`);
|
|
}
|
|
|
|
try {
|
|
process.dlopen({ exports: {} }, appBinary);
|
|
console.log(" ✅ Native module fixed successfully!\n");
|
|
return;
|
|
} catch (err) {
|
|
console.warn(` ⚠️ Copied binary failed to load: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
console.log(" 📥 Attempting to download prebuilt binary via node-pre-gyp...");
|
|
try {
|
|
const { execSync } = await import("node:child_process");
|
|
const preGypBin = join(
|
|
ROOT,
|
|
"app",
|
|
"node_modules",
|
|
".bin",
|
|
process.platform === "win32" ? "node-pre-gyp.cmd" : "node-pre-gyp"
|
|
);
|
|
const preGypFallback = join(
|
|
ROOT,
|
|
"app",
|
|
"node_modules",
|
|
"@mapbox",
|
|
"node-pre-gyp",
|
|
"bin",
|
|
"node-pre-gyp"
|
|
);
|
|
const preGypCmd = existsSync(preGypBin) ? preGypBin : preGypFallback;
|
|
|
|
if (existsSync(preGypCmd)) {
|
|
execSync(`"${process.execPath}" "${preGypCmd}" install --fallback-to-build=false`, {
|
|
cwd: join(ROOT, "app", "node_modules", "better-sqlite3"),
|
|
stdio: "inherit",
|
|
timeout: 60_000,
|
|
});
|
|
mkdirSync(dirname(appBinary), { recursive: true });
|
|
|
|
try {
|
|
process.dlopen({ exports: {} }, appBinary);
|
|
console.log(" ✅ Prebuilt binary downloaded and loaded successfully!\n");
|
|
return;
|
|
} catch (loadErr) {
|
|
console.warn(` ⚠️ Downloaded binary failed to load: ${loadErr.message}`);
|
|
}
|
|
} else {
|
|
console.warn(" ⚠️ node-pre-gyp not found, skipping prebuilt download.");
|
|
}
|
|
} catch (err) {
|
|
console.warn(` ⚠️ node-pre-gyp download failed: ${err.message.split("\n")[0]}`);
|
|
}
|
|
|
|
console.log(" ⚠️ Attempting npm rebuild (requires build tools)...");
|
|
|
|
try {
|
|
const { execSync } = await import("node:child_process");
|
|
|
|
// On Android/Termux, rebuild from source with --build-from-source flag
|
|
const isAndroid = process.platform === "android";
|
|
const rebuildCmd = isAndroid
|
|
? "npm install better-sqlite3 --build-from-source --force"
|
|
: "npm rebuild better-sqlite3";
|
|
|
|
execSync(rebuildCmd, {
|
|
cwd: join(ROOT, "app"),
|
|
stdio: "inherit",
|
|
timeout: 300_000, // 5 minutes for source builds
|
|
});
|
|
|
|
process.dlopen({ exports: {} }, appBinary);
|
|
console.log(" ✅ Native module rebuilt successfully!\n");
|
|
return;
|
|
} catch (err) {
|
|
const isTimeout = err.killed || err.signal === "SIGTERM";
|
|
if (isTimeout) {
|
|
console.warn(" ⚠️ npm rebuild timed out after 300s.");
|
|
} else {
|
|
console.warn(` ⚠️ npm rebuild failed: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
console.warn("\n ⚠️ Could not fix better-sqlite3 native module automatically.");
|
|
console.warn(" The server may not start correctly.");
|
|
console.warn(" Manual fix options:");
|
|
if (process.platform === "win32") {
|
|
console.warn(" Option A (easiest — no build tools needed):");
|
|
console.warn(` cd "${join(ROOT, "app", "node_modules", "better-sqlite3")}"`);
|
|
console.warn(" npx @mapbox/node-pre-gyp install --fallback-to-build=false");
|
|
console.warn(" Option B (requires Build Tools for Visual Studio):");
|
|
console.warn(` cd "${join(ROOT, "app")}" && npm rebuild better-sqlite3`);
|
|
console.warn(" Install from: https://visualstudio.microsoft.com/visual-cpp-build-tools/");
|
|
console.warn(" Also ensure Python is installed: https://python.org");
|
|
} else if (process.platform === "darwin") {
|
|
console.warn(` cd ${join(ROOT, "app")} && npm rebuild better-sqlite3`);
|
|
console.warn(" If build tools are missing: xcode-select --install");
|
|
} else {
|
|
console.warn(` cd ${join(ROOT, "app")} && npm rebuild better-sqlite3`);
|
|
}
|
|
console.warn("");
|
|
}
|
|
|
|
/**
|
|
* Fix wreq-js native binary for the standalone app directory.
|
|
*
|
|
* wreq-js ships platform-specific .node binaries under rust/.
|
|
* The standalone build may only contain Linux binaries from the CI.
|
|
* This copies the correct platform binary from the root install.
|
|
*
|
|
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/1634
|
|
*/
|
|
async function fixWreqJsBinary() {
|
|
const appWreqDir = join(ROOT, "app", "node_modules", "wreq-js", "rust");
|
|
const rootWreqDir = join(ROOT, "node_modules", "wreq-js", "rust");
|
|
|
|
if (!existsSync(join(ROOT, "app", "node_modules", "wreq-js"))) {
|
|
return;
|
|
}
|
|
|
|
const binaryName = `wreq-js.${process.platform}-${process.arch}.node`;
|
|
const appBinaryPath = join(appWreqDir, binaryName);
|
|
const rootBinaryPath = join(rootWreqDir, binaryName);
|
|
|
|
// Check if the platform binary already exists and loads
|
|
if (existsSync(appBinaryPath)) {
|
|
try {
|
|
process.dlopen({ exports: {} }, appBinaryPath);
|
|
return; // Already working
|
|
} catch (err) {
|
|
console.warn(` ⚠️ wreq-js binary exists but failed to load: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
console.log(`\n 🔧 Fixing wreq-js binary for ${process.platform}-${process.arch}...`);
|
|
|
|
// Strategy 1: Copy from root node_modules
|
|
if (existsSync(rootBinaryPath)) {
|
|
try {
|
|
mkdirSync(appWreqDir, { recursive: true });
|
|
copyFileSync(rootBinaryPath, appBinaryPath);
|
|
process.dlopen({ exports: {} }, appBinaryPath);
|
|
console.log(" ✅ wreq-js native module fixed successfully!\n");
|
|
return;
|
|
} catch (err) {
|
|
console.warn(` ⚠️ Copied wreq-js binary failed to load: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Strategy 2: Copy entire rust/ directory from root (gets all platform binaries)
|
|
if (existsSync(rootWreqDir)) {
|
|
try {
|
|
mkdirSync(appWreqDir, { recursive: true });
|
|
const files = readdirSync(rootWreqDir);
|
|
for (const file of files) {
|
|
if (file.endsWith(".node")) {
|
|
copyFileSync(join(rootWreqDir, file), join(appWreqDir, file));
|
|
}
|
|
}
|
|
if (existsSync(appBinaryPath)) {
|
|
process.dlopen({ exports: {} }, appBinaryPath);
|
|
console.log(" ✅ wreq-js native module fixed (full copy) successfully!\n");
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
console.warn(` ⚠️ wreq-js full copy failed: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Strategy 3: Rebuild wreq-js inside app/
|
|
console.log(" 📥 Attempting npm rebuild wreq-js...");
|
|
try {
|
|
const { execSync } = await import("node:child_process");
|
|
execSync("npm rebuild wreq-js", {
|
|
cwd: join(ROOT, "app"),
|
|
stdio: "inherit",
|
|
timeout: 120_000,
|
|
});
|
|
if (existsSync(appBinaryPath)) {
|
|
process.dlopen({ exports: {} }, appBinaryPath);
|
|
console.log(" ✅ wreq-js native module rebuilt successfully!\n");
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
console.warn(` ⚠️ wreq-js rebuild failed: ${err.message}`);
|
|
}
|
|
|
|
console.warn(
|
|
`\n ⚠️ Could not fix wreq-js native module for ${process.platform}-${process.arch}.`
|
|
);
|
|
console.warn(" OAuth-based providers (Codex, Cursor, etc.) may not work.");
|
|
console.warn(` Manual fix: cd ${join(ROOT, "app")} && npm install wreq-js --no-save\n`);
|
|
}
|
|
|
|
async function ensureSwcHelpers() {
|
|
if (!hasStandaloneAppBundle(ROOT)) {
|
|
return;
|
|
}
|
|
|
|
const swcHelpersApp = join(ROOT, "app", "node_modules", "@swc", "helpers");
|
|
const swcHelpersRoot = join(ROOT, "node_modules", "@swc", "helpers");
|
|
|
|
if (existsSync(swcHelpersApp)) {
|
|
return;
|
|
}
|
|
|
|
if (existsSync(swcHelpersRoot)) {
|
|
try {
|
|
const { cpSync } = await import("node:fs");
|
|
mkdirSync(join(ROOT, "app", "node_modules", "@swc"), { recursive: true });
|
|
cpSync(swcHelpersRoot, swcHelpersApp, { recursive: true });
|
|
console.log(" ✅ @swc/helpers copied to standalone app/node_modules.\n");
|
|
} catch (err) {
|
|
console.warn(` ⚠️ Could not copy @swc/helpers: ${err.message}`);
|
|
console.warn(
|
|
" Try manually: cp -r node_modules/@swc/helpers app/node_modules/@swc/helpers\n"
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
console.warn(" ⚠️ @swc/helpers not found in root node_modules either.");
|
|
console.warn(" Try: npm install --save-exact @swc/helpers@0.5.19\n");
|
|
}
|
|
|
|
async function syncProjectEnv() {
|
|
try {
|
|
const { syncEnv } = await import("./sync-env.mjs");
|
|
syncEnv({ rootDir: ROOT });
|
|
} catch (err) {
|
|
console.warn(` ⚠️ .env sync skipped: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
await fixBetterSqliteBinary();
|
|
await fixWreqJsBinary();
|
|
await ensureSwcHelpers();
|
|
await syncProjectEnv();
|
|
|
|
// Warm up native runtimes (better-sqlite3 in ~/.omniroute/runtime/).
|
|
// Non-fatal: errors are caught inside postinstall.mjs.
|
|
try {
|
|
await import("../postinstall.mjs");
|
|
} catch {
|
|
// Silently skip — runtime warm-up is best-effort.
|
|
}
|