343 lines
11 KiB
JavaScript
343 lines
11 KiB
JavaScript
#!/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,
|
|
}
|