281 lines
8.2 KiB
JavaScript
281 lines
8.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { pipeline } from 'node:stream/promises';
|
|
import { Readable } from 'node:stream';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(scriptDir, '..');
|
|
const runtimeLockPath = path.join(repoRoot, 'runtime.lock.json');
|
|
const runtimeDir = path.join(repoRoot, 'resources', 'runtime');
|
|
const downloadRoot = path.join(repoRoot, '.runtime-download');
|
|
|
|
function printUsage() {
|
|
process.stdout.write(`Usage: node scripts/stage-runtime.mjs [options]
|
|
|
|
Options:
|
|
--platform <key> Runtime platform key. Defaults to the current platform.
|
|
--release-tag <tag> Release tag to download from. Defaults to runtime.lock.json.
|
|
--clean Remove staged runtime files and keep resources/runtime/.gitkeep.
|
|
--help Show this message.
|
|
`);
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const parsed = {
|
|
platform: null,
|
|
releaseTag: null,
|
|
clean: false,
|
|
help: false,
|
|
};
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === '--help' || arg === '-h') {
|
|
parsed.help = true;
|
|
continue;
|
|
}
|
|
if (arg === '--clean') {
|
|
parsed.clean = true;
|
|
continue;
|
|
}
|
|
if (arg === '--platform') {
|
|
parsed.platform = argv[index + 1] ?? null;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (arg === '--release-tag') {
|
|
parsed.releaseTag = argv[index + 1] ?? null;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
throw new Error(`Unknown argument: ${arg}`);
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function runOrThrow(command, args) {
|
|
const result = spawnSync(command, args, {
|
|
cwd: repoRoot,
|
|
stdio: 'inherit',
|
|
shell: false,
|
|
});
|
|
|
|
if (result.error) {
|
|
throw new Error(`Failed to run ${command}: ${result.error.message}`);
|
|
}
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(`Command failed: ${command} ${args.join(' ')}`);
|
|
}
|
|
}
|
|
|
|
function readRuntimeLock() {
|
|
return JSON.parse(fs.readFileSync(runtimeLockPath, 'utf8'));
|
|
}
|
|
|
|
function getDefaultPlatformKey() {
|
|
const key = `${process.platform}-${process.arch}`;
|
|
if (
|
|
key === 'darwin-arm64' ||
|
|
key === 'darwin-x64' ||
|
|
key === 'linux-x64' ||
|
|
key === 'win32-x64'
|
|
) {
|
|
return key;
|
|
}
|
|
throw new Error(`No bundled runtime asset is configured for ${key}`);
|
|
}
|
|
|
|
function getReleaseTag(runtimeLock, override) {
|
|
const tag = override?.trim() || runtimeLock.releaseTag?.trim() || runtimeLock.sourceRef?.trim();
|
|
if (!tag) {
|
|
throw new Error('runtime.lock.json does not define releaseTag or sourceRef');
|
|
}
|
|
return tag;
|
|
}
|
|
|
|
function getReleaseAssetUrl(runtimeLock, releaseTag, asset) {
|
|
return `https://github.com/${runtimeLock.releaseRepository}/releases/download/${releaseTag}/${encodeURIComponent(asset.file)}`;
|
|
}
|
|
|
|
function cleanRuntimeDir() {
|
|
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
for (const entry of fs.readdirSync(runtimeDir, { withFileTypes: true })) {
|
|
if (entry.name === '.gitkeep') {
|
|
continue;
|
|
}
|
|
fs.rmSync(path.join(runtimeDir, entry.name), { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function downloadFile(url, destinationPath) {
|
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
'user-agent': 'agent-teams-runtime-stager',
|
|
...(process.env.GH_TOKEN ? { authorization: `Bearer ${process.env.GH_TOKEN}` } : {}),
|
|
},
|
|
redirect: 'follow',
|
|
});
|
|
|
|
if (!response.ok || !response.body) {
|
|
throw new Error(`Failed to download runtime asset: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(destinationPath));
|
|
}
|
|
|
|
function downloadReleaseAssetWithGh(runtimeLock, releaseTag, asset, destinationPath) {
|
|
if (!process.env.GH_TOKEN) {
|
|
return false;
|
|
}
|
|
|
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
const result = spawnSync(
|
|
'gh',
|
|
[
|
|
'release',
|
|
'download',
|
|
releaseTag,
|
|
'--repo',
|
|
runtimeLock.releaseRepository,
|
|
'--pattern',
|
|
asset.file,
|
|
'--dir',
|
|
path.dirname(destinationPath),
|
|
'--clobber',
|
|
],
|
|
{
|
|
cwd: repoRoot,
|
|
stdio: 'inherit',
|
|
shell: false,
|
|
env: process.env,
|
|
}
|
|
);
|
|
|
|
return result.status === 0 && fs.existsSync(destinationPath);
|
|
}
|
|
|
|
function extractArchive(archivePath, extractDir, archiveKind) {
|
|
fs.mkdirSync(extractDir, { recursive: true });
|
|
|
|
if (archiveKind === 'tar.gz') {
|
|
runOrThrow('tar', ['-xzf', archivePath, '-C', extractDir]);
|
|
return;
|
|
}
|
|
|
|
if (archiveKind === 'zip') {
|
|
if (process.platform === 'win32') {
|
|
runOrThrow('powershell', [
|
|
'-NoProfile',
|
|
'-Command',
|
|
`Expand-Archive -LiteralPath '${archivePath.replace(/'/g, "''")}' -DestinationPath '${extractDir.replace(/'/g, "''")}' -Force`,
|
|
]);
|
|
return;
|
|
}
|
|
|
|
runOrThrow('unzip', ['-oq', archivePath, '-d', extractDir]);
|
|
return;
|
|
}
|
|
|
|
throw new Error(`Unsupported runtime archive kind: ${archiveKind}`);
|
|
}
|
|
|
|
function findRuntimePayloadDir(extractDir, binaryName) {
|
|
const candidates = [path.join(extractDir, 'runtime'), extractDir];
|
|
for (const candidate of candidates) {
|
|
if (
|
|
fs.existsSync(path.join(candidate, 'VERSION')) &&
|
|
fs.existsSync(path.join(candidate, binaryName))
|
|
) {
|
|
return candidate;
|
|
}
|
|
}
|
|
throw new Error(`Extracted runtime archive does not contain runtime/VERSION and ${binaryName}`);
|
|
}
|
|
|
|
function verifyStagedRuntime(runtimeLock, asset, platformKey) {
|
|
const versionPath = path.join(runtimeDir, 'VERSION');
|
|
const binaryPath = path.join(runtimeDir, asset.binaryName);
|
|
if (!fs.existsSync(versionPath)) {
|
|
throw new Error('Staged runtime is missing resources/runtime/VERSION');
|
|
}
|
|
if (!fs.existsSync(binaryPath)) {
|
|
throw new Error(`Staged runtime is missing resources/runtime/${asset.binaryName}`);
|
|
}
|
|
|
|
const versionText = fs.readFileSync(versionPath, 'utf8').trim();
|
|
if (!versionText.includes(runtimeLock.version)) {
|
|
throw new Error(
|
|
`Staged runtime version mismatch for ${platformKey}. Expected ${runtimeLock.version}, got ${versionText}`
|
|
);
|
|
}
|
|
}
|
|
|
|
async function stageRuntime(options) {
|
|
const runtimeLock = readRuntimeLock();
|
|
const platformKey = options.platform?.trim() || getDefaultPlatformKey();
|
|
const asset = runtimeLock.assets?.[platformKey];
|
|
if (!asset) {
|
|
throw new Error(`runtime.lock.json has no asset for ${platformKey}`);
|
|
}
|
|
|
|
const releaseTag = getReleaseTag(runtimeLock, options.releaseTag);
|
|
const workDir = path.join(downloadRoot, `stage-${platformKey}-${process.pid}-${Date.now()}`);
|
|
const archivePath = path.join(workDir, asset.file);
|
|
const extractDir = path.join(workDir, 'extracted');
|
|
|
|
fs.rmSync(workDir, { recursive: true, force: true });
|
|
fs.mkdirSync(workDir, { recursive: true });
|
|
|
|
try {
|
|
const url = getReleaseAssetUrl(runtimeLock, releaseTag, asset);
|
|
process.stdout.write(
|
|
`Downloading ${asset.file} from ${runtimeLock.releaseRepository}@${releaseTag}\n`
|
|
);
|
|
if (!downloadReleaseAssetWithGh(runtimeLock, releaseTag, asset, archivePath)) {
|
|
await downloadFile(url, archivePath);
|
|
}
|
|
|
|
process.stdout.write(`Extracting ${asset.file}\n`);
|
|
extractArchive(archivePath, extractDir, asset.archiveKind);
|
|
|
|
const payloadDir = findRuntimePayloadDir(extractDir, asset.binaryName);
|
|
cleanRuntimeDir();
|
|
fs.cpSync(payloadDir, runtimeDir, { recursive: true });
|
|
if (process.platform !== 'win32' && platformKey !== 'win32-x64') {
|
|
fs.chmodSync(path.join(runtimeDir, asset.binaryName), 0o755);
|
|
}
|
|
verifyStagedRuntime(runtimeLock, asset, platformKey);
|
|
process.stdout.write(`Staged runtime ${runtimeLock.version} for ${platformKey}\n`);
|
|
} finally {
|
|
fs.rmSync(workDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const options = parseArgs(process.argv.slice(2));
|
|
if (options.help) {
|
|
printUsage();
|
|
return;
|
|
}
|
|
|
|
if (options.clean) {
|
|
cleanRuntimeDir();
|
|
process.stdout.write('Cleaned resources/runtime\n');
|
|
return;
|
|
}
|
|
|
|
await stageRuntime(options);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exit(1);
|
|
});
|