Files
orca/config/scripts/orcad-windows-prebuild-cache.mjs
T
OrcaWinandOrcaWin 2cd3e6d6a3 ci: narrow churny cache keys and move PR-local cache saves to main (#34600)
* ci: narrow native, launcher and packaging cache keys and stop PR-local saves

* ci: hash Rust crate sources, not build outputs, in launcher and hook keys

* ci: fail closed on unlocked native packages and cover transitive key inputs

* ci: resolve the Windows Rust cache scope with a script, not a nested local action

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-27 18:14:29 -07:00

204 lines
8.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 { nativeDependencyLockDigest } from './native-dependency-lock-digest.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')
// The locked node-pty and node-gyp closures stand in for pnpm-lock.yaml and package.json.
export const WINDOWS_PREBUILD_LOCKED_PACKAGES = ['node-pty', 'node-gyp']
const WINDOWS_PREBUILD_PACKAGE_SCRIPTS = ['build:orcad-prebuilds', 'build:packages']
export const WINDOWS_PREBUILD_CACHE_INPUTS = [
'.github/actions/prepare-orcad-prebuilds/action.yml',
'config/patches/node-pty@1.1.0.patch',
'src/shared/node-runtime-pin.ts',
'config/scripts/build-orcad-prebuilds.mjs',
'config/scripts/orcad-prebuild-node-gyp.mjs',
'config/scripts/initialize-windows-native-toolchain.mjs',
'config/scripts/capture-windows-native-environment.cmd',
'config/scripts/orcad-prebuild-slot-contents.mjs',
'config/scripts/orcad-windows-prebuild-cache.mjs',
'config/scripts/native-dependency-lock-digest.mjs',
'config/scripts/build-workspace-packages.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',
'src/packages/process-host/src/windows-tree-kill.ts',
'src/packages/process-host/src/wsl-distro-allowlist.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
} = {}) {
const slot = windowsSlot(platform, arch)
if (!imageOS) {
throw new Error('Windows prebuild cache needs the hosted ImageOS')
}
// ImageOS, not the weekly ImageVersion: --validate re-checks N-API imports, arch and patches on restore.
const { scripts } = JSON.parse(readFileSync(join(repository, 'package.json'), 'utf8'))
const locked = nativeDependencyLockDigest({
repository,
packages: WINDOWS_PREBUILD_LOCKED_PACKAGES
}).digest
const digest = createHash('sha256').update(
JSON.stringify({
slot,
imageOS,
locked,
scripts: WINDOWS_PREBUILD_PACKAGE_SCRIPTS.map((name) => [name, scripts?.[name] ?? null])
})
)
for (const file of WINDOWS_PREBUILD_CACHE_INPUTS) {
digest.update(`\0${file}\0`).update(readFileSync(join(repository, file)))
}
return {
slot,
key: `orcad-windows-prebuild-v2-${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 })
ownership.assertNativeHostJobOwnership(addon)
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')
}
}