test(package): 加打包产物冒烟检查 (原生模块解包 + 从 app.asar 真实 spawn)
自测 (scripts/selftest/main.ts) 是从**源码**跑的, 用 pty 桩, 根本不碰 app.asar。 而 asarUnpack 配错这类问题只在**打包产物**上才暴露, 且 Linux 上看不出来 (只需要一个 .node, Electron 能 dlopen 归档里的; 但 unpacked 里没有就彻底找不到), 到 Windows 才炸 (conpty.dll / OpenConsole.exe / worker 脚本都必须是真实文件)。 新增 scripts/selftest/packaging-check.cjs (npm run selftest:package): - node-pty 平台包的 prebuilds 下必须有 .node/.dll/.exe (即规则递归生效) - 其它原生模块 (node-hid / serialport / node-usb) 也要解包 - 从 app.asar 里 require node-pty 并真实 spawn, 验证拿到输出 已用人为破坏的解包布局验证过这个检查确实会失败: "平台包的 prebuilds 下没有任何原生文件 —— 规则可能没递归" "Failed to load native module: pty.node"
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
"selftest:build": "esbuild scripts/selftest/main.ts --bundle --platform=node --format=cjs --target=node20 --outfile=scripts/selftest/.out/selftest.cjs --alias:electron=./scripts/selftest/electron-stub.cjs --alias:@lydell/node-pty=./scripts/selftest/pty-shim.cjs --external:better-sqlite3 --external:serialport --external:usb --external:node-hid --external:sudo-prompt --external:ws",
|
"selftest:build": "esbuild scripts/selftest/main.ts --bundle --platform=node --format=cjs --target=node20 --outfile=scripts/selftest/.out/selftest.cjs --alias:electron=./scripts/selftest/electron-stub.cjs --alias:@lydell/node-pty=./scripts/selftest/pty-shim.cjs --external:better-sqlite3 --external:serialport --external:usb --external:node-hid --external:sudo-prompt --external:ws",
|
||||||
"selftest": "npm run selftest:build && cross-env ELECTRON_RUN_AS_NODE=1 electron scripts/selftest/.out/selftest.cjs",
|
"selftest": "npm run selftest:build && cross-env ELECTRON_RUN_AS_NODE=1 electron scripts/selftest/.out/selftest.cjs",
|
||||||
|
"selftest:package": "cross-env ELECTRON_RUN_AS_NODE=1 electron scripts/selftest/packaging-check.cjs",
|
||||||
"package:win": "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 --win --x64",
|
"package:win": "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 --win --x64",
|
||||||
"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: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: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",
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// 打包产物冒烟检查: 验证原生模块真的被解包 + 能从 app.asar 里加载并 spawn
|
||||||
|
//
|
||||||
|
// 为什么需要单独一个脚本:
|
||||||
|
// 自测 (scripts/selftest/main.ts) 是从**源码**跑的, 用的是 pty 桩, 根本不碰 app.asar。
|
||||||
|
// 而 asarUnpack 配错这类问题 (比如 node-pty 平台包没递归解包) **只在打包产物上才暴露**,
|
||||||
|
// 而且在 Linux 上看不出来 (只需要一个 .node, Electron 能 dlopen 归档里的),
|
||||||
|
// 到 Windows 才炸 (conpty.dll / OpenConsole.exe / worker 脚本都必须是真实文件)。
|
||||||
|
//
|
||||||
|
// 用法:
|
||||||
|
// node scripts/selftest/packaging-check.cjs [打包输出目录]
|
||||||
|
// 默认 dist-Electron/linux-unpacked (Windows 上传 dist-Electron/win-unpacked)
|
||||||
|
//
|
||||||
|
// 必须用 Electron 的 node 跑 (app.asar 只有 Electron 认):
|
||||||
|
// cross-env ELECTRON_RUN_AS_NODE=1 electron scripts/selftest/packaging-check.cjs
|
||||||
|
const path = require('node:path')
|
||||||
|
const fs = require('node:fs')
|
||||||
|
|
||||||
|
const results = []
|
||||||
|
async function check(name, fn) {
|
||||||
|
try {
|
||||||
|
const detail = await fn()
|
||||||
|
results.push({ name, ok: true, detail: detail || '' })
|
||||||
|
console.log(` \x1b[32mPASS\x1b[0m ${name}${detail ? ' — ' + detail : ''}`)
|
||||||
|
} catch (e) {
|
||||||
|
results.push({ name, ok: false, detail: String(e?.message || e) })
|
||||||
|
console.log(` \x1b[31mFAIL\x1b[0m ${name} — ${e?.message || e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function assert(cond, msg) { if (!cond) throw new Error(msg) }
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const root = path.resolve(process.argv[2] || path.join(process.cwd(), 'dist-Electron', `${process.platform}-unpacked`))
|
||||||
|
const resources = path.join(root, 'resources')
|
||||||
|
const asar = path.join(resources, 'app.asar')
|
||||||
|
const unpacked = path.join(resources, 'app.asar.unpacked')
|
||||||
|
|
||||||
|
console.log(`\n\x1b[1m[打包产物检查]\x1b[0m ${root}\n`)
|
||||||
|
|
||||||
|
await check('打包产物存在', () => {
|
||||||
|
assert(fs.existsSync(asar), `找不到 ${asar}`)
|
||||||
|
return path.basename(asar)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 1) node-pty 平台包必须完整解包 (含深层 prebuilds/<plat>/)
|
||||||
|
await check('node-pty 平台包已完整解包 (含 prebuilds 下的 .node/.dll/.exe)', () => {
|
||||||
|
const pkgDir = path.join(unpacked, 'node_modules', '@lydell')
|
||||||
|
assert(fs.existsSync(pkgDir), `unpacked 里没有 @lydell: ${pkgDir}`)
|
||||||
|
const platformPkgs = fs.readdirSync(pkgDir).filter(n => n.startsWith('node-pty-'))
|
||||||
|
assert(platformPkgs.length > 0, `没有 node-pty 平台包: ${fs.readdirSync(pkgDir).join(',')}`)
|
||||||
|
const found = []
|
||||||
|
for (const p of platformPkgs) {
|
||||||
|
const pre = path.join(pkgDir, p, 'prebuilds')
|
||||||
|
if (!fs.existsSync(pre)) continue
|
||||||
|
const walk = (d) => {
|
||||||
|
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
||||||
|
const full = path.join(d, e.name)
|
||||||
|
if (e.isDirectory()) walk(full)
|
||||||
|
else if (/\.(node|dll|exe)$/i.test(e.name)) found.push(path.relative(pkgDir, full))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(pre)
|
||||||
|
}
|
||||||
|
assert(found.length > 0, '平台包的 prebuilds 下没有任何原生文件 —— 规则可能没递归')
|
||||||
|
return found.join(', ')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2) 其它原生模块也要解包 (Electron 的 smart unpack 不一定覆盖到)
|
||||||
|
await check('其它原生模块已解包 (node-hid / serialport / node-usb)', () => {
|
||||||
|
const nm = path.join(unpacked, 'node_modules')
|
||||||
|
assert(fs.existsSync(nm), 'unpacked 里没有 node_modules')
|
||||||
|
const found = []
|
||||||
|
const walk = (d, depth) => {
|
||||||
|
if (depth > 4) return
|
||||||
|
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
||||||
|
const full = path.join(d, e.name)
|
||||||
|
if (e.isDirectory()) walk(full, depth + 1)
|
||||||
|
else if (e.name.endsWith('.node')) found.push(path.relative(nm, full))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(nm, 0)
|
||||||
|
assert(found.length > 0, '没有任何 .node 被解包')
|
||||||
|
return `${found.length} 个原生文件`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3) 真正从 app.asar 里加载 node-pty 并 spawn —— 这才是终端能不能用的判据
|
||||||
|
// 注意: 必须异步等, 用同步忙等会把事件循环堵死, pty 的 data 回调永远不触发
|
||||||
|
await check('从 app.asar 加载 node-pty 并真实 spawn 成功', async () => {
|
||||||
|
const pty = require(path.join(asar, 'node_modules', '@lydell', 'node-pty'))
|
||||||
|
const isWin = process.platform === 'win32'
|
||||||
|
const shell = isWin ? 'cmd.exe' : '/bin/sh'
|
||||||
|
const args = isWin ? ['/c', 'echo LNM_PKG_OK'] : ['-c', 'echo LNM_PKG_OK']
|
||||||
|
let out = ''
|
||||||
|
const p = pty.spawn(shell, args, {
|
||||||
|
cols: 80, rows: 24, name: 'xterm-256color', encoding: null,
|
||||||
|
cwd: isWin ? process.env.USERPROFILE : process.env.HOME || process.cwd(),
|
||||||
|
})
|
||||||
|
p.onData(d => { out += Buffer.isBuffer(d) ? d.toString('utf8') : String(d) })
|
||||||
|
const t0 = Date.now()
|
||||||
|
while (!out.includes('LNM_PKG_OK') && Date.now() - t0 < 8000) {
|
||||||
|
await new Promise(r => setTimeout(r, 50))
|
||||||
|
}
|
||||||
|
assert(out.includes('LNM_PKG_OK'), `8s 内没拿到输出: ${JSON.stringify(out.slice(0, 200))}`)
|
||||||
|
return out.trim().split('\n')[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
const pass = results.filter(r => r.ok).length
|
||||||
|
const fail = results.length - pass
|
||||||
|
console.log(`\n\x1b[1m结果: ${pass} 通过 / ${fail} 失败\x1b[0m`)
|
||||||
|
if (fail) {
|
||||||
|
console.log('\n失败项:')
|
||||||
|
for (const r of results.filter(x => !x.ok)) console.log(` - ${r.name}\n ${r.detail}`)
|
||||||
|
}
|
||||||
|
process.exit(fail ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(e => { console.error(e); process.exit(2) })
|
||||||
Reference in New Issue
Block a user