Updates
This commit is contained in:
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user