mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 00:03:20 +00:00
266 lines
9.3 KiB
JavaScript
266 lines
9.3 KiB
JavaScript
import { execFile } from 'node:child_process'
|
|
import { mkdir, readFile, rm } from 'node:fs/promises'
|
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
import { promisify } from 'node:util'
|
|
|
|
import { buildPatchedSshRelayNodePty } from './ssh-relay-node-pty-build.mjs'
|
|
import { extractVerifiedSshRelayNodeBuildInputs } from './ssh-relay-node-tar-inspection.mjs'
|
|
import { extractVerifiedSshRelayNodeZipBuildInputs } from './ssh-relay-node-zip-inspection.mjs'
|
|
import { applySshRelayLinuxNodeGypCompilerFloor } from './ssh-relay-linux-node-gyp-compiler.mjs'
|
|
import {
|
|
createSshRelayRuntimeArchive,
|
|
inspectSshRelayRuntimeArchive
|
|
} from './ssh-relay-runtime-archive.mjs'
|
|
import { writeSshRelayRuntimeMetadata } from './ssh-relay-runtime-provenance.mjs'
|
|
import {
|
|
collectSshRelayRuntimeToolchain,
|
|
sshRelayRuntimeBuilderIdentity,
|
|
sshRelayRuntimeRunnerIdentity
|
|
} from './ssh-relay-runtime-toolchain.mjs'
|
|
import { assembleSshRelayRuntimeTree } from './ssh-relay-runtime-tree.mjs'
|
|
import { validateSshRelayNodeReleaseContract } from './ssh-relay-node-release-verification.mjs'
|
|
import { verifyNodeReleaseInputs } from './verify-ssh-relay-node-release-inputs.mjs'
|
|
|
|
const execFileAsync = promisify(execFile)
|
|
const scriptDirectory = import.meta.dirname
|
|
const projectRoot = resolve(scriptDirectory, '..', '..')
|
|
const defaultContractPath = resolve(projectRoot, 'config', 'ssh-relay-node-release-v24.18.0.json')
|
|
const SUPPORTED_TUPLES = new Set([
|
|
'linux-x64-glibc',
|
|
'linux-arm64-glibc',
|
|
'darwin-x64',
|
|
'darwin-arm64',
|
|
'win32-x64',
|
|
'win32-arm64'
|
|
])
|
|
const BUILD_COMMAND_TIMEOUT_MS = 10 * 60 * 1000
|
|
|
|
function valueAfter(argv, index, flag) {
|
|
const value = argv[index + 1]
|
|
if (!value || value.startsWith('--')) {
|
|
throw new Error(`${flag} requires a value`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
export function parseBuildArguments(argv) {
|
|
const result = { contractPath: defaultContractPath }
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const flag = argv[index]
|
|
const value = valueAfter(argv, index, flag)
|
|
if (flag === '--tuple') {
|
|
result.tuple = value
|
|
} else if (flag === '--inputs-directory') {
|
|
result.inputsDirectory = resolve(value)
|
|
} else if (flag === '--output-directory') {
|
|
result.outputDirectory = resolve(value)
|
|
} else if (flag === '--work-directory') {
|
|
result.workDirectory = resolve(value)
|
|
} else if (flag === '--contract') {
|
|
result.contractPath = resolve(value)
|
|
} else if (flag === '--source-date-epoch') {
|
|
result.sourceDateEpoch = Number(value)
|
|
} else if (flag === '--git-commit') {
|
|
result.gitCommit = value
|
|
} else {
|
|
throw new Error(`Unknown argument: ${flag}`)
|
|
}
|
|
index += 1
|
|
}
|
|
for (const field of [
|
|
'tuple',
|
|
'inputsDirectory',
|
|
'outputDirectory',
|
|
'workDirectory',
|
|
'sourceDateEpoch',
|
|
'gitCommit'
|
|
]) {
|
|
if (result[field] === undefined) {
|
|
throw new Error(`Missing required build argument: ${field}`)
|
|
}
|
|
}
|
|
if (!SUPPORTED_TUPLES.has(result.tuple)) {
|
|
throw new Error(`Unsupported runtime build tuple: ${result.tuple}`)
|
|
}
|
|
if (!Number.isSafeInteger(result.sourceDateEpoch) || result.sourceDateEpoch < 0) {
|
|
throw new Error('--source-date-epoch must be a non-negative safe integer')
|
|
}
|
|
if (!/^[0-9a-f]{40}$/.test(result.gitCommit)) {
|
|
throw new Error('--git-commit must be a full lowercase SHA-1')
|
|
}
|
|
return result
|
|
}
|
|
|
|
function containsPath(parent, candidate) {
|
|
const path = relative(parent, candidate)
|
|
return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path))
|
|
}
|
|
|
|
export function assertDisjointRuntimeBuildDirectories(outputDirectory, workDirectory) {
|
|
if (
|
|
containsPath(outputDirectory, workDirectory) ||
|
|
containsPath(workDirectory, outputDirectory)
|
|
) {
|
|
throw new Error('Runtime output and work directories must be disjoint')
|
|
}
|
|
}
|
|
|
|
function assertTargetNative(tuple) {
|
|
const os = tuple.startsWith('linux-') ? 'linux' : tuple.startsWith('darwin-') ? 'darwin' : 'win32'
|
|
const architecture = tuple.includes('arm64') ? 'arm64' : 'x64'
|
|
if (process.platform !== os || process.arch !== architecture) {
|
|
throw new Error(`Runtime ${tuple} must be assembled on target-native ${os}/${architecture}`)
|
|
}
|
|
if (os === 'linux') {
|
|
const glibc = process.report?.getReport?.().header?.glibcVersionRuntime
|
|
if (typeof glibc !== 'string' || glibc.length === 0) {
|
|
throw new Error(`Runtime ${tuple} requires a native glibc build environment`)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function run(command, args, options = {}) {
|
|
const result = await execFileAsync(command, args, {
|
|
...options,
|
|
encoding: 'utf8',
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
timeout: options.timeout ?? BUILD_COMMAND_TIMEOUT_MS,
|
|
windowsHide: true
|
|
})
|
|
if (result.stdout) {
|
|
process.stdout.write(result.stdout)
|
|
}
|
|
if (result.stderr) {
|
|
process.stderr.write(result.stderr)
|
|
}
|
|
return result.stdout.trim()
|
|
}
|
|
|
|
function relayPlatform(tuple) {
|
|
return tuple.replace('-glibc', '')
|
|
}
|
|
|
|
export async function buildSshRelayRuntime(options) {
|
|
const started = process.hrtime.bigint()
|
|
assertTargetNative(options.tuple)
|
|
assertDisjointRuntimeBuildDirectories(options.outputDirectory, options.workDirectory)
|
|
const release = validateSshRelayNodeReleaseContract(
|
|
JSON.parse(await readFile(options.contractPath, 'utf8'))
|
|
)
|
|
await verifyNodeReleaseInputs({
|
|
contractPath: options.contractPath,
|
|
inputsDirectory: options.inputsDirectory,
|
|
archiveTuples: [options.tuple]
|
|
})
|
|
const workDirectory = options.workDirectory
|
|
// Why: both final and clean-build paths are exclusive, but callers need not pre-create parents.
|
|
await Promise.all([
|
|
mkdir(dirname(options.outputDirectory), { recursive: true }),
|
|
mkdir(dirname(workDirectory), { recursive: true })
|
|
])
|
|
await mkdir(options.outputDirectory)
|
|
let workDirectoryCreated = false
|
|
try {
|
|
await mkdir(workDirectory)
|
|
workDirectoryCreated = true
|
|
await run(process.execPath, [resolve(scriptDirectory, 'build-relay.mjs')], { cwd: projectRoot })
|
|
const extractedDirectory = join(workDirectory, 'node-inputs')
|
|
const nodeArchivePath = join(options.inputsDirectory, release.archives[options.tuple].name)
|
|
const extracted = options.tuple.startsWith('win32-')
|
|
? await extractVerifiedSshRelayNodeZipBuildInputs(
|
|
release,
|
|
options.tuple,
|
|
nodeArchivePath,
|
|
extractedDirectory,
|
|
{
|
|
headersArchivePath: join(
|
|
options.inputsDirectory,
|
|
release.windowsBuildInputs.headersArchive.name
|
|
),
|
|
importLibraryPath: join(
|
|
options.inputsDirectory,
|
|
...release.windowsBuildInputs.importLibraries[options.tuple].name.split('/')
|
|
)
|
|
}
|
|
)
|
|
: await extractVerifiedSshRelayNodeBuildInputs(
|
|
release,
|
|
options.tuple,
|
|
nodeArchivePath,
|
|
extractedDirectory
|
|
)
|
|
const bundledVersion = await run(extracted.nodePath, ['--version'])
|
|
if (bundledVersion !== `v${release.nodeVersion}`) {
|
|
throw new Error(`Bundled Node version mismatch: ${bundledVersion}`)
|
|
}
|
|
await applySshRelayLinuxNodeGypCompilerFloor({
|
|
nodeRoot: extracted.extractedRoot,
|
|
tuple: options.tuple
|
|
})
|
|
const nodePty = await buildPatchedSshRelayNodePty({
|
|
projectRoot,
|
|
nodePath: extracted.nodePath,
|
|
nodeRoot: extracted.extractedRoot,
|
|
nodeVersion: release.nodeVersion,
|
|
tuple: options.tuple,
|
|
buildDirectory: join(workDirectory, 'node-pty')
|
|
})
|
|
const runtimeRoot = join(options.outputDirectory, 'runtime')
|
|
const identity = await assembleSshRelayRuntimeTree({
|
|
tuple: options.tuple,
|
|
nodeRoot: extracted.extractedRoot,
|
|
nodePtyBuildDirectory: nodePty.buildDirectory,
|
|
relayDirectory: resolve(projectRoot, 'out', 'relay', relayPlatform(options.tuple)),
|
|
runtimeRoot,
|
|
nodeVersion: release.nodeVersion
|
|
})
|
|
const archive = await createSshRelayRuntimeArchive({
|
|
runtimeRoot,
|
|
outputDirectory: options.outputDirectory,
|
|
identity,
|
|
sourceDateEpoch: options.sourceDateEpoch
|
|
})
|
|
const inspection = await inspectSshRelayRuntimeArchive(archive.path, identity)
|
|
const toolchain = await collectSshRelayRuntimeToolchain(extracted.nodePath)
|
|
const metadata = await writeSshRelayRuntimeMetadata({
|
|
outputDirectory: options.outputDirectory,
|
|
identity,
|
|
archive,
|
|
nodeRelease: release,
|
|
sourceDateEpoch: options.sourceDateEpoch,
|
|
gitCommit: options.gitCommit,
|
|
builder: sshRelayRuntimeBuilderIdentity({ gitCommit: options.gitCommit }),
|
|
runner: sshRelayRuntimeRunnerIdentity(),
|
|
toolchain
|
|
})
|
|
return {
|
|
tuple: options.tuple,
|
|
contentId: identity.contentId,
|
|
archive,
|
|
inspection,
|
|
metadata,
|
|
toolchain,
|
|
durationMs: Number(process.hrtime.bigint() - started) / 1e6
|
|
}
|
|
} catch (error) {
|
|
await rm(options.outputDirectory, { recursive: true, force: true })
|
|
throw error
|
|
} finally {
|
|
if (workDirectoryCreated) {
|
|
await rm(workDirectory, { recursive: true, force: true })
|
|
}
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const result = await buildSshRelayRuntime(parseBuildArguments(process.argv.slice(2)))
|
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
}
|
|
|
|
if (process.argv[1] && resolve(process.argv[1]) === import.meta.filename) {
|
|
main().catch((error) => {
|
|
process.stderr.write(`SSH relay runtime build failed: ${error.message}\n`)
|
|
process.exitCode = 1
|
|
})
|
|
}
|