Files
nexusterm/scripts/verify-package.mjs
T
2026-09-17 16:16:44 +08:00

121 lines
4.6 KiB
JavaScript

// Checks that a built package is actually runnable, rather than merely built.
//
// node scripts/verify-package.mjs [path/to/AppImage-or-unpacked-directory]
//
// Native modules cannot be loaded from inside an asar archive, so they have to
// be unpacked beside it — and when that goes wrong the app still builds, still
// installs, and still starts. It fails later, on a user's machine, the first
// time a terminal is opened. Nothing in `npm test` can catch that; this can.
//
// Runs the AppImage's own extractor rather than mounting it, so it works
// without FUSE — which is exactly the situation in most CI containers.
import { execFileSync } from 'node:child_process'
import { existsSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
const arch = process.arch
const EXPECTED_NATIVE =
process.platform === 'win32'
? [
`node_modules/better-sqlite3/prebuilds/win32-${arch}.node`,
`node_modules/node-pty/prebuilds/win32-${arch}/pty.node`,
`node_modules/node-pty/prebuilds/win32-${arch}/conpty.node`,
`node_modules/node-pty/prebuilds/win32-${arch}/winpty-agent.exe`,
]
: [
`node_modules/better-sqlite3/prebuilds/linux-${arch}.node`,
'node_modules/node-pty/build/Release/pty.node',
]
function fail(message) {
console.error(`\n ✗ ${message}`)
process.exitCode = 1
}
function findDefaultPackage() {
const dir = resolve('release')
if (!existsSync(dir)) return null
const unpacked = join(dir, 'win-unpacked')
if (process.platform === 'win32' && existsSync(unpacked)) return unpacked
const name = readdirSync(dir).find((f) => f.endsWith('.AppImage'))
return name ? join(dir, name) : null
}
function walk(dir, out = []) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name)
if (entry.isDirectory()) walk(full, out)
else out.push(full)
}
return out
}
const packagePath = process.argv[2] ? resolve(process.argv[2]) : findDefaultPackage()
if (!packagePath || !existsSync(packagePath)) {
fail('no unpacked package found — build one first with `npm run dist` or `npm run dist:win`')
process.exit(1)
}
console.log(` inspecting ${packagePath}`)
const workdir = mkdtempSync(join(tmpdir(), 'nexusterm-verify-'))
try {
// Either a packaged AppImage or an already-unpacked `--dir` build. The dir
// form matters because `electron-builder --dir` is what you iterate against —
// it skips the squashfs and compression steps, which dominate a full build.
let resources
if (statSync(packagePath).isDirectory()) {
resources = join(packagePath, 'resources')
} else {
// `--appimage-extract` writes ./squashfs-root relative to the working
// directory, so this has to run from inside the scratch dir.
execFileSync(packagePath, ['--appimage-extract'], {
cwd: workdir,
stdio: 'ignore',
timeout: 180_000,
})
resources = join(workdir, 'squashfs-root', 'resources')
}
const unpacked = join(resources, 'app.asar.unpacked')
if (!existsSync(unpacked)) {
fail('app.asar.unpacked is missing — every native module is trapped inside the archive')
} else {
const files = walk(unpacked)
for (const expected of EXPECTED_NATIVE) {
const found = files.some((f) => f.replaceAll('\\', '/').endsWith(expected))
if (found) console.log(` ✓ ${expected}`)
else fail(`${expected} is not unpacked — this module will fail to load at runtime`)
}
// ssh2's native crypto binding. Worth asserting separately because the
// library reads as pure JavaScript, and because a failed load falls back to
// a JS implementation silently — so nothing would ever report it.
const sshCrypto = files.filter((f) => f.endsWith('sshcrypto.node'))
if (sshCrypto.length > 0) console.log(` ✓ ssh2 sshcrypto.node (${sshCrypto.length})`)
else if (process.platform === 'win32') {
console.log(' ! ssh2 sshcrypto.node is absent; ssh2 will use its JavaScript fallback')
} else fail('ssh2 sshcrypto.node is missing')
const size = files.reduce((total, file) => total + statSync(file).size, 0)
console.log(` · unpacked: ${(size / 1024 / 1024).toFixed(1)} MB`)
const asar = join(resources, 'app.asar')
if (existsSync(asar)) {
console.log(` · app.asar: ${(statSync(asar).size / 1024 / 1024).toFixed(1)} MB`)
}
}
} finally {
rmSync(workdir, { recursive: true, force: true })
}
console.log(
process.exitCode
? '\n package verification FAILED\n'
: '\n package verification passed\n',
)