mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
746 lines
26 KiB
TypeScript
746 lines
26 KiB
TypeScript
#!/usr/bin/env node
|
|
// Builds OpenClaw packages and plugin SDK artifacts with cache-aware orchestration.
|
|
|
|
import { spawnSync, type SpawnSyncOptions } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { performance } from "node:perf_hooks";
|
|
import { resolveNodeRuntimeExecutable } from "../src/infra/node-runtime-executable.ts";
|
|
import {
|
|
finalizeBuildStepCache,
|
|
resolveBuildStepCacheState,
|
|
restoreBuildStepCacheOutputs,
|
|
type BuildCacheStep,
|
|
} from "./lib/build-artifact-cache.mts";
|
|
import { readCurrentGitCommit, resolveBuildIdentityEnvironment } from "./lib/build-identity.mts";
|
|
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
|
import {
|
|
distArtifactEntryArgs,
|
|
withDistArtifactOwnership,
|
|
} from "./lib/dist-artifact-ownership.mts";
|
|
import { formatDurationElapsed } from "./lib/format-duration.mts";
|
|
import { resolveLiveManagedGatewayDistFence } from "./lib/live-gateway-dist-fence.mts";
|
|
import {
|
|
BUILD_STAMP_FILE,
|
|
RUNTIME_POSTBUILD_STAMP_FILE,
|
|
writeBuildStamp,
|
|
writeRuntimePostBuildStamp,
|
|
} from "./lib/local-build-metadata.mts";
|
|
import { runManagedCommand } from "./lib/managed-child-process.mts";
|
|
import type { MemoryLimitParams } from "./lib/process-memory.mts";
|
|
import { captureRunNodeInputState } from "./lib/run-node-input-state.mts";
|
|
import { preflightInstalledSourceArtifacts } from "./lib/source-update-artifact-preflight.mts";
|
|
import {
|
|
TSDOWN_PACKAGE_CONFIG_GROUP,
|
|
TSDOWN_UNIFIED_CONFIG_GROUP,
|
|
} from "./lib/tsdown-config-groups.mts";
|
|
import {
|
|
TSDOWN_PACKAGE_OUTPUT_ROOTS,
|
|
tsdownPackageOutputRoot,
|
|
} from "./lib/tsdown-output-roots.mts";
|
|
import { resolvePnpmRunner } from "./pnpm-runner.mts";
|
|
import {
|
|
TSDOWN_MAX_OLD_SPACE_MB_ENV,
|
|
TSDOWN_DECLARATION_EXTENSIONS,
|
|
TSDOWN_DECLARATION_TOOL_INPUTS,
|
|
TSDOWN_PACKAGES_CACHE_INPUT,
|
|
listTsdownOutputRoots,
|
|
resolveTsdownBuildPlan,
|
|
} from "./tsdown-build.mts";
|
|
|
|
const nodeBin = resolveNodeRuntimeExecutable() ?? process.execPath;
|
|
|
|
export type BuildAllStep = BuildCacheStep &
|
|
(
|
|
| { kind: "pnpm"; args?: never; pnpmArgs: string[] }
|
|
| { kind?: "node"; args: string[]; pnpmArgs?: never }
|
|
);
|
|
|
|
type BuildAllTiming = { label: string; durationMs: number; status: string };
|
|
|
|
export type BuildAllResult = {
|
|
exitCode: number;
|
|
timings: BuildAllTiming[];
|
|
admissionRefused?: true;
|
|
};
|
|
type BuildAllStepParams = {
|
|
platform?: NodeJS.Platform;
|
|
env?: NodeJS.ProcessEnv;
|
|
nodeExecPath?: string;
|
|
npmExecPath?: string;
|
|
comSpec?: string;
|
|
deferIsolatedAssets?: boolean;
|
|
};
|
|
const RUN_NODE_SKIP_DTS_BUILD_ENV = "OPENCLAW_RUN_NODE_SKIP_DTS_BUILD";
|
|
const TSDOWN_AI_OUTPUT_ROOT = tsdownPackageOutputRoot("ai");
|
|
const TSDOWN_MAIN_PACKAGE_OUTPUT_ROOTS = TSDOWN_PACKAGE_OUTPUT_ROOTS.filter(
|
|
(root) => root !== TSDOWN_AI_OUTPUT_ROOT,
|
|
);
|
|
const declarationCacheOutputs = (roots: string[]) =>
|
|
roots.map((root) => ({ path: root, extensions: TSDOWN_DECLARATION_EXTENSIONS }));
|
|
const tsxScript = (script: string, ...args: string[]) => ["--import", "tsx", script, ...args];
|
|
const nodeStep = (label: string, args: string[]): Extract<BuildAllStep, { kind?: "node" }> => ({
|
|
label,
|
|
kind: "node",
|
|
args,
|
|
});
|
|
const tsxStep = (label: string, script: string, ...args: string[]) =>
|
|
nodeStep(label, tsxScript(script, ...args));
|
|
const PNPM_STEP_NODE_FALLBACKS = new Map([
|
|
["plugins:assets:build", tsxScript("scripts/bundled-plugin-assets.mts", "--phase", "build")],
|
|
["plugins:assets:copy", tsxScript("scripts/bundled-plugin-assets.mts", "--phase", "copy")],
|
|
["ui:build", ["scripts/ui.js", "build"]],
|
|
]);
|
|
export const BUILD_ALL_STEPS: BuildAllStep[] = [
|
|
nodeStep("native-protocol", ["scripts/prepare-native-protocol.mjs"]),
|
|
nodeStep("clean:dist", [
|
|
"-e",
|
|
'require("node:fs").rmSync("dist", { recursive: true, force: true })',
|
|
]),
|
|
{ label: "plugins:assets:build", kind: "pnpm", pnpmArgs: ["plugins:assets:build"] },
|
|
tsxStep("tsdown", "scripts/tsdown-build.mts"),
|
|
{
|
|
...tsxStep("tsdown-ai", "scripts/tsdown-build.mts", "--config", "tsdown.ai.config.ts"),
|
|
cache: {
|
|
inputs: [
|
|
...TSDOWN_DECLARATION_TOOL_INPUTS,
|
|
"tsdown.ai.config.ts",
|
|
TSDOWN_PACKAGES_CACHE_INPUT,
|
|
],
|
|
outputs: declarationCacheOutputs([TSDOWN_AI_OUTPUT_ROOT]),
|
|
restore: "always",
|
|
runOnHit: {
|
|
env: { OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1" },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
...tsxStep(
|
|
"tsdown-packages",
|
|
"scripts/tsdown-build.mts",
|
|
"--config",
|
|
"tsdown.config.ts",
|
|
"--filter",
|
|
TSDOWN_PACKAGE_CONFIG_GROUP,
|
|
),
|
|
cache: {
|
|
inputs: [...TSDOWN_DECLARATION_TOOL_INPUTS, "tsdown.config.ts", TSDOWN_PACKAGES_CACHE_INPUT],
|
|
outputs: declarationCacheOutputs(TSDOWN_MAIN_PACKAGE_OUTPUT_ROOTS),
|
|
restore: "always",
|
|
runOnHit: {
|
|
env: { OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1" },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
...tsxStep(
|
|
"tsdown-unified",
|
|
"scripts/tsdown-build.mts",
|
|
"--config",
|
|
"tsdown.config.ts",
|
|
"--filter",
|
|
TSDOWN_UNIFIED_CONFIG_GROUP,
|
|
),
|
|
env: { OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1" },
|
|
},
|
|
{
|
|
...tsxStep("write-unified-entry-dts", "scripts/write-unified-entry-dts.ts"),
|
|
env: { OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "0" },
|
|
},
|
|
tsxStep("external-plugins:local-dist", "scripts/build-external-plugin-local-dist.mts"),
|
|
tsxStep("check-cli-bootstrap-imports", "scripts/check-cli-bootstrap-imports.mts"),
|
|
{
|
|
label: "plugins:assets:copy",
|
|
kind: "pnpm",
|
|
pnpmArgs: ["plugins:assets:copy"],
|
|
},
|
|
tsxStep("runtime-postbuild", "scripts/runtime-postbuild.mts"),
|
|
tsxStep("build-stamp", "scripts/build-stamp.mts"),
|
|
tsxStep("runtime-postbuild-stamp", "scripts/runtime-postbuild-stamp.mts"),
|
|
{
|
|
...tsxStep("write-plugin-sdk-entry-dts", "scripts/write-plugin-sdk-entry-dts.ts"),
|
|
env: { OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "0" },
|
|
},
|
|
tsxStep("check-plugin-sdk-exports", "scripts/check-plugin-sdk-exports.mts"),
|
|
{
|
|
label: "ui:build",
|
|
kind: "pnpm",
|
|
pnpmArgs: ["ui:build"],
|
|
// No build-all cache: ui/vite.config.ts derives the Control UI build ID
|
|
// from package.json, git HEAD, and OPENCLAW_CONTROL_UI_BUILD_ID env, so a
|
|
// file-input signature cannot exactly invalidate generated assets and a
|
|
// warm hit could restore stale service-worker/app cache metadata.
|
|
cache: undefined,
|
|
},
|
|
tsxStep("write-build-info", "scripts/write-build-info.ts"),
|
|
{
|
|
...tsxStep("write-cli-startup-metadata", "scripts/write-cli-startup-metadata.ts"),
|
|
cache: {
|
|
inputs: [
|
|
"scripts/write-cli-startup-metadata.ts",
|
|
"scripts/lib/cli-startup-root-help-bundle.ts",
|
|
],
|
|
outputs: ["dist/cli-startup-metadata.json"],
|
|
restore: "always",
|
|
runOnHit: { finalize: "refresh" },
|
|
},
|
|
},
|
|
];
|
|
|
|
const RUNTIME_SETUP_STEP_LABELS = [
|
|
"external-plugins:local-dist",
|
|
"check-cli-bootstrap-imports",
|
|
] as const;
|
|
const RUNTIME_FINALIZE_STEP_LABELS = [
|
|
"runtime-postbuild",
|
|
"build-stamp",
|
|
"runtime-postbuild-stamp",
|
|
] as const;
|
|
const RUNTIME_STEP_LABELS = [...RUNTIME_SETUP_STEP_LABELS, ...RUNTIME_FINALIZE_STEP_LABELS];
|
|
const ASSET_RUNTIME_STEP_LABELS = [
|
|
"plugins:assets:build",
|
|
"tsdown",
|
|
...RUNTIME_SETUP_STEP_LABELS,
|
|
// Copy after compiler cleanup, before postbuild records the generated asset inventory.
|
|
"plugins:assets:copy",
|
|
...RUNTIME_FINALIZE_STEP_LABELS,
|
|
];
|
|
const BUILD_METADATA_STEP_LABELS = ["write-build-info", "write-cli-startup-metadata"] as const;
|
|
const SDK_DECLARATION_STEP_LABELS = [
|
|
"write-plugin-sdk-entry-dts",
|
|
"check-plugin-sdk-exports",
|
|
] as const;
|
|
const FINAL_BUILD_ARTIFACTS_STEP_LABELS = [
|
|
...SDK_DECLARATION_STEP_LABELS,
|
|
"ui:build",
|
|
...BUILD_METADATA_STEP_LABELS,
|
|
] as const;
|
|
const CI_ARTIFACT_STEP_LABELS = [
|
|
"native-protocol",
|
|
...ASSET_RUNTIME_STEP_LABELS,
|
|
...FINAL_BUILD_ARTIFACTS_STEP_LABELS,
|
|
];
|
|
const FULL_COMPILER_STEP_LABELS = [
|
|
"tsdown-ai",
|
|
"tsdown-packages",
|
|
"tsdown-unified",
|
|
"write-unified-entry-dts",
|
|
] as const;
|
|
// Typed builds cache declaration groups separately from the runtime graph.
|
|
const FULL_RUNTIME_STEP_LABELS = ASSET_RUNTIME_STEP_LABELS.flatMap((step) =>
|
|
step === "tsdown" ? FULL_COMPILER_STEP_LABELS : [step],
|
|
);
|
|
const FULL_BUILD_STEP_LABELS = [
|
|
"native-protocol",
|
|
...FULL_RUNTIME_STEP_LABELS,
|
|
...FINAL_BUILD_ARTIFACTS_STEP_LABELS,
|
|
];
|
|
|
|
const BUILD_ALL_PROFILES: Record<string, string[]> = {
|
|
full: [...FULL_BUILD_STEP_LABELS],
|
|
package: ["clean:dist", ...FULL_BUILD_STEP_LABELS],
|
|
ciArtifacts: [...CI_ARTIFACT_STEP_LABELS],
|
|
// Smoke builds retain typed compilation and publication checks without the UI/metadata tail.
|
|
strictSmoke: [...FULL_RUNTIME_STEP_LABELS, ...SDK_DECLARATION_STEP_LABELS],
|
|
pluginSdkStrictSmoke: [
|
|
...FULL_COMPILER_STEP_LABELS,
|
|
...RUNTIME_STEP_LABELS,
|
|
...SDK_DECLARATION_STEP_LABELS,
|
|
],
|
|
gatewayWatch: ["tsdown", ...RUNTIME_STEP_LABELS],
|
|
qaRuntime: [...ASSET_RUNTIME_STEP_LABELS],
|
|
sourcePerformance: [...ASSET_RUNTIME_STEP_LABELS, "write-build-info"],
|
|
cliStartup: ["tsdown", ...RUNTIME_STEP_LABELS, "write-cli-startup-metadata"],
|
|
};
|
|
|
|
const FULL_RUNTIME_ONLY_STEPS = [
|
|
"native-protocol",
|
|
...ASSET_RUNTIME_STEP_LABELS,
|
|
"ui:build",
|
|
...BUILD_METADATA_STEP_LABELS,
|
|
];
|
|
|
|
const BUILD_ALL_PROFILE_STEP_ENV: Record<string, Record<string, NodeJS.ProcessEnv>> = {
|
|
full: {
|
|
tsdown: {
|
|
OPENCLAW_PRESERVE_CLI_STARTUP_METADATA: "1",
|
|
},
|
|
"tsdown-unified": {
|
|
OPENCLAW_PRESERVE_CLI_STARTUP_METADATA: "1",
|
|
},
|
|
},
|
|
package: {
|
|
tsdown: {
|
|
OPENCLAW_PRESERVE_CLI_STARTUP_METADATA: "1",
|
|
},
|
|
"tsdown-unified": {
|
|
OPENCLAW_PRESERVE_CLI_STARTUP_METADATA: "1",
|
|
},
|
|
},
|
|
ciArtifacts: {
|
|
tsdown: {
|
|
// Global declaration emission is ~95% of the tsdown wall clock and PR
|
|
// CI's dist consumers are runtime JS only; the plugin-sdk gate below
|
|
// stages the two canonical SDK declaration groups instead. Release/package builds
|
|
// (full profile, docker packaging) keep canonical dts.
|
|
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1",
|
|
OPENCLAW_PRESERVE_CLI_STARTUP_METADATA: "1",
|
|
},
|
|
},
|
|
gatewayWatch: {
|
|
tsdown: {
|
|
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1",
|
|
},
|
|
"runtime-postbuild": {
|
|
OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: "0",
|
|
},
|
|
},
|
|
qaRuntime: {
|
|
tsdown: {
|
|
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1",
|
|
},
|
|
},
|
|
sourcePerformance: {
|
|
tsdown: {
|
|
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1",
|
|
},
|
|
},
|
|
cliStartup: {
|
|
tsdown: {
|
|
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1",
|
|
OPENCLAW_PRESERVE_CLI_STARTUP_METADATA: "1",
|
|
},
|
|
"runtime-postbuild": {
|
|
OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: "0",
|
|
},
|
|
},
|
|
};
|
|
|
|
function buildAllUsage() {
|
|
return [
|
|
"Usage: node --import tsx scripts/build-all.mts [profile]",
|
|
"",
|
|
"Builds OpenClaw artifacts for the selected profile.",
|
|
"",
|
|
"Profiles:",
|
|
...Object.keys(BUILD_ALL_PROFILES).map((profile) => ` ${profile}`),
|
|
"",
|
|
"Options:",
|
|
" -h, --help Show this help.",
|
|
].join("\n");
|
|
}
|
|
|
|
export function parseBuildAllArgs(argv: string[]) {
|
|
const args = {
|
|
help: false,
|
|
profile: "full",
|
|
};
|
|
let sawProfile = false;
|
|
for (const arg of argv) {
|
|
if (arg === "--help" || arg === "-h") {
|
|
args.help = true;
|
|
} else if (arg.startsWith("-")) {
|
|
throw new Error(`unknown argument: ${arg}\n\n${buildAllUsage()}`);
|
|
} else if (sawProfile) {
|
|
throw new Error(`unexpected argument: ${arg}\n\n${buildAllUsage()}`);
|
|
} else {
|
|
args.profile = arg;
|
|
sawProfile = true;
|
|
}
|
|
}
|
|
if (!args.help && !BUILD_ALL_PROFILES[args.profile]) {
|
|
throw new Error(`Unknown build profile: ${args.profile}\n\n${buildAllUsage()}`);
|
|
}
|
|
return args;
|
|
}
|
|
|
|
export function resolveBuildAllSteps(
|
|
profile = "full",
|
|
buildEnv: NodeJS.ProcessEnv = process.env,
|
|
): BuildAllStep[] {
|
|
const profileLabels = BUILD_ALL_PROFILES[profile];
|
|
if (!profileLabels) {
|
|
throw new Error(`Unknown build profile: ${profile}`);
|
|
}
|
|
// A cold runtime-only build has no declarations for the canonical SDK gates.
|
|
// Its uncached graph cannot seed the declaration-only caches used by full builds.
|
|
const runtimeOnly = buildEnv[RUN_NODE_SKIP_DTS_BUILD_ENV] === "1";
|
|
const labels =
|
|
profile === "full" && runtimeOnly
|
|
? FULL_RUNTIME_ONLY_STEPS
|
|
: profile === "package" && runtimeOnly
|
|
? ["clean:dist", ...FULL_RUNTIME_ONLY_STEPS]
|
|
: profileLabels;
|
|
const selected = labels.map((label) => BUILD_ALL_STEPS.find((step) => step.label === label));
|
|
if (selected.some((step) => !step)) {
|
|
const missing = labels.filter((label) => !BUILD_ALL_STEPS.some((step) => step.label === label));
|
|
throw new Error(`Build profile ${profile} references unknown steps: ${missing.join(", ")}`);
|
|
}
|
|
const envOverrides = BUILD_ALL_PROFILE_STEP_ENV[profile] ?? {};
|
|
return selected
|
|
.filter((step): step is NonNullable<typeof step> => step !== undefined)
|
|
.map((step) => {
|
|
const env = envOverrides[step.label];
|
|
if (!env) {
|
|
return step;
|
|
}
|
|
const mergedEnv = Object.assign({}, "env" in step ? step.env : undefined, env);
|
|
// Source-run rebuilds share qaRuntime but retain the caller's explicit
|
|
// declaration choice. The other partial profiles remain runtime-only.
|
|
if (profile === "qaRuntime" && step.label === "tsdown") {
|
|
mergedEnv[RUN_NODE_SKIP_DTS_BUILD_ENV] =
|
|
buildEnv[RUN_NODE_SKIP_DTS_BUILD_ENV] ?? mergedEnv[RUN_NODE_SKIP_DTS_BUILD_ENV];
|
|
}
|
|
const merged: BuildAllStep = Object.assign({}, step, { env: mergedEnv });
|
|
return merged;
|
|
});
|
|
}
|
|
|
|
/** Pin one source identity for every child process that contributes to this build. */
|
|
export function resolveBuildAllEnvironment(
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
now: () => Date = () => new Date(),
|
|
readGitCommit: () => string | null = readCurrentGitCommit,
|
|
) {
|
|
const buildEnv = resolveBuildIdentityEnvironment({
|
|
commitLabel: "build commit",
|
|
env,
|
|
now,
|
|
readGitCommit,
|
|
});
|
|
// Older installed updaters already send this marker to candidate builds.
|
|
// Updates need runtime artifacts; explicit declaration/package builds still win.
|
|
if (buildEnv.OPENCLAW_UPDATE_IN_PROGRESS === "1") {
|
|
buildEnv[RUN_NODE_SKIP_DTS_BUILD_ENV] ??= "1";
|
|
// Published updaters can still pass the serving checkout's source root.
|
|
// Rebind before plugin asset hooks resolve SDK aliases in this candidate.
|
|
buildEnv.OPENCLAW_DEV_SOURCE_ROOT = process.cwd();
|
|
}
|
|
return buildEnv;
|
|
}
|
|
|
|
function resolveBuildAllTsdownPlan(
|
|
profile: string,
|
|
env: NodeJS.ProcessEnv,
|
|
params: Omit<MemoryLimitParams, "env"> = {},
|
|
): {
|
|
env: NodeJS.ProcessEnv;
|
|
heapShortfall: ReturnType<typeof resolveTsdownBuildPlan>["heapShortfall"];
|
|
} {
|
|
if (
|
|
!["full", "package", "ciArtifacts", "strictSmoke", "pluginSdkStrictSmoke"].includes(profile)
|
|
) {
|
|
return { env, heapShortfall: null };
|
|
}
|
|
const plan = resolveTsdownBuildPlan({ ...params, env });
|
|
return {
|
|
// Direct Node steps need NODE_OPTIONS; tsdown descendants also need the frozen budget.
|
|
env: { ...plan.env, [TSDOWN_MAX_OLD_SPACE_MB_ENV]: String(plan.maxOldSpaceMb) },
|
|
heapShortfall: plan.heapShortfall,
|
|
};
|
|
}
|
|
|
|
export function resolveBuildAllStep(step: BuildAllStep, params: BuildAllStepParams = {}) {
|
|
const platform = params.platform ?? process.platform;
|
|
const env = step.env
|
|
? Object.assign({}, params.env ?? process.env, step.env)
|
|
: (params.env ?? process.env);
|
|
const assetArgs =
|
|
params.deferIsolatedAssets && step.label === "plugins:assets:build" ? ["--defer-isolated"] : [];
|
|
const pnpmArgs = step.kind === "pnpm" ? [...step.pnpmArgs, ...assetArgs] : undefined;
|
|
const nodeArgs =
|
|
step.kind !== "pnpm"
|
|
? step.args
|
|
: env.OPENCLAW_BUILD_ALL_NO_PNPM === "1"
|
|
? PNPM_STEP_NODE_FALLBACKS.get(step.label)
|
|
: undefined;
|
|
if (nodeArgs) {
|
|
return {
|
|
command: params.nodeExecPath ?? nodeBin,
|
|
args: [...nodeArgs, ...assetArgs],
|
|
options: {
|
|
stdio: "inherit",
|
|
env,
|
|
// Managed commands default to a Windows shell; Node needs literal argv,
|
|
// including percent-encoded file URLs passed to --import.
|
|
shell: false,
|
|
} satisfies SpawnSyncOptions,
|
|
};
|
|
}
|
|
const runner = resolvePnpmRunner({
|
|
env,
|
|
pnpmArgs,
|
|
nodeExecPath: params.nodeExecPath ?? nodeBin,
|
|
npmExecPath: params.npmExecPath ?? env.npm_execpath,
|
|
comSpec: params.comSpec,
|
|
platform,
|
|
});
|
|
return {
|
|
command: runner.command,
|
|
args: runner.args,
|
|
options: {
|
|
stdio: "inherit",
|
|
env,
|
|
shell: runner.shell,
|
|
windowsVerbatimArguments: runner.windowsVerbatimArguments,
|
|
} satisfies SpawnSyncOptions,
|
|
};
|
|
}
|
|
|
|
function resolveBuildAllStepOnCacheHit(step: BuildAllStep) {
|
|
if (!step.cache?.runOnHit) {
|
|
return null;
|
|
}
|
|
return {
|
|
...step,
|
|
env: Object.assign({}, step.env, step.cache.runOnHit.env),
|
|
};
|
|
}
|
|
|
|
export function formatBuildAllDuration(durationMs: number) {
|
|
const clampedMs = Math.max(0, durationMs);
|
|
const roundedMs =
|
|
clampedMs < 1000
|
|
? Math.round(clampedMs)
|
|
: clampedMs < 10_000
|
|
? Math.round(clampedMs / 10) * 10
|
|
: Math.round(clampedMs / 100) * 100;
|
|
return formatDurationElapsed(roundedMs, {
|
|
secondsDecimalDigits: clampedMs < 10_000 ? 2 : 1,
|
|
});
|
|
}
|
|
|
|
export function formatBuildAllTimingSummary(timings: BuildAllTiming[]) {
|
|
if (timings.length === 0) {
|
|
return "[build-all] phase timings: no phases ran";
|
|
}
|
|
const totalMs = timings.reduce((sum, timing) => sum + timing.durationMs, 0);
|
|
const phases = timings
|
|
.toSorted((left, right) => right.durationMs - left.durationMs)
|
|
.map((timing) => {
|
|
const status = timing.status === "ran" ? "" : ` (${timing.status})`;
|
|
return `${timing.label}${status} ${formatBuildAllDuration(timing.durationMs)}`;
|
|
})
|
|
.join("; ");
|
|
return `[build-all] phase timings: total ${formatBuildAllDuration(totalMs)}; slowest ${phases}`;
|
|
}
|
|
|
|
export async function runBuildAllSteps(
|
|
profile: string,
|
|
params: {
|
|
cacheEnabled?: boolean;
|
|
signal?: AbortSignal;
|
|
requireVerifiedGatewayFence?: boolean;
|
|
cwd?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
finalizeCache?: typeof finalizeBuildStepCache;
|
|
logger?: Pick<Console, "error" | "warn">;
|
|
memoryLimit?: Omit<MemoryLimitParams, "env">;
|
|
now?: () => number;
|
|
resolveCacheState?: typeof resolveBuildStepCacheState;
|
|
restoreCache?: typeof restoreBuildStepCacheOutputs;
|
|
runStep?: (
|
|
invocation: ReturnType<typeof resolveBuildAllStep>,
|
|
) => { status: number | null } | Promise<{ status: number | null }>;
|
|
steps?: BuildAllStep[];
|
|
} = {},
|
|
): Promise<BuildAllResult> {
|
|
params.signal?.throwIfAborted();
|
|
await preflightInstalledSourceArtifacts(params.env ?? process.env);
|
|
params.signal?.throwIfAborted();
|
|
const { env: buildEnv, heapShortfall } = resolveBuildAllTsdownPlan(
|
|
profile,
|
|
resolveBuildAllEnvironment(params.env),
|
|
params.memoryLimit,
|
|
);
|
|
const steps = params.steps ?? resolveBuildAllSteps(profile, buildEnv);
|
|
const deferIsolatedAssets = steps.some((step) => step.label === "external-plugins:local-dist");
|
|
const cacheEnabled = params.cacheEnabled ?? buildEnv.OPENCLAW_BUILD_CACHE !== "0";
|
|
const logger = params.logger ?? console;
|
|
// One owner for both `pnpm build` and run-node dirty-tree auto-build: both
|
|
// enter here before clean:dist can delete hashed modules a live Gateway still imports.
|
|
const fence = await resolveLiveManagedGatewayDistFence(params.cwd ?? process.cwd(), {
|
|
env: buildEnv,
|
|
requireVerified: params.requireVerifiedGatewayFence,
|
|
outputPaths: listTsdownOutputRoots(),
|
|
});
|
|
params.signal?.throwIfAborted();
|
|
if (fence.refuse) {
|
|
logger.error(fence.message);
|
|
return {
|
|
exitCode: 1,
|
|
timings: [] satisfies BuildAllTiming[],
|
|
admissionRefused: true,
|
|
};
|
|
}
|
|
const now = params.now ?? performance.now.bind(performance);
|
|
const resolveCacheState = params.resolveCacheState ?? resolveBuildStepCacheState;
|
|
const restoreCache = params.restoreCache ?? restoreBuildStepCacheOutputs;
|
|
const finalizeCache = params.finalizeCache ?? finalizeBuildStepCache;
|
|
const cwd = params.cwd ?? process.cwd();
|
|
const inputDeps = { cwd, distRoot: path.join(cwd, "dist"), fs, env: buildEnv, spawnSync };
|
|
const capturesNativeInputs =
|
|
!params.runStep && steps.some((step) => step.label.endsWith("build-stamp"));
|
|
const hasAssetBuild =
|
|
capturesNativeInputs && steps.some((step) => step.label === "plugins:assets:build");
|
|
const assetInputState = hasAssetBuild
|
|
? captureRunNodeInputState(inputDeps, "build", { assetPhase: true })
|
|
: null;
|
|
let buildInputState =
|
|
capturesNativeInputs && !hasAssetBuild && steps.some((step) => step.label === "build-stamp")
|
|
? captureRunNodeInputState(inputDeps, "build")
|
|
: null;
|
|
const runtimeEnv = {
|
|
...buildEnv,
|
|
...steps.find((step) => step.label === "runtime-postbuild")?.env,
|
|
};
|
|
let runtimeInputState =
|
|
capturesNativeInputs &&
|
|
!hasAssetBuild &&
|
|
steps.some((step) => step.label === "runtime-postbuild-stamp")
|
|
? captureRunNodeInputState({ ...inputDeps, env: runtimeEnv }, "runtime")
|
|
: null;
|
|
let stampsInvalidated = false;
|
|
const invalidateInputStamps = () => {
|
|
// Injected steps own their fixture writes; native writers share this lifecycle.
|
|
if (
|
|
stampsInvalidated ||
|
|
params.runStep ||
|
|
!steps.some((step) => step.label.endsWith("build-stamp"))
|
|
) {
|
|
return;
|
|
}
|
|
for (const name of [BUILD_STAMP_FILE, RUNTIME_POSTBUILD_STAMP_FILE]) {
|
|
fs.rmSync(path.join(cwd, "dist", name), { force: true });
|
|
}
|
|
stampsInvalidated = true;
|
|
};
|
|
const runStep =
|
|
params.runStep ??
|
|
(async (invocation: ReturnType<typeof resolveBuildAllStep>) => {
|
|
const script = invocation.args[2];
|
|
if (
|
|
script === "scripts/build-stamp.mts" ||
|
|
script === "scripts/runtime-postbuild-stamp.mts"
|
|
) {
|
|
const buildStamp = script === "scripts/build-stamp.mts";
|
|
(buildStamp ? writeBuildStamp : writeRuntimePostBuildStamp)({
|
|
cwd,
|
|
env: buildStamp ? buildEnv : runtimeEnv,
|
|
inputState: buildStamp ? buildInputState : runtimeInputState,
|
|
});
|
|
return { status: 0 };
|
|
}
|
|
return {
|
|
status: await runManagedCommand({
|
|
bin: invocation.command,
|
|
args:
|
|
script === "scripts/tsdown-build.mts" ||
|
|
script === "scripts/write-unified-entry-dts.ts" ||
|
|
script === "scripts/write-plugin-sdk-entry-dts.ts" ||
|
|
script === "scripts/runtime-postbuild.mts"
|
|
? distArtifactEntryArgs(script, invocation.args.slice(3))
|
|
: invocation.args,
|
|
...invocation.options,
|
|
signal: params.signal,
|
|
requireProcessTreeExit: process.platform !== "win32",
|
|
}),
|
|
};
|
|
});
|
|
const timings: BuildAllTiming[] = [];
|
|
let exitCode = 0;
|
|
if (heapShortfall) {
|
|
if (heapShortfall.fatal) {
|
|
logger.error(heapShortfall.message);
|
|
return { exitCode: 1, timings };
|
|
}
|
|
logger.warn(heapShortfall.message);
|
|
}
|
|
for (const step of steps) {
|
|
params.signal?.throwIfAborted();
|
|
const cacheStartedAt = now();
|
|
const cacheState = resolveCacheState(step, { env: buildEnv });
|
|
const cacheDurationMs = now() - cacheStartedAt;
|
|
const startedAt = now();
|
|
let stepToRun = step;
|
|
let reusedCache = false;
|
|
if (cacheEnabled && cacheState.fresh) {
|
|
if (cacheState.restorable) {
|
|
invalidateInputStamps();
|
|
if (!restoreCache(cacheState)) {
|
|
throw new Error(`Build cache changed before restoration: ${step.label}; rerun the build`);
|
|
}
|
|
}
|
|
const cacheHitStep = resolveBuildAllStepOnCacheHit(step);
|
|
if (!cacheHitStep) {
|
|
const durationMs = cacheDurationMs + now() - startedAt;
|
|
timings.push({ label: step.label, status: "cached", durationMs });
|
|
logger.error(`[build-all] ${step.label} (cached) ${formatBuildAllDuration(durationMs)}`);
|
|
continue;
|
|
}
|
|
reusedCache = true;
|
|
stepToRun = cacheHitStep;
|
|
}
|
|
logger.error(`[build-all] ${step.label}${reusedCache ? " (cache restored)" : ""}`);
|
|
const invocation = resolveBuildAllStep(stepToRun, { env: buildEnv, deferIsolatedAssets });
|
|
invalidateInputStamps();
|
|
const result = await runStep(invocation);
|
|
params.signal?.throwIfAborted();
|
|
const durationMs = cacheDurationMs + now() - startedAt;
|
|
if (result.status !== 0) {
|
|
timings.push({ label: step.label, status: "failed", durationMs });
|
|
logger.error(`[build-all] ${step.label} failed after ${formatBuildAllDuration(durationMs)}`);
|
|
exitCode = typeof result.status === "number" ? result.status : 1;
|
|
break;
|
|
}
|
|
if (step.label === "plugins:assets:build" && !params.runStep) {
|
|
const current = captureRunNodeInputState(inputDeps, "build", { assetPhase: true });
|
|
if (
|
|
assetInputState &&
|
|
(!current ||
|
|
current.signature !== assetInputState.signature ||
|
|
current.generation !== assetInputState.generation)
|
|
) {
|
|
throw new Error("Build inputs changed during asset preparation; rerun the build");
|
|
}
|
|
buildInputState = assetInputState ? captureRunNodeInputState(inputDeps, "build") : null;
|
|
runtimeInputState = assetInputState
|
|
? captureRunNodeInputState({ ...inputDeps, env: runtimeEnv }, "runtime")
|
|
: null;
|
|
}
|
|
// Runtime-only tsdown cleans its output roots. Cache hits restore
|
|
// declarations again after that pass so the full build stays complete.
|
|
if (!finalizeCache(step, cacheState, { env: buildEnv, reusedCache })) {
|
|
throw new Error(`Build cache changed during ${step.label}; rerun the build`);
|
|
}
|
|
timings.push({ label: step.label, status: reusedCache ? "reused" : "ran", durationMs });
|
|
logger.error(`[build-all] ${step.label} done in ${formatBuildAllDuration(durationMs)}`);
|
|
}
|
|
logger.error(formatBuildAllTimingSummary(timings));
|
|
return { exitCode, timings };
|
|
}
|
|
|
|
if (isDirectRunUrl(process.argv[1], import.meta.url)) {
|
|
let args;
|
|
try {
|
|
args = parseBuildAllArgs(process.argv.slice(2));
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exitCode = 2;
|
|
}
|
|
if (args?.help) {
|
|
console.log(buildAllUsage());
|
|
} else if (args) {
|
|
const { runLegacySourceUpdateBuild } = await import("./lib/source-update-build.mts");
|
|
const legacyExit = await runLegacySourceUpdateBuild(args.profile, (env) =>
|
|
runBuildAllSteps(args.profile, { env }),
|
|
);
|
|
const exitCode =
|
|
legacyExit ??
|
|
(await withDistArtifactOwnership(process.cwd(), () => runBuildAllSteps(args.profile)))
|
|
.exitCode;
|
|
if (exitCode !== 0) {
|
|
process.exitCode = exitCode;
|
|
}
|
|
}
|
|
}
|