diff --git a/AGENTS.md b/AGENTS.md index 5a5f404..04b90a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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--` 里, 主包没有 + `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. diff --git a/docs/remote-terminal-forward-usb.md b/docs/remote-terminal-forward-usb.md index 78448db..ca8615c 100644 --- a/docs/remote-terminal-forward-usb.md +++ b/docs/remote-terminal-forward-usb.md @@ -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 深度计数 | 不涉及 | diff --git a/docs/windows-compat.md b/docs/windows-compat.md index ebe36b8..3f8c2e1 100644 --- a/docs/windows-compat.md +++ b/docs/windows-compat.md @@ -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--` 里。**换平台时别忘了 diff --git a/package.json b/package.json index 427bd8e..c02f1a5 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,14 @@ "homepage": "https://github.com/localnetmsg/local-net-msg", "author": "LocalNetMsg ", "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", diff --git a/scripts/postinstall.js b/scripts/postinstall.js new file mode 100644 index 0000000..5d6300e --- /dev/null +++ b/scripts/postinstall.js @@ -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-- 里, +// 主包本身没有 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--/prebuilds/-/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')