168 lines
5.8 KiB
JavaScript
168 lines
5.8 KiB
JavaScript
import { spawn, spawnSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import http from 'node:http';
|
|
import net from 'node:net';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import process from 'node:process';
|
|
import { createRequire } from 'node:module';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const { buildChildEnv } = require('../apps/desktop/sidecar.cjs');
|
|
|
|
const PYTHON_VERSION = '3.13.12';
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const venvDir = path.join(repoRoot, '.sidecar-venv');
|
|
const python = path.join(venvDir, 'Scripts', 'python.exe');
|
|
const lockFile = path.join(repoRoot, 'packaging', 'requirements-sidecar.lock');
|
|
const specFile = path.join(repoRoot, 'packaging', 'aps-sidecar.spec');
|
|
const executable = path.join(repoRoot, 'dist', 'sidecar', 'aps-sidecar', 'aps-sidecar.exe');
|
|
|
|
function run(command, args, options = {}) {
|
|
const completed = spawnSync(command, args, {
|
|
cwd: repoRoot,
|
|
env: buildChildEnv(process.env, { UV_NO_CONFIG: '1' }),
|
|
stdio: 'inherit',
|
|
windowsHide: true,
|
|
...options,
|
|
});
|
|
if (completed.error) throw completed.error;
|
|
if (completed.status !== 0) {
|
|
throw new Error(`${command} ${args.join(' ')} exited with code ${completed.status}`);
|
|
}
|
|
}
|
|
|
|
function readRuntimeIdentity() {
|
|
const script = [
|
|
'import json, platform, sys',
|
|
'print(json.dumps({',
|
|
' "implementation": platform.python_implementation(),',
|
|
' "version": platform.python_version(),',
|
|
' "machine": platform.machine(),',
|
|
' "base_executable": getattr(sys, "_base_executable", sys.executable),',
|
|
'}))',
|
|
].join('\n');
|
|
const completed = spawnSync(python, ['-c', script], {
|
|
cwd: repoRoot,
|
|
env: buildChildEnv(process.env, {}),
|
|
encoding: 'utf8',
|
|
windowsHide: true,
|
|
});
|
|
if (completed.status !== 0) throw new Error(completed.stderr || 'cannot inspect sidecar Python');
|
|
return JSON.parse(completed.stdout.trim());
|
|
}
|
|
|
|
function validateRuntime() {
|
|
const identity = readRuntimeIdentity();
|
|
if (identity.implementation !== 'CPython' || identity.version !== PYTHON_VERSION) {
|
|
throw new Error(`Sidecar runtime must be CPython ${PYTHON_VERSION}; got ${JSON.stringify(identity)}`);
|
|
}
|
|
if (!/amd64|x86_64/i.test(identity.machine)) {
|
|
throw new Error(`Sidecar runtime must be x86-64; got ${identity.machine}`);
|
|
}
|
|
if (/anaconda|conda/i.test(identity.base_executable)) {
|
|
throw new Error(`Conda runtime is forbidden for release sidecars: ${identity.base_executable}`);
|
|
}
|
|
console.log(`sidecar runtime: ${identity.implementation} ${identity.version} ${identity.machine}`);
|
|
}
|
|
|
|
function reservePort() {
|
|
return new Promise((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.once('error', reject);
|
|
server.listen({ host: '127.0.0.1', port: 0, exclusive: true }, () => {
|
|
const address = server.address();
|
|
const port = typeof address === 'object' && address ? address.port : 0;
|
|
server.close((error) => error ? reject(error) : resolve(port));
|
|
});
|
|
});
|
|
}
|
|
|
|
function healthProbe(url, expectedNonce) {
|
|
return new Promise((resolve, reject) => {
|
|
const request = http.get(url, {
|
|
timeout: 1_500,
|
|
headers: { 'x-aps-sidecar-nonce': expectedNonce },
|
|
}, (response) => {
|
|
response.resume();
|
|
if (response.statusCode !== 200) {
|
|
reject(new Error(`health returned HTTP ${response.statusCode}`));
|
|
} else if (response.headers['x-aps-sidecar-nonce'] !== expectedNonce) {
|
|
reject(new Error('health returned an unexpected sidecar identity'));
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
request.once('timeout', () => request.destroy(new Error('health probe timed out')));
|
|
request.once('error', reject);
|
|
});
|
|
}
|
|
|
|
function sidecarEnv(overrides) {
|
|
return buildChildEnv(process.env, overrides);
|
|
}
|
|
|
|
async function smokeFrozenSidecar() {
|
|
const port = await reservePort();
|
|
const smokeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'aps-frozen-smoke-'));
|
|
const uiDir = path.join(repoRoot, 'apps', 'web', 'dist');
|
|
const nonce = 'f'.repeat(64);
|
|
const child = spawn(executable, [], {
|
|
cwd: path.dirname(executable),
|
|
env: sidecarEnv({
|
|
APS_MODE: 'desktop',
|
|
APS_HOME: smokeHome,
|
|
APS_API_HOST: '127.0.0.1',
|
|
APS_API_PORT: String(port),
|
|
APS_UI_DIR: uiDir,
|
|
APS_SIDECAR_NONCE: nonce,
|
|
}),
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
windowsHide: true,
|
|
});
|
|
let output = '';
|
|
child.stdout.on('data', (chunk) => { output += String(chunk); });
|
|
child.stderr.on('data', (chunk) => { output += String(chunk); });
|
|
|
|
try {
|
|
const deadline = Date.now() + 60_000;
|
|
while (Date.now() < deadline) {
|
|
if (child.exitCode !== null) {
|
|
throw new Error(`frozen sidecar exited with ${child.exitCode}:\n${output}`);
|
|
}
|
|
try {
|
|
await healthProbe(`http://127.0.0.1:${port}/api/health`, nonce);
|
|
console.log(`frozen sidecar smoke passed on port ${port}`);
|
|
return;
|
|
} catch {
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
}
|
|
}
|
|
throw new Error(`frozen sidecar health timeout:\n${output}`);
|
|
} finally {
|
|
child.kill();
|
|
fs.rmSync(smokeHome, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
if (process.platform !== 'win32') {
|
|
throw new Error('The current Sidecar release target is Windows x86-64.');
|
|
}
|
|
|
|
run('uv', ['venv', '--managed-python', '--python', PYTHON_VERSION, '--clear', venvDir]);
|
|
validateRuntime();
|
|
run('uv', [
|
|
'pip', 'install', '--python', python, '--require-hashes', '--only-binary', ':all:',
|
|
'--requirements', lockFile,
|
|
]);
|
|
run(python, ['scripts/check_solver_runtime.py', '--iterations', '5']);
|
|
run(python, [
|
|
'-m', 'PyInstaller', '--clean', '--noconfirm',
|
|
'--distpath', path.join(repoRoot, 'dist', 'sidecar'),
|
|
'--workpath', path.join(repoRoot, 'build', 'sidecar'),
|
|
specFile,
|
|
]);
|
|
run(executable, ['--probe-child']);
|
|
await smokeFrozenSidecar();
|