Updates
CI / test (push) Successful in 2m37s
Release rolling / release (push) Successful in 11m29s

This commit is contained in:
Raven Scott
2026-07-30 15:45:33 -04:00
parent 4b5b78892f
commit 726dbdfd56
10 changed files with 523 additions and 28 deletions
+342
View File
@@ -0,0 +1,342 @@
#!/usr/bin/env node
/**
* Force-install bare-runtime platform binaries needed for QVAC Electron clients.
*
* Why this exists
* ---------------
* `bare-runtime` resolves the worker binary via:
* require(`bare-runtime-${platform}-${arch}`)
* Those packages are **optionalDependencies** with `os`/`cpu` filters, so
* `npm ci` on a Linux CI runner never installs darwin/win32 platform packages.
* Cross-compiling clients then packages an app that fails at runtime with:
* Could not load the Bare runtime binary for darwin-arm64
* … bare-runtime-darwin-arm64 … missing
* (https://github.com/tetherto/qvac/issues/1492)
*
* Usage
* -----
* node scripts/ensure-qvac-bare-runtimes.cjs
* node scripts/ensure-qvac-bare-runtimes.cjs --hosts darwin-arm64,linux-x64
* node scripts/ensure-qvac-bare-runtimes.cjs --all
*
* Env:
* PEARDATA_CLIENT_HOSTS comma list (default: all QVAC-native hosts)
* PEARDATA_SKIP_QVAC=1 no-op exit 0
*/
'use strict'
const fs = require('fs')
const path = require('path')
const { execFileSync } = require('child_process')
const { QVAC_NATIVE_HOSTS, parseHostList, ALL_64 } = require('./hosts.cjs')
const root = path.resolve(__dirname, '..')
function readBareRuntimeVersion() {
try {
// exports map uses "./package" not "./package.json"
const pkgPath = require.resolve('bare-runtime/package', { paths: [root] })
return require(pkgPath).version
} catch {
try {
return require(path.join(root, 'node_modules', 'bare-runtime', 'package.json'))
.version
} catch {
const rootPkg = require(path.join(root, 'package.json'))
const v =
rootPkg.dependencies?.['bare-runtime'] ||
rootPkg.optionalDependencies?.['bare-runtime'] ||
'1.30.3'
return String(v).replace(/^[\^~]/, '')
}
}
}
/**
* @param {string} host e.g. darwin-arm64
* @returns {string} path to bare binary inside the platform package
*/
function bareBinaryPath(host) {
const [platform] = host.split('-')
const bin = platform === 'win32' ? 'bare.exe' : 'bare'
return path.join(root, 'node_modules', `bare-runtime-${host}`, 'bin', bin)
}
/**
* @param {string} host
* @returns {boolean}
*/
function hasPlatformPackage(host) {
const pkgJson = path.join(
root,
'node_modules',
`bare-runtime-${host}`,
'package.json'
)
if (!fs.existsSync(pkgJson)) return false
return fs.existsSync(bareBinaryPath(host))
}
/**
* Install one platform package without npm's os/cpu prune of siblings.
*
* Why not `npm install --force --os=… --cpu=…`?
* Each install with a foreign --os/--cpu **removes** other bare-runtime-*
* packages that don't match that os/cpu. Cross-compiling the full client
* matrix needs linux + darwin + win32 packages side-by-side on the runner.
*
* Approach: `npm pack` the tarball → extract into node_modules/<name>.
* Also ensure `require-asset` (dependency of platform packages) is present.
*
* @param {string} host e.g. darwin-arm64
* @param {string} version
*/
function installPlatformPackage(host, version) {
const name = `bare-runtime-${host}`
const spec = `${name}@${version}`
const dest = path.join(root, 'node_modules', name)
const tmp = fs.mkdtempSync(path.join(require('os').tmpdir(), 'peardata-bare-'))
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
console.log(`[ensure-bare-runtime] packing ${spec}`)
try {
const out = execFileSync(npm, ['pack', spec, '--pack-destination', tmp], {
cwd: root,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, npm_config_fund: 'false', npm_config_audit: 'false' },
})
const tgzName = String(out)
.trim()
.split(/\r?\n/)
.filter(Boolean)
.pop()
if (!tgzName) throw new Error(`npm pack produced no tarball for ${spec}`)
const tgz = path.join(tmp, tgzName)
if (!fs.existsSync(tgz)) throw new Error(`missing tarball ${tgz}`)
// Extract package/* → node_modules/<name>/
if (fs.existsSync(dest)) {
fs.rmSync(dest, { recursive: true, force: true })
}
fs.mkdirSync(dest, { recursive: true })
// npm pack layout: package/bin/bare, package/package.json, …
execFileSync('tar', ['-xzf', tgz, '-C', dest, '--strip-components=1'], {
stdio: 'inherit',
})
// Ensure the Bare binary is executable (tar + some FS syncs drop +x)
chmodBareBinary(dest, host)
console.log(`[ensure-bare-runtime] extracted ${name} → node_modules/${name}`)
} finally {
try {
fs.rmSync(tmp, { recursive: true, force: true })
} catch {
// ignore
}
}
// Platform packages depend on require-asset (and its small dep chain).
ensureRequireAsset()
}
/**
* @param {string} pkgDir node_modules/bare-runtime-<host>
* @param {string} host
*/
function chmodBareBinary(pkgDir, host) {
const binName = host.startsWith('win32') ? 'bare.exe' : 'bare'
const bin = path.join(pkgDir, 'bin', binName)
if (!fs.existsSync(bin)) return
try {
fs.chmodSync(bin, 0o755)
} catch (err) {
console.warn(`[ensure-bare-runtime] chmod ${bin}:`, err?.message || err)
}
}
/**
* Ensure require-asset is available for platform package index.js
* (they do: require.asset = require('require-asset')).
*/
function ensureRequireAsset() {
const dest = path.join(root, 'node_modules', 'require-asset')
if (fs.existsSync(path.join(dest, 'package.json'))) return
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
console.log('[ensure-bare-runtime] installing require-asset (platform package dep)…')
execFileSync(
npm,
['install', 'require-asset@^1.0.2', '--no-save', '--no-audit', '--no-fund'],
{ cwd: root, stdio: 'inherit' }
)
}
/**
* Also mirror into bare-runtime/node_modules so nested require() always finds it
* (matches how bare-runtime optionalDeps would look when installed on-target).
* @param {string} host
*/
function linkIntoBareRuntime(host) {
const name = `bare-runtime-${host}`
const src = path.join(root, 'node_modules', name)
const bareRt = path.join(root, 'node_modules', 'bare-runtime')
if (!fs.existsSync(src) || !fs.existsSync(bareRt)) return
const destParent = path.join(bareRt, 'node_modules')
const dest = path.join(destParent, name)
try {
fs.mkdirSync(destParent, { recursive: true })
if (fs.existsSync(dest)) {
const st = fs.lstatSync(dest)
if (st.isSymbolicLink() || st.isDirectory()) {
// Already present — leave alone if realpath matches
try {
if (fs.realpathSync(dest) === fs.realpathSync(src)) return
} catch {
// replace
}
fs.rmSync(dest, { recursive: true, force: true })
}
}
// Prefer junction/symlink on same volume; fall back to copy
try {
fs.symlinkSync(src, dest, process.platform === 'win32' ? 'junction' : 'dir')
console.log(`[ensure-bare-runtime] linked ${name} → bare-runtime/node_modules/`)
} catch {
fs.cpSync(src, dest, { recursive: true })
console.log(`[ensure-bare-runtime] copied ${name} → bare-runtime/node_modules/`)
}
} catch (err) {
console.warn(
`[ensure-bare-runtime] warn: could not nest ${name}:`,
err?.message || err
)
}
}
/**
* @param {string[]} hosts
*/
function ensure(hosts) {
if (
process.env.PEARDATA_SKIP_QVAC === '1' ||
process.env.PEARDATA_SKIP_QVAC === 'true'
) {
console.log('[ensure-bare-runtime] skipped (PEARDATA_SKIP_QVAC=1)')
return { skipped: true, hosts: [] }
}
const version = readBareRuntimeVersion()
console.log(
`[ensure-bare-runtime] bare-runtime@${version}; hosts=${hosts.join(',') || '(none)'}`
)
// Parent package must exist
const bareRoot = path.join(root, 'node_modules', 'bare-runtime')
if (!fs.existsSync(bareRoot)) {
console.log(`[ensure-bare-runtime] installing bare-runtime@${version}`)
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
execFileSync(
npm,
['install', `bare-runtime@${version}`, '--no-save', '--no-audit', '--no-fund', '--force'],
{ cwd: root, stdio: 'inherit' }
)
}
/** @type {string[]} */
const installed = []
/** @type {string[]} */
const missing = []
for (const host of hosts) {
if (!/^[a-z0-9]+-[a-z0-9]+$/i.test(host)) {
console.warn(`[ensure-bare-runtime] skip invalid host: ${host}`)
continue
}
// win32-arm64 has no bare-runtime package published? check optional list — it is published
if (hasPlatformPackage(host)) {
// Re-apply +x (packages copied from CI artifacts / packagers may lose it)
chmodBareBinary(path.join(root, 'node_modules', `bare-runtime-${host}`), host)
console.log(`[ensure-bare-runtime] ok (present): bare-runtime-${host}`)
linkIntoBareRuntime(host)
installed.push(host)
continue
}
try {
installPlatformPackage(host, version)
} catch (err) {
console.error(
`[ensure-bare-runtime] FAILED to install bare-runtime-${host}:`,
err?.message || err
)
missing.push(host)
continue
}
if (!hasPlatformPackage(host)) {
console.error(
`[ensure-bare-runtime] install reported ok but binary missing: ${bareBinaryPath(host)}`
)
missing.push(host)
continue
}
linkIntoBareRuntime(host)
installed.push(host)
}
if (missing.length) {
const msg =
`[ensure-bare-runtime] missing platform packages after install: ${missing.join(', ')}\n` +
`QVAC clients for those hosts will fail with BareRuntimeBinaryNotFoundError.\n` +
`See https://github.com/tetherto/qvac/issues/1492`
console.error(msg)
process.exitCode = 1
return { ok: false, installed, missing }
}
console.log(
`[ensure-bare-runtime] ready: ${installed.map((h) => `bare-runtime-${h}`).join(', ')}`
)
return { ok: true, installed, missing: [] }
}
function parseArgs(argv) {
/** @type {string[]|null} */
let hosts = null
let all = false
for (let i = 0; i < argv.length; i++) {
const a = argv[i]
if (a === '--all') {
all = true
} else if (a === '--hosts' && argv[i + 1]) {
hosts = parseHostList(argv[++i], [])
} else if (a.startsWith('--hosts=')) {
hosts = parseHostList(a.slice('--hosts='.length), [])
} else if (a === '--help' || a === '-h') {
console.log(`Usage: node scripts/ensure-qvac-bare-runtimes.cjs [--all | --hosts a,b]`)
process.exit(0)
}
}
if (all) {
// All client hosts that can ship QVAC + bare-runtime platform pkgs
return [...QVAC_NATIVE_HOSTS]
}
if (hosts?.length) return hosts
if (process.env.PEARDATA_CLIENT_HOSTS) {
return parseHostList(process.env.PEARDATA_CLIENT_HOSTS, [...QVAC_NATIVE_HOSTS]).filter(
(h) => QVAC_NATIVE_HOSTS.includes(h) || ALL_64.includes(h)
)
}
// Default: every host that can run full QVAC
return [...QVAC_NATIVE_HOSTS]
}
if (require.main === module) {
ensure(parseArgs(process.argv.slice(2)))
}
module.exports = {
ensure,
hasPlatformPackage,
bareBinaryPath,
readBareRuntimeVersion,
QVAC_NATIVE_HOSTS,
}
+19
View File
@@ -90,10 +90,29 @@ function predownloadElectron(hosts) {
})
}
function ensureBareRuntimes(hosts) {
if (process.env.PEARDATA_SKIP_QVAC === '1' || process.env.PEARDATA_SKIP_QVAC === 'true') {
console.log('[make] skip bare-runtime ensure (PEARDATA_SKIP_QVAC=1)')
return
}
// Only QVAC-native hosts need the platform bare binary in the client package
const need = hosts.filter((h) => hostSupportsQvacNative(h))
if (!need.length) return
console.log(`[make] ensuring bare-runtime platform packages: ${need.join(', ')}`)
run(process.execPath, [
path.join(root, 'scripts', 'ensure-qvac-bare-runtimes.cjs'),
'--hosts',
need.join(','),
])
}
function makeClient(hosts = CLIENT_HOSTS) {
console.log(`[make] client hosts: ${hosts.join(', ')}`)
npmRun('build:client-bundle')
predownloadElectron(hosts)
// Cross-host CI: npm ci never installs darwin/win32 optional bare-runtime-* packages.
// Install them before forge packages so the app ships the Bare worker binary.
ensureBareRuntimes(hosts)
for (const host of hosts) {
const script = clientNpmScript(host)