用 Codex 提交第一个 GitHub PR
在 Noi 开发中,会遇到各种问题,今天这个比较有趣就想特别记录一下。
问题描述:electron + better-sqlite3 因 node 版本不一致,构建时经常出现各种错误。node-gyp[1] 更是大坑,不少依赖构建都死在上面...
背景
Noi 的开发依赖一直是基于最新稳定版 npm 包构建,比如 electron 已经升级到 v39.2.0 了,今天刚推送的稳定版(Chromium v142.0.7444.162)。
Chromium 142 意味着什么?它可以让你无后顾之忧畅玩各种新特性,比如 corner-shape,仅在 chromium 系浏览器实现了。
用最新技术栈就意味着依赖不稳定风险会成倍增加,下面先介绍点背景知识。
构建错误
Electron 版本升级会导致 better-sqlite3 构建失败,本质原因在于它是原生扩展,直接绑定在 Node/V8 的二进制 ABI 上。Electron 并不是“套一层壳的系统 Node”,而是自带一套特制的 Node 和 V8,每次大版本升级都会更换 ABI。之前为旧版本 Electron 或系统 Node 编译出来的 .node 二进制文件,和新 ABI 不再兼容,必须通过 node-gyp 重新编译;一旦重编过程有任何问题,就会表现为你看到的那类 “node-gyp failed to rebuild better-sqlite3”。
better-sqlite3 自身有“支持哪些 Node 版本”的节奏,它会针对新的 Node 大版本发布对应的兼容版本(但已经好久没更新了)。如果项目中依然使用的是较老版本,而 Electron 升级后内置的 Node 已经跳到了更新的大版本,那么原来的 better-sqlite3 可能既没有预编译产物,又缺乏对新 API 或新 ABI 的支持,只能走源码构建,最终在编译阶段因为接口变化或宏定义不匹配而失败。因此,Electron 升级通常需要同步升级所有原生依赖到“官方声明支持该 Node 版本”的版本,否则构建风险极高。
原生模块的编译目标必须明确面向 Electron。很多项目在本地或 CI 中只是简单执行 npm install,这会用系统 Node 的头文件和 ABI 去编译 better-sqlite3,而运行时却由 Electron 自带的 Node 来加载。两者的 NODE_MODULE_VERSION 不一致时,Electron 会尝试触发重新编译,如果此时构建环境不完整(缺少 Python、编译器、Electron 头文件下载失败等),就会直接导致在 Electron Forge 流水线中看到“Preparing native dependencies 失败”的整串错误。
总结来说:Electron 升级相当于对所有原生依赖进行一次“强制体检”,之前在旧版本上勉强能工作的隐性问题——例如未锁定兼容版本、依赖系统 Node 编译结果、CI 上缺少 node-gyp 所需工具链——都会在这一次 ABI 变更中集中暴露出来。理解这条因果链条,有助于在升级 Electron 时同步规划:更新 better-sqlite3 到兼容版本,确保使用 electron-rebuild 之类工具针对 Electron 目标编译,并在本地与 CI 上准备好完整的原生构建环境;如果长期不想再被 node-gyp 牵制,则可以进一步评估迁移到内置的 node:sqlite 或 WASM 方案。
node-gyp 可以简单理解为:给 Node.js 原生扩展(C/C++ Addon)做“编译和构建”的官方工具链。
更具体一点:
- 很多 npm 包(比如
better-sqlite3、sqlite3、sharp等)内部是用 C/C++ 写的二进制模块,以.node文件形式提供给 Node 使用,这类模块需要针对 当前平台 + CPU 架构 + Node ABI 编译。 node-gyp就负责这一整套流程:
- 读取项目里的
binding.gyp(构建配置文件) - 调用 Python + GYP 生成对应平台的项目文件(如 Makefile / MSBuild 工程)
- 再调用系统上的编译器(gcc / clang / MSVC)把 C/C++ 源码编译成最终的
.node二进制。
由于它要“靠近底层”,所以对环境有明确要求:
- 需要安装 Python(通常是 3.x);
- 需要完整的 C/C++ 编译工具链(macOS 上是 Xcode 命令行工具,Windows 上是 Visual Studio Build Tools,Linux 上是 gcc 等);
- 还要能下载对应 runtime(Node/Electron)的头文件和库,用来对齐 ABI。
一旦这些条件有任何一项缺失或版本不匹配,就会出现你很熟悉的那种报错:node-gyp failed to rebuild ...。在普通 Node 项目中,这表现为“某个依赖装不上”;在 Electron 项目中,还会叠加 Electron 自带 Node 与系统 Node 的 ABI 差异,所以常常需要再配合 electron-rebuild 之类工具,专门为 Electron 目标重编所有原生模块。
从工程视角看,node-gyp 是 Node 原生生态的“基建”:它让 npm 包可以封装高性能的 C/C++ 逻辑,同时对调用方暴露的是标准 JS API。但也正因为涉及到底层工具链、ABI、平台差异,它也是很多前端 / Electron 开发者印象中“最容易踩坑”的一环,这也是为什么越来越多库开始提供预编译二进制、基于 N-API 降低重编次数,或者干脆用 WASM / 纯 JS 来绕开 node-gyp 依赖的原因。
Node 和 Electron 的 abi 映射关系可以通过 node-abi[2] 获取。
const nodeAbi = require('node-abi');
console.log(nodeAbi.getAbi('7.2.0', 'node')) // 51
console.log(nodeAbi.getTarget('51', 'node')) // 7.0.0
console.log(nodeAbi.getAbi('1.4.10', 'electron')) // 50
console.log(nodeAbi.getTarget('50', 'electron')) // 1.4.0
console.log(nodeAbi.getAbi('39.2.0', 'electron')) // 140
console.log(nodeAbi.getTarget('140', 'electron')) // 39.0.0
console.log(nodeAbi.getAbi('22.21.1', 'node')) // 127
console.log(nodeAbi.getTarget('127', 'node')) // 22.0.0
绕开 node-gyp 构建
如果简单使用,已内置的 node:sqlite 可直接调用(node.js 最低版本要求 v22.5.0)。
或者使用 wasm 类 sqlite 实现,主要有以下几种:
基础型 WASM SQLite
这些是“把 SQLite 本体编成 WASM”,你自己决定怎么做持久化、怎么包 API:
- sql.js[3]:最早、应用最广的 WASM SQLite 实现之一。核心思路是
SQLite + Emscripten → WASM,在内存里开一个 DB,文件要自己读写(Uint8Arraydump / restore)。需自己包一层胶水代码,控制数据落盘持久化存储。 - wa-sqlite[4]:更现代一点的 WASM SQLite,支持 Web Worker、多种持久化后端(OPFS、IndexedDB 等)。更强调可扩展性,可以在浏览器 / Electron / Cloudflare Workers 等环境使用。
- sqlite-wasm[5] (官方 wasm 版本):SQLite 官方提供的 wasm 端口,包含 VFS、虚拟文件系统等,可接浏览器存储 API。接近“官方标配”,但是封装相对偏底层一些,需要自己写胶水代码。
友好封装方案
在上面那层 WASM 之上包了一些“更好用的前端接口”和持久化策略:
- absurd-sql[6]:基于 sql.js / wasm,在浏览器里用 IndexedDB 模拟真正的 SQLite 文件系统。目标是“让 SQLite 在浏览器里更像桌面版 SQLite”,支持较完整的事务、分页等。
- ElectricSQL(@electric-sql/client[7]):整体是 “Postgres ↔ SQLite ↔ 浏览器”的同步框架,如果未来考虑“本地 SQLite + 云同步”,他们的一些设计值得参考。
- libsql[8]:通过 rust 构建的 wasm npm 包,兼容
better-sqlite3api,支持云同步。
Codex 发力
在 Electron v39 中使用 [email protected] 的报错信息如下:
Building module: better-sqlite3, Completed: 0
TOUCH ba23eeee118cd63e16015df367567cb043fed872.intermediate
ACTION deps_sqlite3_gyp_locate_sqlite3_target_copy_builtin_sqlite3 ba23eeee118cd63e16015df367567cb043fed872.intermediate
TOUCH Release/obj.target/deps/locate_sqlite3.stamp
CC(target) Release/obj.target/sqlite3/gen/sqlite3/sqlite3.o
LIBTOOL-STATIC Release/sqlite3.a
CXX(target) Release/obj.target/better_sqlite3/src/better_sqlite3.o
In file included from ../src/better_sqlite3.cpp:26:
../src/util/data.cpp:149:30: warning: 'GetPrototype' is deprecated: V8 will stop providing access to hidden prototype (i.e. JSGlobalObject). Use GetPrototypeV2() instead. See http://crbug.com/333672197. [-Wdeprecated-declarations]
149 | v8::Object::New(isolate)->GetPrototype(),
| ^
/Users/lencx/.electron-gyp/39.1.2/include/node/v8-object.h:445:3: note: 'GetPrototype' has been explicitly marked deprecated here
445 | V8_DEPRECATED(
| ^
/Users/lencx/.electron-gyp/39.1.2/include/node/v8config.h:612:35: note: expanded from macro 'V8_DEPRECATED'
612 | # define V8_DEPRECATED(message) [[deprecated(message)]]
| ^
In file included from ../src/better_sqlite3.cpp:28:
../src/util/row-builder.cpp:36:30: warning: 'GetPrototype' is deprecated: V8 will stop providing access to hidden prototype (i.e. JSGlobalObject). Use GetPrototypeV2() instead. See http://crbug.com/333672197. [-Wdeprecated-declarations]
36 | v8::Object::New(isolate)->GetPrototype(),
| ^
/Users/lencx/.electron-gyp/39.1.2/include/node/v8-object.h:445:3: note: 'GetPrototype' has been explicitly marked deprecated here
445 | V8_DEPRECATED(
| ^
/Users/lencx/.electron-gyp/39.1.2/include/node/v8config.h:612:35: note: expanded from macro 'V8_DEPRECATED'
612 | # define V8_DEPRECATED(message) [[deprecated(message)]]
| ^
In file included from ../src/better_sqlite3.cpp:41:
../src/util/binder.cpp:36:37: warning: 'GetPrototype' is deprecated: V8 will stop providing access to hidden prototype (i.e. JSGlobalObject). Use GetPrototypeV2() instead. See http://crbug.com/333672197. [-Wdeprecated-declarations]
36 | v8::Local<v8::Value> proto = obj->GetPrototype();
| ^
/Users/lencx/.electron-gyp/39.1.2/include/node/v8-object.h:445:3: note: 'GetPrototype' has been explicitly marked deprecated here
445 | V8_DEPRECATED(
| ^
/Users/lencx/.electron-gyp/39.1.2/include/node/v8config.h:612:35: note: expanded from macro 'V8_DEPRECATED'
612 | # define V8_DEPRECATED(message) [[deprecated(message)]]
| ^
In file included from ../src/better_sqlite3.cpp:41:
../src/util/binder.cpp:39:62: warning: 'GetPrototype' is deprecated: V8 will stop providing access to hidden prototype (i.e. JSGlobalObject). Use GetPrototypeV2() instead. See http://crbug.com/333672197. [-Wdeprecated-declarations]
39 | v8::Local<v8::Value> baseProto = v8::Object::New(isolate)->GetPrototype();
| ^
/Users/lencx/.electron-gyp/39.1.2/include/node/v8-object.h:445:3: note: 'GetPrototype' has been explicitly marked deprecated here
445 | V8_DEPRECATED(
| ^
/Users/lencx/.electron-gyp/39.1.2/include/node/v8config.h:612:35: note: expanded from macro 'V8_DEPRECATED'
612 | # define V8_DEPRECATED(message) [[deprecated(message)]]
| ^
In file included from ../src/better_sqlite3.cpp:44:
../src/objects/statement.cpp:366:31: warning: 'GetPrototype' is deprecated: V8 will stop providing access to hidden prototype (i.e. JSGlobalObject). Use GetPrototypeV2() instead. See http://crbug.com/333672197. [-Wdeprecated-declarations]
366 | v8::Object::New(isolate)->GetPrototype(),
| ^
/Users/lencx/.electron-gyp/39.1.2/include/node/v8-object.h:445:3: note: 'GetPrototype' has been explicitly marked deprecated here
445 | V8_DEPRECATED(
| ^
/Users/lencx/.electron-gyp/39.1.2/include/node/v8config.h:612:35: note: expanded from macro 'V8_DEPRECATED'
612 | # define V8_DEPRECATED(message) [[deprecated(message)]]
| ^
../src/better_sqlite3.cpp:48:1: warning: cast from 'void (*)(v8::Local<v8::Object>, v8::Local<v8::Value>, v8::Local<v8::Context>)' to 'node::addon_context_register_func' (aka 'void (*)(v8::Local<v8::Object>, v8::Local<v8::Value>, v8::Local<v8::Context>, void *)') converts to incompatible functiontype [-Wcast-function-type-mismatch]
48 | NODE_MODULE_INIT(/* exports, context */) {
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/Users/lencx/.electron-gyp/39.1.2/include/node/node.h:1320:3: note: expanded from macro 'NODE_MODULE_INIT'
1320 | NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, \
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1321 | NODE_MODULE_INITIALIZER) \
| ~~~~~~~~~~~~~~~~~~~~~~~~
/Users/lencx/.electron-gyp/39.1.2/include/node/node.h:1289:3: note: expanded from macro 'NODE_MODULE_CONTEXT_AWARE'
1289 | NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, NULL, 0)
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/Users/lencx/.electron-gyp/39.1.2/include/node/node.h:1271:7: note: expanded from macro 'NODE_MODULE_CONTEXT_AWARE_X'
1271 | (node::addon_context_register_func) (regfunc), \
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
../src/better_sqlite3.cpp:49:34: error: no member named 'GetIsolate'in'v8::Context'
49 | v8::Isolate* isolate = context->GetIsolate();
| ~~~~~~~~~^
6 warnings and 1 error generated.
make: *** [Release/obj.target/better_sqlite3/src/better_sqlite3.o] Error 1
rm ba23eeee118cd63e16015df367567cb043fed872.intermediate
Error: `make` failed with exit code: 2
at ChildProcess.<anonymous> (/Users/lencx/github/noi-workspace/noi-next/node_modules/node-gyp/lib/build.js:219:23)
✖ Rebuild Failed
An unhandled error occurred inside electron-rebuild
node-gyp failed to rebuild '/Users/lencx/github/noi-workspace/noi-next/node_modules/better-sqlite3'
Error: node-gyp failed to rebuild '/Users/lencx/github/noi-workspace/noi-next/node_modules/better-sqlite3'
at ChildProcess.<anonymous> (file:///Users/lencx/github/noi-workspace/noi-next/node_modules/@electron/rebuild/lib/module-type/node-gyp/node-gyp.js:114:24)
at ChildProcess.emit (node:events:519:28)
at ChildProcess._handle.onexit (node:internal/child_process:293:12)
error Command failed with exit code 255.
在了解关于依赖构建的背景知识后(node-gyp、electron、better-sqlite3 之间的关系),就可以让 codex 来 review better-sqlite3 源代码进行新版 electron 兼容修复。修复过程我截了两张图,核心 prompt 就是把问题原因准确表述给 codex,让其 review,经过几轮迭代重写,基本就解决问题了(验证修复出现报错,可以再次将报错信息完整贴给 codex 让其分析)。
如果你也在使用最新版 electron + better-sqlite3 构建应用,可以通过修改项目 package.json,来临时解决 build 错误:
{
"scripts": {
+ "postinstall": "electron-rebuild -f -w better-sqlite3",
"start": "electron-forge start"
},
"dependencies": {
+ "better-sqlite3": "github:lencx/better-sqlite3#a956ff3743ff922c900f71192480fabd6fd82a94"
}
}
大概解释一下上面的代码,github:lencx/better-sqlite3#a956ff3... 是让 npm 基于 lencx 的某次提交 hash 安装依赖,在安装依赖后会自动执行 postinstall 命令,进行 better-sqlite3 重构建。了解更多 lencx/better-sqlite3/pull[9]。
提 PR 修复问题,如果迟迟不合并代码也很麻烦。这时也可以通过 patch-package[10] 来快速 fix 项目中的第三方依赖。直接修改 node_modules/better-sqlite3 中的源码,然后执行 npx patch-package better-sqlite3 即可。
References
node-gyp:https://github.com/nodejs/node-gyp
[2]node-abi:https://github.com/electron/node-abi
[3]sql.js:https://github.com/sql-js/sql.js
[4]wa-sqlite:https://github.com/rhashimoto/wa-sqlite
[5]sqlite-wasm:https://github.com/sqlite/sqlite-wasm
[6]absurd-sql:https://github.com/jlongster/absurd-sql
[7]@electric-sql/client:https://github.com/electric-sql/electric/tree/main/packages/typescript-client
[8]libsql:https://github.com/tursodatabase/libsql-js
[9]lencx/better-sqlite3/pull:https://github.com/WiseLibs/better-sqlite3/pull/1418
[10]patch-package:https://www.npmjs.com/package/patch-package