#!/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/. * 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// 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- * @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, }