Files
bare-operating-system/scripts/verify-standalone-build.cjs
T
Raven Scott 0fcca9021d
Release rolling / release (push) Successful in 9m57s
Updates
2026-08-13 10:28:15 -04:00

244 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Post-build checks for bare-os seeder/booter standalones (CI + local).
*
* Verifies each out/bare-os-{product}-{host}/ binary exists, records size,
* rejects oversized Booter/Seeder builds (CodeRange OOM risk), and smoke-runs
* the native-host booter when present.
*
* Usage:
* node scripts/verify-standalone-build.cjs
* BARE_OS_HOSTS=linux-x64,darwin-arm64 node scripts/verify-standalone-build.cjs
*/
'use strict'
const path = require('path')
const fs = require('fs')
const { spawnSync } = require('child_process')
const { ALL_64, parseHostList, hostSupportsQvacNative } = require('./hosts.cjs')
const root = path.resolve(__dirname, '..')
const hosts = parseHostList(process.env.BARE_OS_HOSTS, ALL_64)
const nativeHost = `${process.platform}-${process.arch}`
/**
* Soft ceilings (bytes). CI fails above these — keeps pear-runtime / dup natives out.
* Booter hosts with in-process QVAC (`@qvac/llm-llamacpp`) are much larger (~250300+ MiB);
* win32-arm64 stubs QVAC and stays on the lean ceiling.
*/
const MAX_BYTES = {
seeder: {
'darwin-arm64': 110 * 1024 * 1024,
'darwin-x64': 120 * 1024 * 1024,
'linux-x64': 145 * 1024 * 1024,
'linux-arm64': 145 * 1024 * 1024,
'win32-x64': 150 * 1024 * 1024,
'win32-arm64': 150 * 1024 * 1024,
},
booter: {
'darwin-arm64': 110 * 1024 * 1024,
'darwin-x64': 120 * 1024 * 1024,
'linux-x64': 145 * 1024 * 1024,
'linux-arm64': 145 * 1024 * 1024,
'win32-x64': 150 * 1024 * 1024,
'win32-arm64': 150 * 1024 * 1024,
},
}
/** Ceiling when QVAC natives are packed into the booter (~294 MiB seen on linux-x64 CI). */
const MAX_BYTES_BOOTER_WITH_QVAC = 360 * 1024 * 1024
/**
* @param {string} product
* @param {string} host
* @param {string} file
*/
function maxBytesFor(product, host, file) {
const lean = MAX_BYTES[product]?.[host] || 160 * 1024 * 1024
if (product !== 'booter') return lean
const infoPath = path.join(path.dirname(file), 'build-info.json')
let stubbed = null
try {
const info = JSON.parse(fs.readFileSync(infoPath, 'utf8'))
if (info.skipQvac === true || info.qvacStubbed === true) stubbed = true
else if (info.skipQvac === false || info.qvacStubbed === false) stubbed = false
} catch {
/* fall through */
}
if (stubbed === true) return lean
if (stubbed === false || hostSupportsQvacNative(host)) {
return Math.max(lean, MAX_BYTES_BOOTER_WITH_QVAC)
}
return lean
}
function binName(product, host) {
const name = `bare-os-${product}`
return host.startsWith('win32') ? `${name}.exe` : name
}
function findBinary(product, host) {
const dir = path.join(root, 'out', `bare-os-${product}-${host}`)
const preferred = path.join(dir, binName(product, host))
if (fs.existsSync(preferred)) return preferred
if (!fs.existsSync(dir)) return null
const walk = (d) => {
for (const ent of fs.readdirSync(d, { withFileTypes: true })) {
const p = path.join(d, ent.name)
if (ent.isDirectory()) {
const f = walk(p)
if (f) return f
} else if (ent.name === binName(product, host) || ent.name === `bare-os-${product}`) {
return p
}
}
return null
}
return walk(dir)
}
function assertSize(product, host, file) {
const st = fs.statSync(file)
const mb = (st.size / 1024 / 1024).toFixed(1)
const max = maxBytesFor(product, host, file)
console.log(`[verify] ${product}/${host}: ${mb} MiB (${file})`)
if (st.size > max) {
throw new Error(
`${product}/${host} binary is ${mb} MiB — exceeds ${(max / 1024 / 1024).toFixed(0)} MiB ceiling (likely packed pear-runtime, duplicate natives, or unexpected QVAC growth)`
)
}
return st.size
}
/** Darwin JIT/library-validation entitlements required under Hardened Runtime. */
const REQUIRED_ENTITLEMENTS = [
'com.apple.security.cs.allow-jit',
'com.apple.security.cs.allow-unsigned-executable-memory',
'com.apple.security.cs.disable-library-validation',
]
function assertDarwinEntitlements(product, host, file) {
if (!host.startsWith('darwin') || process.platform !== 'darwin') return
const r = spawnSync('codesign', ['-d', '--entitlements', '-', file], {
encoding: 'utf8',
})
if (r.status !== 0) {
throw new Error(
`[verify] ${product}/${host}: codesign -d --entitlements failed: ${r.stderr || r.stdout}`
)
}
const out = r.stdout || ''
const missing = REQUIRED_ENTITLEMENTS.filter((key) => !out.includes(key))
if (missing.length) {
throw new Error(
`[verify] ${product}/${host}: missing macOS entitlements: ${missing.join(', ')}`
)
}
console.log(`[verify] ${product}/${host}: darwin entitlements ok`)
}
function assertMarkers(file) {
// Sample start of file as latin1 for cheap string checks on embedded bundle
const fd = fs.openSync(file, 'r')
try {
const buf = Buffer.alloc(Math.min(fs.fstatSync(fd).size, 48 * 1024 * 1024))
fs.readSync(fd, buf, 0, buf.length, Math.max(0, fs.fstatSync(fd).size - buf.length))
const s = buf.toString('latin1')
if (!s.includes('standalone.mjs') && !s.includes('bare-os-host-flags') && !s.includes('--datadir')) {
// binary may compress/strip; require at least one app marker
if (!s.includes('bare-os-booter') && !s.includes('bare-os-seeder')) {
throw new Error(`${file}: missing bare-os markers in embed`)
}
}
// pear-runtime stub path or real module both contain the string; prefer stub class marker
if (s.includes('PearRuntimeStub') || s.includes('pear-runtime not packed')) {
console.log('[verify] pear-runtime stub present (OTA not embedded)')
}
if (file.includes('bare-os-booter') && !s.includes('bare-os-discord-gateway-ws')) {
throw new Error(
`${file}: packed booter missing Discord WHATWG gateway WS bootstrap (bare-os-discord-gateway-ws)`
)
}
} finally {
fs.closeSync(fd)
}
}
function smokeNativeBooter(file) {
if (process.platform === 'win32') {
console.log('[verify] skip smoke on win32')
return
}
const data = path.join(
require('os').tmpdir(),
`bare-os-verify-${process.pid}-${Date.now()}`
)
fs.mkdirSync(data, { recursive: true })
const env = {
...process.env,
BARE_OS_HOST_DATA: data,
BARE_OS_BOOT_TIMEOUT_MS: '1500',
BARE_OS_OTA_DISABLE: '1',
}
console.log(`[verify] smoke: ${file} --datadir ${data}`)
const r = spawnSync(file, ['--datadir', data, '--no-updates'], {
env,
encoding: 'utf8',
timeout: 20_000,
})
const out = `${r.stdout || ''}${r.stderr || ''}`
if (/Failed to reserve virtual memory for CodeRange/i.test(out)) {
throw new Error('smoke failed: CodeRange OOM')
}
if (/MODULE_NOT_FOUND.*bare-os-booter/i.test(out)) {
throw new Error(`smoke failed: module missing\n${out.slice(0, 800)}`)
}
// Expected without a seeder peer
if (!/No swarm peers|swarm peers within/i.test(out) && r.status === 0 && !out.trim()) {
console.warn('[verify] WARN: smoke produced little output; status', r.status)
} else {
console.log('[verify] smoke ok (exit', r.status, ')')
}
// datadir must have been applied
const store = path.join(data, 'corestore', 'booter')
if (!fs.existsSync(store)) {
throw new Error(`smoke: --datadir did not create ${store}`)
}
console.log('[verify] --datadir ok →', store)
}
function main() {
const argProducts = process.argv.slice(2).filter((a) => a === 'seeder' || a === 'booter')
const products = argProducts.length ? argProducts : ['seeder', 'booter']
let checked = 0
for (const product of products) {
for (const host of hosts) {
const file = findBinary(product, host)
if (!file) {
console.warn(`[verify] WARN: missing ${product}/${host}`)
continue
}
assertSize(product, host, file)
assertMarkers(file)
assertDarwinEntitlements(product, host, file)
checked++
if (product === 'booter' && host === nativeHost) {
smokeNativeBooter(file)
}
}
}
if (checked === 0) {
throw new Error('no binaries found under out/ — run make first')
}
console.log(`[verify] passed (${checked} binaries)`)
}
try {
main()
} catch (err) {
console.error('[verify] FAIL:', err.message || err)
process.exit(1)
}