Files
orca/config/scripts/headless-detector-compiler-cache.mjs
T
Neil a824fb74ab Reuse the headless detector compiler without installing full dependencies (#24895)
* Reuse the headless detector compiler without full dependency setup

* Keep optional compiler-cache saves from failing cache warming
2026-10-02 16:56:08 -07:00

161 lines
5.1 KiB
JavaScript

import { createHash } from 'node:crypto'
import {
appendFileSync,
cpSync,
existsSync,
lstatSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
symlinkSync,
writeFileSync
} from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const ROOT = resolve(import.meta.dirname, '../..')
function inventory(directory, prefix = '') {
if (lstatSync(directory).isSymbolicLink()) {
throw new Error('Compiler cache directory is a symlink')
}
return readdirSync(directory)
.flatMap((name) => {
const path = join(directory, name)
const file = `${prefix}${name}`
const stat = lstatSync(path)
if (stat.isSymbolicLink()) {
throw new Error('Compiler cache contains a symlink')
}
if (stat.isDirectory()) {
return inventory(path, `${file}/`)
}
if (!stat.isFile()) {
throw new Error('Compiler cache contains a special file')
}
return [{ file, sha256: createHash('sha256').update(readFileSync(path)).digest('hex') }]
})
.sort((a, b) => a.file.localeCompare(b.file))
}
export function compilerCacheIdentity({
policyHash = process.env.COMPILER_POLICY_HASH,
cacheRoot = process.env.RUNNER_TEMP,
platform = process.platform,
arch = process.arch,
node = process.version
} = {}) {
if (!policyHash || !cacheRoot) {
throw new Error('Compiler cache requires policy hash and cache root')
}
return {
key: `headless-compiler-v1-${platform}-${arch}-${node}-${policyHash}`,
path: join(cacheRoot, 'headless-detector-compiler')
}
}
export function packCompilerCache({ root = ROOT, identity = compilerCacheIdentity() } = {}) {
const require = createRequire(join(root, 'package.json'))
const esbuildDir = dirname(require.resolve('esbuild/package.json'))
const nativeName = `@esbuild/${process.platform}-${process.arch}`
const nativeDir = dirname(require.resolve(`${nativeName}/package.json`, { paths: [esbuildDir] }))
rmSync(identity.path, { recursive: true, force: true })
mkdirSync(join(identity.path, 'node_modules', '@esbuild'), { recursive: true })
cpSync(esbuildDir, join(identity.path, 'node_modules', 'esbuild'), {
recursive: true,
dereference: true
})
cpSync(nativeDir, join(identity.path, 'node_modules', nativeName), {
recursive: true,
dereference: true
})
writeFileSync(
join(identity.path, 'manifest.json'),
JSON.stringify({
key: identity.key,
node: process.version,
version: require('esbuild').version,
files: inventory(identity.path)
})
)
}
export async function activateCompilerCache({
root = ROOT,
identity = compilerCacheIdentity()
} = {}) {
const dependencies = join(root, 'node_modules')
let created = false
try {
if (existsSync(dependencies)) {
throw new Error('Compiler activation requires an empty dependency tree')
}
const files = inventory(identity.path).filter((row) => row.file !== 'manifest.json')
const manifest = JSON.parse(readFileSync(join(identity.path, 'manifest.json'), 'utf8'))
if (
manifest.key !== identity.key ||
manifest.node !== process.version ||
JSON.stringify(files) !== JSON.stringify(manifest.files)
) {
throw new Error('Compiler cache identity or contents differ')
}
mkdirSync(join(dependencies, '@esbuild'), { recursive: true })
created = true
for (const name of ['esbuild', `@esbuild/${process.platform}-${process.arch}`]) {
symlinkSync(join(identity.path, 'node_modules', name), join(dependencies, name), 'dir')
}
const require = createRequire(join(root, 'package.json'))
const esbuild = require('esbuild')
if (esbuild.version !== manifest.version) {
throw new Error('Compiler API version differs')
}
await esbuild.build({
stdin: { contents: 'export const value = 1' },
write: false,
logLevel: 'silent'
})
return { available: true }
} catch (error) {
if (created) {
rmSync(dependencies, { recursive: true, force: true })
}
return { available: false, reason: String(error) }
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const phase = process.argv[2]
const identity =
phase === 'identity'
? compilerCacheIdentity()
: {
key: process.env.COMPILER_CACHE_KEY,
path: process.env.COMPILER_CACHE_PATH
}
if (!identity.key || !identity.path) {
throw new Error('Compiler cache identity required')
}
const output = (values) => {
for (const [name, value] of Object.entries(values)) {
appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`)
}
}
if (phase === 'identity') {
output(identity)
} else if (phase === 'pack') {
packCompilerCache({ identity })
} else if (phase === 'activate') {
const result = await activateCompilerCache({ identity })
output({ available: result.available })
console.log(
result.available
? 'Validated headless compiler cache'
: `Use normal dependency install: ${result.reason}`
)
} else {
throw new Error('Expected identity, pack, or activate')
}
}