#!/usr/bin/env node /** * Orchestrate peardata binary builds. * * node scripts/make.cjs server # Linux server hosts (bare-build) * node scripts/make.cjs client # all client hosts (electron-forge) * node scripts/make.cjs both|all # server + client * * Env: * PEARDATA_SERVER_HOSTS=linux-x64,linux-arm64 * PEARDATA_CLIENT_HOSTS=linux-x64,darwin-arm64,… * PEARDATA_CLIENT_TIMEOUT_MS=600000 (raise when packaging QVAC natives) * PEARDATA_SKIP_ELECTRON_PREDOWNLOAD=1 * PEARDATA_SKIP_QVAC=1 tools-only Electron client (no @qvac/sdk bundle) */ 'use strict' const path = require('path') const { spawnSync } = require('child_process') const { SERVER_LINUX, ALL_64, parseHostList, clientNpmScript, hostToElectron, hostSupportsQvacNative, } = require('./hosts.cjs') const root = path.resolve(__dirname, '..') const SERVER_HOSTS = parseHostList(process.env.PEARDATA_SERVER_HOSTS, SERVER_LINUX) const CLIENT_HOSTS = parseHostList(process.env.PEARDATA_CLIENT_HOSTS, ALL_64) // QVAC native bundle + forge package is heavier than the old asar-only client const CLIENT_TIMEOUT_MS = Number( process.env.PEARDATA_CLIENT_TIMEOUT_MS || (process.env.PEARDATA_SKIP_QVAC === '1' ? 600_000 : 1_200_000) ) function run(cmd, args, opts = {}) { console.log(`\n$ ${cmd} ${args.join(' ')}\n`) const t0 = Date.now() const res = spawnSync(cmd, args, { cwd: root, stdio: 'inherit', env: process.env, shell: process.platform === 'win32', ...opts, }) if (res.error) throw res.error if (res.status !== 0) process.exit(res.status || 1) console.log(`[make] ok in ${((Date.now() - t0) / 1000).toFixed(1)}s`) } function npmRun(script, env) { run(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', script], { env: env ? { ...process.env, ...env } : process.env, }) } function makeServer(hosts = SERVER_HOSTS) { for (const host of hosts) { if (!SERVER_LINUX.includes(host)) { console.error(`[make] refusing non-Linux server host: ${host}`) process.exit(1) } } console.log(`[make] server hosts (Linux only): ${hosts.join(', ')}`) for (const host of hosts) { run(process.execPath, [ path.join(root, 'scripts', 'bare-standalone.cjs'), '--product', 'server', '--host', host, ]) } } function predownloadElectron(hosts) { if (process.env.PEARDATA_SKIP_ELECTRON_PREDOWNLOAD === '1') { console.log('[make] skip electron predownload (PEARDATA_SKIP_ELECTRON_PREDOWNLOAD=1)') return } console.log('[make] pre-downloading Electron for client hosts…') run(process.execPath, [path.join(root, 'scripts', 'predownload-electron.cjs')], { env: { ...process.env, PEARDATA_CLIENT_HOSTS: hosts.join(','), }, }) } 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) if (!script) { console.error(`[make] no client script for host ${host}`) process.exit(1) } const { platform, arch } = hostToElectron(host) // QVAC Bare prebuilds are not published for every Electron host (e.g. no win32-arm64). // Package a tools-only client there so the matrix still ships a binary. const forceSkipQvac = process.env.PEARDATA_SKIP_QVAC === '1' || !hostSupportsQvacNative(host) if (forceSkipQvac && process.env.PEARDATA_SKIP_QVAC !== '1') { console.log( `[make] client ${host}: no @qvac/llm-llamacpp prebuild — PEARDATA_SKIP_QVAC=1 (tools-only)` ) } console.log( `[make] client ${host} (filter prebuilds to ${platform}-${arch}, qvac=${forceSkipQvac ? 'off' : 'on'})` ) const env = { ...process.env, npm_config_build_from_source: 'false', PEARDATA_SKIP_REBUILD: process.env.PEARDATA_SKIP_REBUILD || '1', PEARDATA_PACKAGE_PLATFORM: platform, PEARDATA_PACKAGE_ARCH: arch, PEARDATA_SKIP_PREPACKAGE_BUNDLE: '1', PEARDATA_SKIP_QVAC: forceSkipQvac ? '1' : process.env.PEARDATA_SKIP_QVAC || '0', DEBUG: process.env.DEBUG || (process.env.CI ? 'electron-packager,electron-forge:lifecycle' : ''), } console.log( `[make] client timeout: ${(CLIENT_TIMEOUT_MS / 1000).toFixed(0)}s (PEARDATA_CLIENT_TIMEOUT_MS)` ) const t0 = Date.now() const res = spawnSync( process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', script], { cwd: root, stdio: 'inherit', env, shell: process.platform === 'win32', timeout: CLIENT_TIMEOUT_MS, killSignal: 'SIGKILL', } ) if (res.error) { if (res.error.code === 'ETIMEDOUT') { console.error( `[make] FATAL: client package ${host} exceeded ${CLIENT_TIMEOUT_MS}ms — killed.` ) process.exit(1) } throw res.error } if (res.status !== 0) { console.error(`[make] client package ${host} failed with status ${res.status}`) process.exit(res.status || 1) } console.log(`[make] ok ${host} in ${((Date.now() - t0) / 1000).toFixed(1)}s`) } } function main() { const mode = process.argv[2] || 'all' const thisHost = `${process.platform}-${process.arch}` switch (mode) { case 'server': makeServer() break case 'client': makeClient() break case 'client-this': makeClient([thisHost]) break case 'both': case 'all': makeServer() makeClient() break case 'client-all': makeClient(ALL_64) break default: console.error(`Unknown mode: ${mode}`) console.error( 'Usage: node scripts/make.cjs [server|client|client-this|both|all|client-all]' ) console.error(`Server (Linux): ${SERVER_LINUX.join(', ')}`) console.error(`Client (all): ${ALL_64.join(', ')}`) process.exit(1) } console.log('\n[make] complete') } main()