121 lines
4.6 KiB
JavaScript
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',
|
|
)
|