Files
orca/config/scripts/orcad-windows-prebuild-cache.mjs
T
Neil 1912c90141 Extract subprocess handling into @orca/process-host (#26963)
Extract subprocess execution and process contracts into @orca/process-host with explicit public entry points, independent checks, and one Node 18 compatible CommonJS runtime. Migrate application consumers and integrate package builds, dependency selection, import enforcement, and runtime resource staging.

Preserve daemon startup ownership and profile isolation, and qualify development rebuild recovery and concurrent output publication. Remove unnecessary aliases, test forwarding exports, and a duplicate output scan.

Validation: full typechecks, changed-code quality, package and artifact checks, native Windows builds/types plus 306 runtime tests, and independent native reviews.

Matched full E2E controls on original main and unchanged reviewed head both pass all 17 applicable tests (nine skips). Earlier intermittent E2E failures are documented in the PR; no assertions were weakened. The final combined-tree audit preserves both branches with no import-boundary violations.
2026-10-09 18:32:28 -07:00

183 lines
7.2 KiB
JavaScript

import { createHash } from 'node:crypto'
import { appendFileSync, lstatSync, readFileSync, readdirSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join, resolve } from 'node:path'
import { NODE_RUNTIME_PIN } from '../../src/shared/node-runtime-pin.ts'
import { assertNodePtyPatchApplied } from './build-orcad-prebuilds.mjs'
import {
findPostBaselineNodeApiNames,
findSlotProblems,
MANIFEST_SCHEMA_VERSION,
readManifest,
sha256Of,
slotSourceFiles,
SLOT_NAPI_VERSION
} from './orcad-prebuild-slot-contents.mjs'
const require = createRequire(import.meta.url)
const root = resolve(import.meta.dirname, '../..')
const ownership = require('./node-pty-job-ownership.cjs')
export const WINDOWS_PREBUILD_CACHE_INPUTS = [
'package.json',
'.github/actions/prepare-orcad-prebuilds/action.yml',
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'config/patches/node-pty@1.1.0.patch',
'src/shared/node-runtime-pin.ts',
'config/scripts/build-orcad-prebuilds.mjs',
'config/scripts/orcad-prebuild-slot-contents.mjs',
'config/scripts/orcad-windows-prebuild-cache.mjs',
'config/scripts/node-pty-job-ownership.cjs',
'config/scripts/windows-pe-machine.cjs',
'config/scripts/pinned-node-downloads.mjs',
'config/scripts/update-node-runtime-pin.mjs',
'src/packages/process-host/.gitignore',
'src/packages/process-host/package.json',
'src/packages/process-host/scripts/build-dist.mjs',
'src/packages/process-host/tsconfig.json',
'src/packages/process-host/src/run-process.ts',
'src/packages/process-host/src/spawn-resolution.ts',
'src/packages/process-host/src/process-tree-termination.ts',
'src/packages/process-host/src/process-tree-kill-gate.ts',
'src/packages/process-host/src/spawn-observer.ts',
'src/packages/process-host/src/bounded-output-sink.ts',
'src/packages/process-host/src/growing-byte-buffer.ts',
'src/packages/process-host/src/child-termination-reporter.ts',
'src/packages/process-host/src/process-spec.ts',
'src/packages/process-host/src/windows-command-line.ts',
'src/packages/process-host/src/windows-cmd-shim-resolution.ts',
'src/packages/process-host/src/windows-system-binary.ts'
]
function windowsSlot(platform, arch) {
if (platform !== 'win32' || !['x64', 'arm64'].includes(arch)) {
throw new Error(`Windows prebuild caching requires win32 x64/arm64, got ${platform}/${arch}`)
}
return `win32-${arch}`
}
export function windowsPrebuildCacheIdentity({
repository = root,
platform = process.platform,
arch = process.arch,
imageOS = process.env.ImageOS,
imageVersion = process.env.ImageVersion
} = {}) {
const slot = windowsSlot(platform, arch)
if (!imageOS || !imageVersion) {
throw new Error('Windows prebuild cache needs the hosted ImageOS and ImageVersion')
}
const digest = createHash('sha256').update(JSON.stringify({ slot, imageOS, imageVersion }))
for (const file of WINDOWS_PREBUILD_CACHE_INPUTS) {
digest.update(`\0${file}\0`).update(readFileSync(join(repository, file)))
}
return {
slot,
key: `orcad-windows-prebuild-v1-${slot}-${digest.digest('hex')}`,
path: join(repository, 'out', 'orcad-prebuilds')
}
}
function filesBelow(directory, prefix = '') {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const relative = `${prefix}${entry.name}`
if (entry.isSymbolicLink()) {
throw new Error(`Cached payload cannot contain symlinks: ${relative}`)
}
return entry.isDirectory()
? [`${relative}/`, ...filesBelow(join(directory, entry.name), `${relative}/`)]
: [relative]
})
}
export function validateWindowsPrebuildCache({
prebuildsDir = join(root, 'out', 'orcad-prebuilds'),
sourceDir = dirname(require.resolve('node-pty/package.json')),
platform = process.platform,
arch = process.arch
} = {}) {
const slot = windowsSlot(platform, arch)
assertNodePtyPatchApplied(sourceDir)
readFileSync(join(sourceDir, 'src', 'win', 'conpty.cc'))
ownership.assertNodePtySourceDeniesMsysBreakaway({ nodePtyDir: sourceDir })
for (const directory of [prebuildsDir, join(prebuildsDir, slot)]) {
if (!lstatSync(directory).isDirectory() || lstatSync(directory).isSymbolicLink()) {
throw new Error(`Cached payload must use a real directory: ${directory}`)
}
}
if (!lstatSync(join(prebuildsDir, 'manifest.json')).isFile()) {
throw new Error('Cached manifest must be a regular file')
}
const manifest = readManifest(prebuildsDir)
const sourceVersion = JSON.parse(readFileSync(join(sourceDir, 'package.json'), 'utf8')).version
if (
manifest?.schemaVersion !== MANIFEST_SCHEMA_VERSION ||
manifest.module !== 'node-pty' ||
manifest.version !== sourceVersion ||
manifest.napi !== SLOT_NAPI_VERSION ||
manifest.nodeHeaders !== NODE_RUNTIME_PIN.version
) {
throw new Error(
'Cached Windows prebuild manifest does not match the current module/N-API/headers'
)
}
const entry = manifest.slots?.[slot]
if (
entry?.platform !== platform ||
entry.arch !== arch ||
entry.libc !== 'none' ||
entry.glibc !== null ||
entry.napi !== SLOT_NAPI_VERSION
) {
throw new Error(`Cached ${slot} metadata does not match this Windows host`)
}
const slotDir = join(prebuildsDir, slot)
const sources = slotSourceFiles({ platform, arch, buildDir: slotDir, nodePtyDir: sourceDir })
const expected = sources.map(([relative]) => relative).sort()
const sameFiles = (files, expectedFiles) =>
JSON.stringify(files.sort()) === JSON.stringify(expectedFiles.sort())
if (
!sameFiles(Object.keys(entry.files ?? {}), expected) ||
!sameFiles(filesBelow(slotDir), [...expected, 'conpty/'])
) {
throw new Error(`Cached ${slot} must contain exactly the current ConPTY payload`)
}
const problems = findSlotProblems(manifest, prebuildsDir, [slot])
if (problems.length > 0) {
throw new Error(`Cached ${slot} is invalid: ${problems.join('; ')}`)
}
for (const [relative, source] of sources.filter(([file]) => file !== 'conpty.node')) {
if (sha256Of(source) !== entry.files[relative]) {
throw new Error(`Cached ${slot}/${relative} differs from the current vendored ConPTY payload`)
}
}
for (const relative of expected.filter((file) => file.endsWith('.node'))) {
const newer = findPostBaselineNodeApiNames(readFileSync(join(slotDir, relative)))
if (newer.length > 0) {
throw new Error(
`Cached ${slot}/${relative} imports ${newer.join(', ')}, above N-API ${SLOT_NAPI_VERSION}`
)
}
}
const addon = join(slotDir, 'conpty.node')
ownership.assertRebuiltConptyMatchesArch(addon, arch)
ownership.assertCygwinBreakawayDenied(addon, { dir: slotDir })
return slot
}
if (process.argv[1]?.endsWith('orcad-windows-prebuild-cache.mjs')) {
if (process.argv.includes('--fingerprint')) {
const identity = windowsPrebuildCacheIdentity()
if (process.env.GITHUB_OUTPUT) {
for (const [name, value] of Object.entries(identity)) {
appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`)
}
}
console.log(JSON.stringify(identity))
} else if (process.argv.includes('--validate')) {
console.log(`[orcad-prebuilds] validated cached ${validateWindowsPrebuildCache()}`)
} else {
throw new Error('Usage: orcad-windows-prebuild-cache.mjs --fingerprint | --validate')
}
}