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') } }