fix(build): 修好 npm install 挂掉 (postinstall 只重建 better-sqlite3)
`npm install` 在 postinstall 阶段直接失败退出:
node-hid 编译报错 -> node-addon-api/napi.h:2477 expected unqualified-id
根因不是 node-hid 坏了, 是重建范围错了:
1. postinstall 跑的是 `electron-builder install-app-deps`, 它重建**所有**原生模块;
2. 它给子进程传 `npm_config_build_from_source=false` —— 但这是**字符串** "false",
npm 11 对未知的 npm_config_* 不做布尔转换;
3. pkg-prebuilds-verify 用 `if (process.env.npm_config_build_from_source)` 判断,
字符串 "false" 在 JS 里是真值 -> 直接 exit 1 -> 强制源码编译;
4. node-hid 于是去编译它自带的 node-addon-api 3.2.1, 和现代 Node 头文件不兼容 -> 编译失败
-> 整个 npm install 挂掉。
而 node-hid 根本不需要编译 —— 它自带 N-API 预编译产物, 任何 ABI 都能加载。
真正需要按 Electron ABI 重编的只有 better-sqlite3 (非 N-API, 绑死 V8/ABI,
Electron 32 -> node ABI 128); 其余 (@lydell/node-pty / node-hid /
@serialport/bindings-cpp / usb) 都是 N-API 预编译, 重编既没必要又有害。
改动:
- postinstall 换成 `node scripts/postinstall.js`:
只重建 better-sqlite3, 装完**在 Electron 下真跑一次读写校验**, 失败就 exit 1
—— 把"启动即崩"提前到 install 阶段暴露
- package.json 加 `allowScripts` 白名单 (npm 11 新机制, 目前只提醒不拦截,
但迟早会拦; 顺便让 npm install 输出干净)
- AGENTS.md 坑 #3 改写成新规矩 + 完整根因链
- docs/windows-compat.md / docs/remote-terminal-forward-usb.md 里过时的
"postinstall 走 install-app-deps" 表述一并纠正
验证:
- npm run build ✅
- npm run typecheck ✅ node + web
- npm run selftest ✅ 64 通过 / 0 失败
- npm run selftest:sync ✅ 全部通过
- npm run package:linux ✅ 新 deb
- npm run selftest:package ✅ 4/4 (从 app.asar 真实 spawn pty)
This commit is contained in:
@@ -99,7 +99,21 @@ UDP discovery on **47800**, chat WS on **47900**, file HTTP on **47901**. All th
|
||||
|
||||
2. **Drag-drop file path.** Electron 32 removed `File.path`. Use `webUtils.getPathForFile(file)` from `electron` — exposed in `src/preload/index.ts` as `getPathForFile(file)`. Callers must read it via `window.api.getPathForFile(f)` BEFORE the await boundary.
|
||||
|
||||
3. **Native rebuild.** `postinstall` runs `electron-builder install-app-deps` to rebuild `better-sqlite3` against the bundled Electron ABI. After `npm i` or upgrading Electron, this must run. If you forget, sqlite open will throw on launch.
|
||||
3. **Native rebuild —— 只重建 `better-sqlite3`, 别用 `electron-builder install-app-deps`.**
|
||||
`better-sqlite3` 不是 N-API, 绑死 V8/ABI (Electron 32 → node ABI 128), 必须对着 Electron
|
||||
头文件编译; `npm i` 或升级 Electron 后必须重编, 否则 sqlite 打开即崩。
|
||||
`postinstall` 现在是 `node scripts/postinstall.js`: 只重建 better-sqlite3, 装完**在 Electron 下
|
||||
真跑一次读写校验** (失败就 exit 1, 把"启动即崩"提前到 install 阶段暴露)。
|
||||
**别改回 `electron-builder install-app-deps`** —— 它重建**所有**原生模块, 且给子进程塞
|
||||
`npm_config_build_from_source=false`; npm 11 对未知 npm_config_* 保留原始字符串, 于是子进程里
|
||||
它是字符串 `"false"` —— JS 里是真值 —— `pkg-prebuilds-verify` 直接 exit 1
|
||||
(`node_modules/pkg-prebuilds/bin/verify.mjs:14`), 强制源码编译。node-hid 于是去编译它自带的
|
||||
node-addon-api 3.2.1, 和现代 Node 头文件不兼容, 编译失败, 整个 `npm install` 挂掉 ——
|
||||
而它本来根本不需要编译 (自带 N-API 预编译产物, 任何 ABI 都能加载)。
|
||||
同理, node-pty 1.2 的二进制在**平台包** `@lydell/node-pty-<plat>-<arch>` 里, 主包没有
|
||||
`prebuilds/` 目录 —— 按主包去找只会误报。
|
||||
npm 11 还有个 `allowScripts` 机制 (根 package.json 的 `allowScripts` 字段): 没被显式列入的
|
||||
安装脚本只会被"提醒"而非拦截, 但建议把信任的包列进白名单, 免得将来 npm 真的开始拦截。
|
||||
|
||||
4. **`mkdir` for Windows reserved names.** Output dir is `dist-Electron\` (escaped; `dist\electron` chokes on Windows). Don't rename without verifying the build still produces a valid path.
|
||||
|
||||
|
||||
@@ -550,7 +550,7 @@ CREATE INDEX idx_audit_ts ON audit_log(ts DESC);
|
||||
|---|---|
|
||||
| #1 `chat-client` 必带 `'error'` 监听 | 新增帧引入不影响;已有 on('error') 已稳 |
|
||||
| #2 drag-drop file path | 不涉及 |
|
||||
| #3 native rebuild | `@lydell/node-pty`、`usb`、`node-hid`、`serialport` 都是 native,必须走现有 `postinstall` 的 `electron-builder install-app-deps`;CI 加 `npm rebuild --runtime=electron` 兜底 |
|
||||
| #3 native rebuild | `@lydell/node-pty`、`usb`、`node-hid`、`serialport` 都是 N-API 预编译,**不需要**按 Electron 重编;只有 `better-sqlite3`(非 N-API)要编,由 `postinstall` 的 `scripts/postinstall.js` 单独重建并在 Electron 下自校验。**别用 `electron-builder install-app-deps`**(会把 N-API 模块也拖进源码编译,node-hid 编译失败,根因见 AGENTS.md 坑 #3)。CI 兜底直接 `npm ci`(会触发 postinstall → `scripts/postinstall.js`),或手动 `node scripts/postinstall.js` |
|
||||
| #4 dist-Electron 路径 | 不动 |
|
||||
| #5 nsis oneClick false | 不动 |
|
||||
| #6 dragenter/dragleave 深度计数 | 不涉及 |
|
||||
|
||||
@@ -35,11 +35,18 @@ LocalNetMsg 是跨平台应用 (Electron 32 / Node 20 / Chromium 128)。这份
|
||||
| `node-hid` | `prebuilds/HID-win32-x64/node-napi-v4.node` | N-API | 否 |
|
||||
| `@serialport/bindings-cpp` | `prebuilds/win32-x64/*.node` | N-API | 否 |
|
||||
| `usb` (node-usb 3) | `@node-usb/usb-win32-x64-msvc` | napi-rs | 否 |
|
||||
| `better-sqlite3` | 源码编译 | **非 N-API** | 是,靠 `postinstall` 的 `electron-builder install-app-deps` |
|
||||
| `better-sqlite3` | 源码编译 | **非 N-API** | 是,靠 `postinstall` 的 `scripts/postinstall.js` |
|
||||
|
||||
> **坑**:`better-sqlite3` 是唯一需要编译的。`npmRebuild: false` 只影响打包期,
|
||||
> `postinstall` 仍然会重编它。**如果用 `npm ci --ignore-scripts`,应用会启动即崩**
|
||||
> (sqlite 打开失败)。
|
||||
>
|
||||
> `postinstall` **只重建 better-sqlite3 这一个包**,不是 `electron-builder install-app-deps`。
|
||||
> 后者会重建所有原生模块,并给子进程传 `npm_config_build_from_source=false`;
|
||||
> npm 11 对未知的 `npm_config_*` 保留原始字符串,子进程里它是字符串 `"false"`(JS 真值),
|
||||
> `pkg-prebuilds-verify` 直接 exit 1 强制源码编译,node-hid 编译失败把整个
|
||||
> `npm install` 拖垮 —— 而它自带 N-API 预编译产物,本来根本不用编译。
|
||||
> 完整根因见 `AGENTS.md` 坑 #3。
|
||||
|
||||
node-pty 1.2 换成了"主包 + 平台包"拆包架构:主包只是 loader,
|
||||
真正的二进制在 `@lydell/node-pty-<platform>-<arch>` 里。**换平台时别忘了
|
||||
|
||||
+9
-1
@@ -6,6 +6,14 @@
|
||||
"homepage": "https://github.com/localnetmsg/local-net-msg",
|
||||
"author": "LocalNetMsg <dev@localnetmsg.local>",
|
||||
"main": "out/main/index.js",
|
||||
"allowScripts": {
|
||||
"better-sqlite3": true,
|
||||
"electron": true,
|
||||
"esbuild": true,
|
||||
"node-hid": true,
|
||||
"@serialport/bindings-cpp": true,
|
||||
"vue-demi": true
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "electron-vite dev",
|
||||
"build": "electron-vite build",
|
||||
@@ -23,7 +31,7 @@
|
||||
"package:mac": "cross-env ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ electron-vite build && electron-builder --mac",
|
||||
"package:linux": "cross-env ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ electron-vite build && electron-builder --linux",
|
||||
"package:dir": "cross-env ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ electron-vite build && electron-builder --dir",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
"postinstall": "node scripts/postinstall.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* postinstall —— 只重建"必须按 Electron ABI 重编"的原生模块, 其余一概不碰。
|
||||
*
|
||||
* 为什么不用 `electron-builder install-app-deps`:
|
||||
* 它会重建**所有**原生模块, 并给子进程带上 `npm_config_build_from_source=false`。
|
||||
* npm 11 对未知的 npm_config_* 保留原始字符串, 于是子进程里它是字符串 "false" ——
|
||||
* 在 JS 里是真值 —— pkg-prebuilds-verify 直接 exit 1
|
||||
* (node_modules/pkg-prebuilds/bin/verify.mjs:14), 强制走源码编译。
|
||||
* node-hid 于是去编译它自带的 node-addon-api 3.2.1, 和现代 Node 头文件不兼容, 编译失败,
|
||||
* 整个 `npm install` 挂掉 —— 而它本来根本不需要编译。
|
||||
*
|
||||
* 真正需要按 Electron ABI 重编的只有 better-sqlite3: 它不是 N-API, 绑死 V8/ABI
|
||||
* (Electron 32 → node ABI 128), 必须对着 Electron 头文件编译。
|
||||
* 其余原生模块 (@lydell/node-pty / node-hid / @serialport/bindings-cpp / usb)
|
||||
* 自带 N-API 预编译产物, 任何 ABI 都能加载 —— 重编既没必要, 又有害。
|
||||
*
|
||||
* 环境变量:
|
||||
* LNM_ELECTRON_DISTURL 覆盖 electron 头文件源 (默认走 npmmirror 镜像)
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const { execFileSync } = require('child_process')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
|
||||
const root = path.resolve(__dirname, '..')
|
||||
const log = (...a) => console.log('[postinstall]', ...a)
|
||||
|
||||
function readJSON(file) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function npmExec() {
|
||||
const cli = process.env.npm_execpath
|
||||
if (cli && fs.existsSync(cli)) {
|
||||
return { cmd: process.execPath, args: [cli] }
|
||||
}
|
||||
return { cmd: process.platform === 'win32' ? 'npm.cmd' : 'npm', args: [] }
|
||||
}
|
||||
|
||||
function electronBin() {
|
||||
const base = path.join(root, 'node_modules/electron/dist')
|
||||
const name = process.platform === 'win32' ? 'electron.exe' : 'electron'
|
||||
const p = path.join(base, name)
|
||||
return fs.existsSync(p) ? p : null
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- electron 版本
|
||||
const electronPkg = readJSON(path.join(root, 'node_modules/electron/package.json'))
|
||||
if (!electronPkg) {
|
||||
log('electron 未安装, 跳过原生模块重建')
|
||||
process.exit(0)
|
||||
}
|
||||
const electronVersion = electronPkg.version
|
||||
log(`electron ${electronVersion}`)
|
||||
|
||||
// ------------------------------------------------------- 只重建 better-sqlite3
|
||||
const disturl =
|
||||
process.env.LNM_ELECTRON_DISTURL ||
|
||||
process.env.ELECTRON_MIRROR ||
|
||||
'https://npmmirror.com/mirrors/electron'
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
npm_config_runtime: 'electron',
|
||||
npm_config_target: electronVersion,
|
||||
npm_config_arch: process.arch,
|
||||
npm_config_target_arch: process.arch,
|
||||
npm_config_platform: process.platform,
|
||||
npm_config_target_platform: process.platform,
|
||||
npm_config_disturl: disturl,
|
||||
npm_config_devdir: path.join(process.env.HOME || process.env.USERPROFILE || '', '.electron-gyp'),
|
||||
// better-sqlite3 必须真编译 (prebuild-install 的 electron 预编译产物不一定覆盖当前 ABI)。
|
||||
// 注意: 这个开关只作用于下面显式重建的 better-sqlite3 一个包, 不会波及 node-hid 等
|
||||
// N-API 模块 —— 那些包根本不进这条路径, 继续用自带预编译产物。
|
||||
npm_config_build_from_source: 'true',
|
||||
npm_config_update_binary: 'true',
|
||||
npm_config_fallback_to_build: 'true',
|
||||
}
|
||||
|
||||
const npm = npmExec()
|
||||
log('重建 better-sqlite3 (Electron ABI) ...')
|
||||
try {
|
||||
execFileSync(npm.cmd, [...npm.args, 'rebuild', 'better-sqlite3'], {
|
||||
cwd: root,
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('[postinstall] better-sqlite3 重建失败 —— 应用启动会因 sqlite 打不开而崩。')
|
||||
console.error('[postinstall] 手动修复: cd node_modules/better-sqlite3 && npx node-gyp rebuild --release')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- 装完就验一次
|
||||
const bin = electronBin()
|
||||
if (!bin) {
|
||||
log('找不到 electron 可执行文件, 跳过 sqlite 校验 (不影响开发)')
|
||||
} else {
|
||||
const probe = path.join(os.tmpdir(), `lnm-sqlite-probe-${process.pid}.js`)
|
||||
// 探针放在系统临时目录, 所以不能用裸 require —— 解析路径会以 /tmp 为基准,
|
||||
// 找不到项目里的 node_modules。这里写绝对路径。
|
||||
const sqlitePath = path.join(root, 'node_modules', 'better-sqlite3')
|
||||
fs.writeFileSync(
|
||||
probe,
|
||||
`const D=require(${JSON.stringify(sqlitePath)});const db=new D(':memory:');db.exec('create table t(a)');db.prepare('insert into t values (?)').run(1);if(db.prepare('select count(*) c from t').get().c!==1)throw new Error('sqlite write/read mismatch');console.log('sqlite-ok');`
|
||||
)
|
||||
try {
|
||||
const out = execFileSync(bin, [probe], {
|
||||
cwd: root,
|
||||
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
})
|
||||
if (!out.includes('sqlite-ok')) throw new Error(`unexpected output: ${out}`)
|
||||
log('better-sqlite3 在 Electron ABI 下验证通过')
|
||||
} catch (err) {
|
||||
console.error('[postinstall] better-sqlite3 在 Electron 下加载/读写失败:', err.message)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
fs.rmSync(probe, { force: true })
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------- N-API 模块: 确认预编译产物还在
|
||||
// node-pty 1.2 是"主包 + 平台包"拆包架构: 二进制在 @lydell/node-pty-<plat>-<arch> 里,
|
||||
// 主包本身没有 prebuilds/ 目录 —— 别按主包去找, 否则只会误报。
|
||||
const prebuilt = [
|
||||
['node-hid', 'prebuilds'],
|
||||
['@serialport/bindings-cpp', 'prebuilds'],
|
||||
]
|
||||
for (const [pkg, sub] of prebuilt) {
|
||||
const dir = path.join(root, 'node_modules', pkg, sub)
|
||||
if (!fs.existsSync(dir)) {
|
||||
log(`提醒: ${pkg} 缺少 ${sub}/ 预编译产物, 该功能可能不可用`)
|
||||
}
|
||||
}
|
||||
|
||||
// node-pty: 二进制在平台包里 (node-pty-<plat>-<arch>/prebuilds/<plat>-<arch>/pty.node)
|
||||
const ptyPlat = `@lydell/node-pty-${process.platform}-${process.arch}`
|
||||
const ptyBin = path.join(root, 'node_modules', ptyPlat, 'prebuilds', `${process.platform}-${process.arch}`)
|
||||
if (!fs.existsSync(ptyBin)) {
|
||||
log(`提醒: ${ptyPlat} 缺少 ${path.relative(root, ptyBin)}, 远程终端可能不可用`)
|
||||
}
|
||||
|
||||
log('done')
|
||||
Reference in New Issue
Block a user