mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
183 lines
8.2 KiB
TypeScript
183 lines
8.2 KiB
TypeScript
/**
|
|
* Build repository artifacts, type-check the aggregate test and script programs, or both.
|
|
*
|
|
* `--scope artifacts` emits every artifact and binds the client outputs to their public environment;
|
|
* `--scope check` type-checks only the Host and Client aggregate programs against the package
|
|
* declarations already on disk; the default runs both step sets as one dependency graph, so the
|
|
* aggregate checks overlap the remaining artifact steps. `--scope typecheck` (`pnpm run typecheck`)
|
|
* type-checks every package project and both aggregate programs; beside Host compilation it writes
|
|
* the Typert declarations that Client compilation imports, without bundling.
|
|
*/
|
|
|
|
import { rmSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
import { parseArgs } from 'node:util'
|
|
import {
|
|
CLIENT_BUILD_RECORD_PATH,
|
|
CLIENT_BUILD_PROFILE_SELECTOR,
|
|
clientBuildProcessEnvironment,
|
|
repositoryClientBuildEnvironment,
|
|
resolveClientBuildEnvironment,
|
|
writeClientBuildRecord,
|
|
} from './client-build-environment.ts'
|
|
import { aggregateBuildArgs, aggregateCheckArgs, contractSteps, libraryBuildSteps } from './compile-referenced-projects.ts'
|
|
import type { LibraryStep } from './compile-referenced-projects.ts'
|
|
import { pnpmCommand } from './release/process.ts'
|
|
import { defaultParallelism, describeDefaultParallelism } from './parallelism.ts'
|
|
import { runAggregate } from './run-gates.ts'
|
|
import type { Gate } from './run-gates.ts'
|
|
|
|
/**
|
|
* Steps one build invocation runs: every artifact and check, only artifacts, only aggregate checks,
|
|
* or every type check with the generated Typert declarations it needs.
|
|
*/
|
|
export type BuildScope = 'complete' | 'artifacts' | 'check' | 'typecheck'
|
|
|
|
/** Parsed command-line selection of one build invocation. */
|
|
export interface BuildArguments {
|
|
readonly scope: BuildScope
|
|
/** Client build profile requested by `--profile`; `complete` and `artifacts` scopes only. */
|
|
readonly profile: string | undefined
|
|
}
|
|
|
|
/**
|
|
* Parse the build command line.
|
|
* @param args - Arguments after the script path.
|
|
* @returns The selected scope and requested client build profile.
|
|
* @throws If an option is unknown, the scope is not `artifacts`, `check`, or `typecheck`,
|
|
* or `--profile` accompanies `--scope check` or `--scope typecheck`.
|
|
*/
|
|
export function parseBuildArguments(args: readonly string[]): BuildArguments {
|
|
const { values } = parseArgs({
|
|
args: [...args],
|
|
options: {
|
|
profile: { type: 'string' },
|
|
scope: { type: 'string' },
|
|
},
|
|
allowPositionals: false,
|
|
})
|
|
if (values.scope !== undefined && values.scope !== 'artifacts' && values.scope !== 'check' && values.scope !== 'typecheck') {
|
|
throw new Error(`build: --scope must be artifacts, check, or typecheck, got ${JSON.stringify(values.scope)}; omit it for the complete build`)
|
|
}
|
|
const scope: BuildScope = values.scope ?? 'complete'
|
|
if ((scope === 'check' || scope === 'typecheck') && values.profile !== undefined) {
|
|
throw new Error(`build: --profile selects artifact values and does not apply to --scope ${scope}`)
|
|
}
|
|
return { scope, profile: values.profile }
|
|
}
|
|
|
|
/**
|
|
* Construct the build steps of one scope as a gate dependency graph.
|
|
* Artifact steps generate Typert from source before Host compilation, then bundle Host,
|
|
* Desktop, and Client artifacts before Web. Native output also precedes Host compilation.
|
|
* The typecheck scope generates Typert before compiling Host and Client packages; it writes no bundle. In the complete and typecheck
|
|
* scopes each aggregate check waits only for its own face's package compilation; alone, the
|
|
* checks have no predecessors and read whatever declarations already exist. The typecheck scope
|
|
* checks aggregates in build mode, which skips a program whose tsbuildinfo is current; the other
|
|
* scopes use project mode, which compiles no reference.
|
|
* @param root - Repository root containing the compiler and bundle configurations.
|
|
* @param scope - Steps to include.
|
|
* @param environment - Environment overlay applied to every step; the parent value of a key mapped to `undefined` is removed.
|
|
* @returns The gate graph in report order.
|
|
* @throws If an aggregate configuration is invalid.
|
|
*/
|
|
export function buildGates(
|
|
root: string,
|
|
scope: BuildScope,
|
|
environment: Readonly<Record<string, string | undefined>> = {},
|
|
): Gate[] {
|
|
const env = { ...environment }
|
|
const gates: Gate[] = []
|
|
if (scope === 'complete' || scope === 'artifacts') {
|
|
gates.push(pnpmGate('native', 'native addon', ['run', 'build:native-system'], [], env))
|
|
for (const step of libraryBuildSteps(root, 'all')) {
|
|
gates.push(libraryGate(step, step.id === 'host-src' ? ['native', ...step.needs] : step.needs, env))
|
|
}
|
|
gates.push(pnpmGate('web', 'Web build', ['run', 'build:web'], ['client-bundle'], env))
|
|
}
|
|
if (scope === 'typecheck') {
|
|
for (const step of contractSteps(root, 'all')) gates.push(libraryGate(step, step.needs, env))
|
|
}
|
|
if (scope !== 'artifacts') {
|
|
for (const face of ['host', 'client'] as const) {
|
|
gates.push({
|
|
id: `${face}-check`,
|
|
label: `${face} aggregate check`,
|
|
displayCommand: `tsc ${scope === 'typecheck' ? '-b' : '-p'} tsconfig.${face}.json`,
|
|
command: process.execPath,
|
|
args: scope === 'typecheck' ? aggregateBuildArgs(face) : aggregateCheckArgs(face),
|
|
needs: scope === 'check' ? [] : [`${face}-src`],
|
|
env,
|
|
})
|
|
}
|
|
}
|
|
return gates
|
|
}
|
|
|
|
function libraryGate(step: LibraryStep, needs: readonly string[], env: Record<string, string | undefined>): Gate {
|
|
return {
|
|
id: step.id,
|
|
label: step.label,
|
|
displayCommand: step.displayCommand,
|
|
command: step.command,
|
|
args: [...step.args],
|
|
needs: [...needs],
|
|
env,
|
|
}
|
|
}
|
|
|
|
function pnpmGate(
|
|
id: string,
|
|
label: string,
|
|
args: readonly string[],
|
|
needs: readonly string[],
|
|
env: Record<string, string | undefined>,
|
|
): Gate {
|
|
const [command, ...prefix] = pnpmCommand()
|
|
return { id, label, displayCommand: `pnpm ${args.join(' ')}`, command, args: [...prefix, ...args], needs: [...needs], env }
|
|
}
|
|
|
|
/**
|
|
* Express a complete child environment as an overlay on the parent environment.
|
|
* @param parent - Environment the gate executor merges the overlay into.
|
|
* @param child - Complete environment every step must observe.
|
|
* @returns `child` plus an `undefined` entry for each parent key it omits.
|
|
*/
|
|
function environmentOverlay(parent: NodeJS.ProcessEnv, child: NodeJS.ProcessEnv): Record<string, string | undefined> {
|
|
return { ...Object.fromEntries(Object.keys(parent).map(name => [name, undefined])), ...child }
|
|
}
|
|
|
|
async function main(): Promise<number> {
|
|
// tsdown.config.ts loads only through Node type stripping (`--config-loader native`); this names the cause before tsdown fails.
|
|
if (!process.features.typescript) {
|
|
throw new Error('build: Node.js TypeScript type stripping is unavailable in this Node.js process; remove --no-experimental-strip-types from NODE_OPTIONS or use a Node.js build with TypeScript support')
|
|
}
|
|
const { scope, profile } = parseBuildArguments(process.argv.slice(2))
|
|
const root = resolve(import.meta.dirname, '..')
|
|
const name = scope === 'complete' ? 'build' : scope === 'typecheck' ? 'typecheck' : `build:${scope}`
|
|
const budget = defaultParallelism()
|
|
const workers = (gates: readonly Gate[]) => ({
|
|
workers: Math.min(gates.length, budget),
|
|
source: describeDefaultParallelism(),
|
|
})
|
|
if (scope === 'check' || scope === 'typecheck') {
|
|
const gates = buildGates(root, scope)
|
|
return runAggregate(name, gates, workers(gates))
|
|
}
|
|
|
|
const repositoryEnvironment = repositoryClientBuildEnvironment(root, process.env)
|
|
const clientEnvironment = resolveClientBuildEnvironment(repositoryEnvironment, profile ?? process.env[CLIENT_BUILD_PROFILE_SELECTOR])
|
|
const environment = environmentOverlay(process.env, clientBuildProcessEnvironment(process.env, clientEnvironment))
|
|
rmSync(resolve(root, CLIENT_BUILD_RECORD_PATH), { force: true })
|
|
const gates = buildGates(root, scope, environment)
|
|
const exitCode = await runAggregate(name, gates, workers(gates))
|
|
if (exitCode !== 0) return exitCode
|
|
const record = writeClientBuildRecord(root, clientEnvironment)
|
|
console.log(
|
|
`build: recorded ${String(record.artifacts.fileCount)} client artifact(s) with ${String(Object.keys(record.environment).length)} public value(s)`,
|
|
)
|
|
return 0
|
|
}
|
|
|
|
if (import.meta.main) process.exitCode = await main()
|