mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 01:27:35 +00:00
* fix(orcad): materialize the app's agent-browser driver into the local slot Replaces #34111's ORCA_AGENT_BROWSER_DRIVER hand-off, Windows profile copy and claimed snapshot store. Packaging now references the app's own resources/agent-browser-<target> from the orcad template manifest (packagedBrowser, like packagedRipgrep), so the materialized slot carries it and every local orcad launch finds it beside orcad-server.js. macOS signs the driver before hashing; Windows reseals it after SignPath. The resolver now also tries the musl driver name on Linux. Refs #33846 * build(orcad): let a checkout keep template drivers with ORCAD_OMIT_AGENT_BROWSER=0
241 lines
9.0 KiB
JavaScript
241 lines
9.0 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { createHash } from 'node:crypto'
|
|
import {
|
|
chmodSync,
|
|
copyFileSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync
|
|
} from 'node:fs'
|
|
import { dirname, join, resolve } from 'node:path'
|
|
import {
|
|
ORCAD_NODE_PTY_DIR,
|
|
ORCAD_NODE_RUNTIME_MARKER_FILENAME,
|
|
ORCAD_SERVER_TARGET_FILENAME,
|
|
ORCAD_TEMPLATE_MANIFEST_FILENAME,
|
|
ORCAD_TEMPLATE_TARGETS_DIR,
|
|
orcadNodePtySlotFiles,
|
|
orcadTemplateCommonFilenames,
|
|
orcadTemplateTargetFilenames
|
|
} from '../../src/shared/orcad-artifacts.ts'
|
|
import { orcadAgentBrowserNativeName } from '../../src/shared/orcad-agent-browser-name.ts'
|
|
import {
|
|
COMPAT_SERVER_TARGET_BASES,
|
|
COMPAT_SERVER_TARGETS,
|
|
ORCAD_TEMPLATE_TARGETS,
|
|
pinnedNodeRuntimeAsset
|
|
} from '../../src/shared/node-runtime-pin.ts'
|
|
import { ORCAD_PREBUILDS_DIR } from './build-orcad-prebuilds.mjs'
|
|
import {
|
|
COMPAT_SLOT_ADDONS,
|
|
findCompatAddonGaps,
|
|
findSlotProblems,
|
|
readManifest
|
|
} from './orcad-prebuild-slot-contents.mjs'
|
|
import { runProcessSync } from '@orca/process-host'
|
|
import { verifyPackagedOrcadTemplate } from './verify-packaged-orcad-template.cjs'
|
|
import { readOrcadMobileWebAssetFilenames } from './orcad-mobile-web-bundle.mjs'
|
|
import { assertNotDarkBuild, darkBuildMarkers } from '../dark-build-switches.cjs'
|
|
|
|
const root = resolve(import.meta.dirname, '../..')
|
|
const outputDir = join(root, 'out', 'orcad-template')
|
|
const buildDir = join(root, 'out', '.orcad-template-build')
|
|
|
|
function isExecutable(filename) {
|
|
return /(?:^|\/)(?:rg|spawn-helper)$/.test(filename)
|
|
}
|
|
|
|
function copy(source, destination, executable = false) {
|
|
mkdirSync(dirname(destination), { recursive: true })
|
|
copyFileSync(source, destination)
|
|
if (executable && process.platform !== 'win32') {
|
|
chmodSync(destination, 0o755)
|
|
}
|
|
}
|
|
|
|
function sha256(path) {
|
|
return createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
}
|
|
|
|
/** One full package per target; each needs that target's node-pty slot in out/orcad-prebuilds. */
|
|
function buildTargetPackage(target) {
|
|
const packageDir = join(buildDir, target, 'orcad')
|
|
const result = runProcessSync({
|
|
program: process.execPath,
|
|
args: [
|
|
join(root, 'config/scripts/build-orcad-node.mjs'),
|
|
'--target',
|
|
target,
|
|
'--out-dir',
|
|
packageDir
|
|
],
|
|
cwd: root,
|
|
// Desktop template omits optional ~10MB browsers per target; packaging references the app's own copy instead.
|
|
// A checkout has no app copy, so ORCAD_OMIT_AGENT_BROWSER=0 keeps drivers for local Chromium serve.
|
|
env: { ...process.env, ORCAD_OMIT_AGENT_BROWSER: process.env.ORCAD_OMIT_AGENT_BROWSER ?? '1' },
|
|
stdio: 'inherit',
|
|
timeoutMs: null
|
|
})
|
|
if (result.code !== 0) {
|
|
throw new Error(`orcad ${target} package build failed with exit ${result.code ?? 'unknown'}`)
|
|
}
|
|
return packageDir
|
|
}
|
|
|
|
function stageTarget(target, packageDir) {
|
|
const destination = join(outputDir, ORCAD_TEMPLATE_TARGETS_DIR, target)
|
|
const files = {}
|
|
for (const filename of orcadTemplateTargetFilenames(target)) {
|
|
const staged = join(destination, ...filename.split('/'))
|
|
copy(join(packageDir, ...filename.split('/')), staged, isExecutable(filename))
|
|
files[filename] = sha256(staged)
|
|
}
|
|
const browserName = orcadAgentBrowserNativeName(
|
|
target.split('-')[0],
|
|
target.split('-')[1],
|
|
target.endsWith('-musl') ? 'musl' : 'glibc'
|
|
)
|
|
const browserSource = join(packageDir, browserName)
|
|
if (!existsSync(browserSource)) {
|
|
return { files }
|
|
}
|
|
const browserDestination = join(destination, browserName)
|
|
copy(browserSource, browserDestination, true)
|
|
return { files, browserName, browserSha256: sha256(browserDestination) }
|
|
}
|
|
|
|
// Allow partial CI --targets only; shipped templates require every target.
|
|
export function requestedTemplateTargets(argv = process.argv) {
|
|
const index = argv.indexOf('--targets')
|
|
if (index === -1) {
|
|
return ORCAD_TEMPLATE_TARGETS
|
|
}
|
|
const targets = (argv[index + 1] ?? '').split(',').filter(Boolean)
|
|
const unknown = targets.filter((target) => !ORCAD_TEMPLATE_TARGETS.includes(target))
|
|
if (targets.length === 0 || unknown.length > 0) {
|
|
throw new Error(
|
|
`--targets needs a comma-separated subset of ${ORCAD_TEMPLATE_TARGETS.join(',')}; got ${argv[index + 1] ?? 'nothing'}`
|
|
)
|
|
}
|
|
return [...new Set(targets)]
|
|
}
|
|
|
|
// Compat target reuses base package with slot addons/runtime marker swapped; omit missing slots so rung B reports unavailable.
|
|
function stageCompatTarget(compat, basePackageDir) {
|
|
const problems = findSlotProblems(readManifest(ORCAD_PREBUILDS_DIR), ORCAD_PREBUILDS_DIR, [
|
|
compat
|
|
])
|
|
if (problems.length > 0) {
|
|
process.stdout.write(
|
|
`[build-orcad-template] skipping compat target ${compat}: ${problems.join('; ')}\n`
|
|
)
|
|
return null
|
|
}
|
|
const destination = join(outputDir, ORCAD_TEMPLATE_TARGETS_DIR, compat)
|
|
const slotFiles = new Map([
|
|
...orcadNodePtySlotFiles(compat).map((file) => [
|
|
`${ORCAD_NODE_PTY_DIR}/build/Release/${file}`,
|
|
join(ORCAD_PREBUILDS_DIR, compat, ...file.split('/'))
|
|
]),
|
|
...Object.entries(COMPAT_SLOT_ADDONS).map(([file, shipped]) => [
|
|
shipped,
|
|
join(ORCAD_PREBUILDS_DIR, compat, ...file.split('/'))
|
|
])
|
|
])
|
|
// Why fatal: the base binary would pass every check here and fail only on a compat host.
|
|
const gaps = findCompatAddonGaps(orcadTemplateTargetFilenames(compat), slotFiles)
|
|
if (gaps.length > 0) {
|
|
throw new Error(`compat target ${compat} would ship base-target addons: ${gaps.join(', ')}`)
|
|
}
|
|
const files = {}
|
|
for (const filename of orcadTemplateTargetFilenames(compat)) {
|
|
const staged = join(destination, ...filename.split('/'))
|
|
if (filename === ORCAD_SERVER_TARGET_FILENAME) {
|
|
mkdirSync(dirname(staged), { recursive: true })
|
|
writeFileSync(staged, `${compat}\n`)
|
|
} else if (filename === ORCAD_NODE_RUNTIME_MARKER_FILENAME) {
|
|
mkdirSync(dirname(staged), { recursive: true })
|
|
writeFileSync(staged, `${pinnedNodeRuntimeAsset(compat).executableSha256}\n`)
|
|
} else {
|
|
const source = slotFiles.get(filename) ?? join(basePackageDir, ...filename.split('/'))
|
|
copy(source, staged, isExecutable(filename))
|
|
}
|
|
files[filename] = sha256(staged)
|
|
}
|
|
return { files }
|
|
}
|
|
|
|
/**
|
|
* `--dark-test-template` lets a dark switch through for test servers only; the template then
|
|
* carries the switch's marker, which packaging refuses.
|
|
*/
|
|
export function assertTemplateBuildAllowed(argv = process.argv, env = process.env) {
|
|
if (!argv.includes('--dark-test-template')) {
|
|
// The template is what releases ship as the server; a dark build must never become one.
|
|
assertNotDarkBuild([], env)
|
|
}
|
|
}
|
|
|
|
function sameBytes(left, right) {
|
|
return sha256(left) === sha256(right)
|
|
}
|
|
|
|
async function main() {
|
|
assertTemplateBuildAllowed()
|
|
const templateTargets = requestedTemplateTargets()
|
|
rmSync(buildDir, { recursive: true, force: true })
|
|
const packages = Object.fromEntries(
|
|
templateTargets.map((target) => [target, buildTargetPackage(target)])
|
|
)
|
|
rmSync(outputDir, { recursive: true, force: true })
|
|
mkdirSync(outputDir, { recursive: true })
|
|
const [firstTarget] = templateTargets
|
|
// Every package carries the same page: its manifest is a common file, checked byte for byte below.
|
|
const commonArtifacts = orcadTemplateCommonFilenames(
|
|
readOrcadMobileWebAssetFilenames(packages[firstTarget])
|
|
)
|
|
for (const filename of commonArtifacts) {
|
|
const source = join(packages[firstTarget], ...filename.split('/'))
|
|
// Why check every target: the template keeps one copy, so a per-target difference would ship wrong bytes.
|
|
for (const target of templateTargets) {
|
|
if (!sameBytes(source, join(packages[target], ...filename.split('/')))) {
|
|
throw new Error(`${filename} differs between ${firstTarget} and ${target} packages`)
|
|
}
|
|
}
|
|
copy(source, join(outputDir, ...filename.split('/')), isExecutable(filename))
|
|
}
|
|
const targets = Object.fromEntries(
|
|
templateTargets.map((target) => [target, stageTarget(target, packages[target])])
|
|
)
|
|
// A partial CI template stages a compat target only beside its base target.
|
|
for (const compat of COMPAT_SERVER_TARGETS.filter((candidate) =>
|
|
templateTargets.includes(COMPAT_SERVER_TARGET_BASES[candidate])
|
|
)) {
|
|
const staged = stageCompatTarget(compat, packages[COMPAT_SERVER_TARGET_BASES[compat]])
|
|
if (staged) {
|
|
targets[compat] = staged
|
|
}
|
|
}
|
|
const commonSha256 = Object.fromEntries(
|
|
commonArtifacts.map((filename) => [filename, sha256(join(outputDir, filename))])
|
|
)
|
|
writeFileSync(
|
|
join(outputDir, ORCAD_TEMPLATE_MANIFEST_FILENAME),
|
|
`${JSON.stringify({ schemaVersion: 3, commonSha256, targets }, null, 2)}\n`
|
|
)
|
|
verifyPackagedOrcadTemplate(join(root, 'out'), templateTargets)
|
|
// Outside the manifest, so deploys never copy it; it marks the whole template as test-only.
|
|
for (const marker of darkBuildMarkers()) {
|
|
writeFileSync(join(outputDir, marker.fileName), marker.text)
|
|
}
|
|
rmSync(buildDir, { recursive: true, force: true })
|
|
process.stdout.write(`[build-orcad-template] ok — ${Object.keys(targets).length} targets\n`)
|
|
}
|
|
|
|
if (process.argv[1]?.endsWith('build-orcad-template.mjs')) {
|
|
await main()
|
|
}
|