#!/usr/bin/env node // 重编 better-sqlite3 (项目唯一的运行时 native module) 匹配当前 Electron ABI。 // // 调用方: // - scripts/start.cjs 在检测到 .node mtime 早于 electron.exe 时自动调 // - 用户手动跑 `npm run rebuild:native` // // 关键: // - ELECTRON_RUN_AS_NODE=1 让 child process 的 require('electron') 返回 // electron.exe 路径,而不是 electron 模块本身 —— 这样 electron-rebuild // 才能正确读取 Electron 内嵌的 Node 版本并匹配 NODE_MODULE_VERSION // - stdio: 'inherit' 让 electron-rebuild 的进度输出原样透传 // - 不捕获 stdout,失败时直接非零退出码交给 caller const { spawnSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const projectRoot = path.resolve(__dirname, '..'); const electronRebuildCli = path.resolve( projectRoot, 'node_modules', '@electron', 'rebuild', 'lib', 'cli.js', ); // node-gyp 失败时会把 build/Release/ 整个删掉 —— 包括原来那个**能用**的 .node。 // 在没有 MSVC 工具链、或 GitHub prebuild 下载超时的机器上(离线 / 网络受限), // 这会把一个本来能跑的仓库变成跑不起来的仓库;更糟的是 start.cjs 检测到 .node // 缺失会再次触发 rebuild,于是每次 npm run dev 都重复这个破坏。 // 所以:rebuild 前快照,失败就还原。 const nodeFile = path.join( projectRoot, 'node_modules', 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node', ); const backupDir = path.join(projectRoot, 'node_modules', '.cache', 'rkms'); const backupFile = path.join(backupDir, 'better_sqlite3.node.bak'); // --no-snapshot:调用方已经知道当前 .node 是坏的(缺失 / ABI 不匹配),别把它 // 当成"上一个可用版本"存进快照 —— 那会把之前真正可用的快照覆盖掉,把安全网变成陷阱。 // start.cjs 的自动 rebuild 永远走这个分支(它只在探测失败时才调用本脚本); // 人手跑 `npm run rebuild:native` 时默认做快照,这样 rebuild 失败还能退回去。 const noSnapshot = process.argv.includes('--no-snapshot'); /** * 当前 build/Release/.node 是否真的能在 Electron 里 dlopen。 * * 只用 existsSync 判断"值得快照"是不够的 —— 本仓库的 .node 在两个 ABI 之间来回换: * `npm run test:run` 的 pretest 会把它换成系统 Node ABI(137),`npm run dev` 需要的 * 是 Electron ABI(123)。文件一直存在,但对 Electron 而言可能是坏的。若把这种 * 文件存成"上一个可用版本",rebuild 失败后还原回去,安全网就变成了陷阱: * 真正可用的旧快照被覆盖,而且日志还会谎称"已还原可用的 .node"。 * * 探测手段与 start.cjs 的 probeBetterSqliteInElectron 一致(ELECTRON_RUN_AS_NODE=1 * 跑 Electron 内嵌 Node,ABI 与主进程完全一致)。探测不了就保守返回 false —— 宁可 * 不快照,也不要存一个来路不明的二进制。 */ function existingBindingLoadsInElectron() { let electronExePath; try { electronExePath = require('electron'); } catch { return false; } if (typeof electronExePath !== 'string' || !fs.existsSync(electronExePath)) { return false; } const probe = [ "try {", " const Db = require('better-sqlite3');", " const db = new Db(':memory:');", " db.close();", " process.exit(0);", "} catch (e) {", " process.exit(1);", "}", ].join('\n'); try { const r = spawnSync(electronExePath, ['--no-deprecation', '-e', probe], { cwd: projectRoot, env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, encoding: 'utf8', timeout: 30000, }); return r.status === 0; } catch { return false; } } if (!noSnapshot && fs.existsSync(nodeFile)) { if (existingBindingLoadsInElectron()) { try { fs.mkdirSync(backupDir, { recursive: true }); fs.copyFileSync(nodeFile, backupFile); console.log(`[rebuild-native] Snapshotted existing binding -> ${backupFile}`); } catch (e) { console.warn('[rebuild-native] Snapshot failed (continuing):', e.message); } } else { console.log( '[rebuild-native] 跳过快照:现有 .node 在 Electron 里加载失败(ABI 不匹配或已损坏),', ); console.log('[rebuild-native] 存它只会覆盖掉真正可用的旧快照。'); } } function restoreBackup() { // tri-state: 'restored' 成功还原,'no-backup' 快照不存在,'kept-existing' // rebuild 失败但原地留了个文件 —— 这是最危险的中间态(node-gyp 异常退出 // 时可能留个 0 字节 .node),以前被 if/else 漏掉,用户看不到任何提示。 if (!fs.existsSync(backupFile)) return 'no-backup'; if (fs.existsSync(nodeFile)) return 'kept-existing'; try { fs.mkdirSync(path.dirname(nodeFile), { recursive: true }); fs.copyFileSync(backupFile, nodeFile); return 'restored'; } catch (e) { console.error('[rebuild-native] Restore failed:', e.message); return 'kept-existing'; } } console.log('[rebuild-native] Rebuilding better-sqlite3 against Electron ABI...'); console.log('[rebuild-native] electron-rebuild CLI:', electronRebuildCli); const result = spawnSync( process.execPath, [ electronRebuildCli, '-f', // force:即使 mtime 一致也重新编译 '-w', 'better-sqlite3', // 只重编这一个模块(避免动到其它 native module) '--module-dir', projectRoot, '--only', // 不读 package.json 里的所有 deps,只处理 -w 指定的 ], { stdio: 'inherit', env: { ...process.env, // 关键:让 spawn 出来的 node 进程"扮演"成 electron 内嵌 node, // 这样 require('electron') 仍能拿到 electron 二进制路径供 electron-rebuild 推断 ABI ELECTRON_RUN_AS_NODE: '1', }, }, ); if (result.error) { console.error('[rebuild-native] spawn failed:', result.error.message); if (restoreBackup() === 'restored') { console.error('[rebuild-native] 已还原之前可用的 better_sqlite3.node,仓库仍可运行。'); } process.exit(1); } const status = result.status ?? 1; if (status !== 0) { // 失败诊断:这两个是实战里最常见的原因,直接点名比让用户去读 node-gyp 堆栈快得多。 console.error(''); console.error('====================================================='); console.error('[rebuild-native] Rebuild 失败。常见原因:'); console.error(' 1) 没有 C++ 工具链 —— node-gyp 报 "Could not find any Visual Studio installation"。'); console.error(' 装 VS Build Tools(勾选 "Desktop development with C++"):'); console.error(' winget install Microsoft.VisualStudio.2022.BuildTools'); console.error(' 2) prebuild 下载超时 —— better-sqlite3 的预编译包在 GitHub Releases 上,'); console.error(' 网络受限时会 timeout。配好代理后重试:'); console.error(' set HTTPS_PROXY=http://127.0.0.1:'); console.error(' npm run rebuild:native'); console.error('====================================================='); const restoreOutcome = restoreBackup(); if (restoreOutcome === 'restored') { console.error(''); console.error('[rebuild-native] 已还原 rebuild 之前那个可用的 better_sqlite3.node。'); console.error('[rebuild-native] 仓库仍可 npm run dev —— 只要 Electron 版本没变,ABI 依旧匹配。'); } else if (restoreOutcome === 'no-backup') { console.error(''); console.error('[rebuild-native] 警告:build/Release/better_sqlite3.node 现在不存在,'); console.error('[rebuild-native] 且没有可还原的快照。修好上面的原因后再跑一次。'); } else if (restoreOutcome === 'kept-existing') { console.error(''); console.error('[rebuild-native] 警告:rebuild 失败,但原地还留着一个 better_sqlite3.node。'); let size = -1; try { size = fs.statSync(nodeFile).size; } catch {} console.error( `[rebuild-native] 该文件大小 = ${size} 字节 —— 很可能是 node-gyp 异常退出时留下的残骸。`, ); console.error('[rebuild-native] 手动删掉后再重跑:`rm build/Release/better_sqlite3.node`'); console.error( '[rebuild-native] 不删的话下次探测照样不过,start.cjs 会反复触发 rebuild 死循环。', ); } } process.exit(status);