mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-17 12:11:20 +00:00
fix: unblock windows update build
This commit is contained in:
@@ -1074,7 +1074,7 @@
|
||||
"canon:check": "node scripts/canon.mjs check",
|
||||
"canon:check:json": "node scripts/canon.mjs check --json",
|
||||
"canon:enforce": "node scripts/canon.mjs enforce --json",
|
||||
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
|
||||
"canvas:a2ui:bundle": "node scripts/bundle-a2ui.mjs",
|
||||
"check": "pnpm check:no-conflict-markers && pnpm tool-display:check && pnpm check:host-env-policy:swift && pnpm tsgo && node scripts/prepare-extension-package-boundary-artifacts.mjs && pnpm lint && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
|
||||
"check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check",
|
||||
"check:bundled-channel-config-metadata": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check",
|
||||
|
||||
@@ -1,39 +1,131 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
|
||||
|
||||
const nodeBin = process.execPath;
|
||||
const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
|
||||
const steps = [
|
||||
{ cmd: pnpmBin, args: ["canvas:a2ui:bundle"] },
|
||||
{ cmd: nodeBin, args: ["scripts/tsdown-build.mjs"] },
|
||||
{ cmd: nodeBin, args: ["scripts/runtime-postbuild.mjs"] },
|
||||
{ cmd: nodeBin, args: ["scripts/build-stamp.mjs"] },
|
||||
{ cmd: pnpmBin, args: ["build:plugin-sdk:dts"] },
|
||||
{ cmd: nodeBin, args: ["--import", "tsx", "scripts/write-plugin-sdk-entry-dts.ts"] },
|
||||
{ cmd: nodeBin, args: ["scripts/check-plugin-sdk-exports.mjs"] },
|
||||
{ cmd: nodeBin, args: ["--import", "tsx", "scripts/canvas-a2ui-copy.ts"] },
|
||||
{ cmd: nodeBin, args: ["--import", "tsx", "scripts/copy-hook-metadata.ts"] },
|
||||
{ cmd: nodeBin, args: ["--import", "tsx", "scripts/copy-export-html-templates.ts"] },
|
||||
{ cmd: nodeBin, args: ["--import", "tsx", "scripts/write-build-info.ts"] },
|
||||
const WINDOWS_BUILD_MAX_OLD_SPACE_MB = 4096;
|
||||
export const BUILD_ALL_STEPS = [
|
||||
{ label: "canvas:a2ui:bundle", kind: "pnpm", pnpmArgs: ["canvas:a2ui:bundle"] },
|
||||
{ label: "tsdown", kind: "node", args: ["scripts/tsdown-build.mjs"] },
|
||||
{ label: "runtime-postbuild", kind: "node", args: ["scripts/runtime-postbuild.mjs"] },
|
||||
{ label: "build-stamp", kind: "node", args: ["scripts/build-stamp.mjs"] },
|
||||
{
|
||||
cmd: nodeBin,
|
||||
label: "build:plugin-sdk:dts",
|
||||
kind: "pnpm",
|
||||
pnpmArgs: ["build:plugin-sdk:dts"],
|
||||
windowsNodeOptions: `--max-old-space-size=${WINDOWS_BUILD_MAX_OLD_SPACE_MB}`,
|
||||
},
|
||||
{
|
||||
label: "write-plugin-sdk-entry-dts",
|
||||
kind: "node",
|
||||
args: ["--import", "tsx", "scripts/write-plugin-sdk-entry-dts.ts"],
|
||||
},
|
||||
{
|
||||
label: "check-plugin-sdk-exports",
|
||||
kind: "node",
|
||||
args: ["scripts/check-plugin-sdk-exports.mjs"],
|
||||
},
|
||||
{
|
||||
label: "canvas-a2ui-copy",
|
||||
kind: "node",
|
||||
args: ["--import", "tsx", "scripts/canvas-a2ui-copy.ts"],
|
||||
},
|
||||
{
|
||||
label: "copy-hook-metadata",
|
||||
kind: "node",
|
||||
args: ["--import", "tsx", "scripts/copy-hook-metadata.ts"],
|
||||
},
|
||||
{
|
||||
label: "copy-export-html-templates",
|
||||
kind: "node",
|
||||
args: ["--import", "tsx", "scripts/copy-export-html-templates.ts"],
|
||||
},
|
||||
{
|
||||
label: "write-build-info",
|
||||
kind: "node",
|
||||
args: ["--import", "tsx", "scripts/write-build-info.ts"],
|
||||
},
|
||||
{
|
||||
label: "write-cli-startup-metadata",
|
||||
kind: "node",
|
||||
args: ["--experimental-strip-types", "scripts/write-cli-startup-metadata.ts"],
|
||||
},
|
||||
{ cmd: nodeBin, args: ["--import", "tsx", "scripts/write-cli-compat.ts"] },
|
||||
{
|
||||
label: "write-cli-compat",
|
||||
kind: "node",
|
||||
args: ["--import", "tsx", "scripts/write-cli-compat.ts"],
|
||||
},
|
||||
];
|
||||
|
||||
for (const step of steps) {
|
||||
const result = spawnSync(step.cmd, step.args, {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
if (typeof result.status === "number") {
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status);
|
||||
}
|
||||
continue;
|
||||
function resolveStepEnv(step, env, platform) {
|
||||
if (platform !== "win32" || !step.windowsNodeOptions) {
|
||||
return env;
|
||||
}
|
||||
const currentNodeOptions = env.NODE_OPTIONS?.trim() ?? "";
|
||||
if (currentNodeOptions.includes(step.windowsNodeOptions)) {
|
||||
return env;
|
||||
}
|
||||
return {
|
||||
...env,
|
||||
NODE_OPTIONS: currentNodeOptions
|
||||
? `${currentNodeOptions} ${step.windowsNodeOptions}`
|
||||
: step.windowsNodeOptions,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveBuildAllStep(step, params = {}) {
|
||||
const platform = params.platform ?? process.platform;
|
||||
const env = resolveStepEnv(step, params.env ?? process.env, platform);
|
||||
if (step.kind === "pnpm") {
|
||||
const runner = resolvePnpmRunner({
|
||||
pnpmArgs: step.pnpmArgs,
|
||||
nodeExecPath: params.nodeExecPath ?? nodeBin,
|
||||
npmExecPath: params.npmExecPath ?? env.npm_execpath,
|
||||
comSpec: params.comSpec ?? env.ComSpec,
|
||||
platform,
|
||||
});
|
||||
return {
|
||||
command: runner.command,
|
||||
args: runner.args,
|
||||
options: {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
shell: runner.shell,
|
||||
windowsVerbatimArguments: runner.windowsVerbatimArguments,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
command: params.nodeExecPath ?? nodeBin,
|
||||
args: step.args,
|
||||
options: {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isMainModule() {
|
||||
const argv1 = process.argv[1];
|
||||
if (!argv1) {
|
||||
return false;
|
||||
}
|
||||
return import.meta.url === pathToFileURL(argv1).href;
|
||||
}
|
||||
|
||||
if (isMainModule()) {
|
||||
for (const step of BUILD_ALL_STEPS) {
|
||||
console.error(`[build-all] ${step.label}`);
|
||||
const invocation = resolveBuildAllStep(step);
|
||||
const result = spawnSync(invocation.command, invocation.args, invocation.options);
|
||||
if (typeof result.status === "number") {
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
162
scripts/bundle-a2ui.mjs
Normal file
162
scripts/bundle-a2ui.mjs
Normal file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
|
||||
|
||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const hashFile = path.join(rootDir, "src", "canvas-host", "a2ui", ".bundle.hash");
|
||||
const outputFile = path.join(rootDir, "src", "canvas-host", "a2ui", "a2ui.bundle.js");
|
||||
const a2uiRendererDir = path.join(rootDir, "vendor", "a2ui", "renderers", "lit");
|
||||
const a2uiAppDir = path.join(rootDir, "apps", "shared", "OpenClawKit", "Tools", "CanvasA2UI");
|
||||
const inputPaths = [
|
||||
path.join(rootDir, "package.json"),
|
||||
path.join(rootDir, "pnpm-lock.yaml"),
|
||||
a2uiRendererDir,
|
||||
a2uiAppDir,
|
||||
];
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
console.error("A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle");
|
||||
console.error("If this persists, verify pnpm deps and try again.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function pathExists(targetPath) {
|
||||
try {
|
||||
await fs.stat(targetPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function walkFiles(entryPath, files) {
|
||||
const stat = await fs.stat(entryPath);
|
||||
if (!stat.isDirectory()) {
|
||||
files.push(entryPath);
|
||||
return;
|
||||
}
|
||||
const entries = await fs.readdir(entryPath);
|
||||
for (const entry of entries) {
|
||||
await walkFiles(path.join(entryPath, entry), files);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePath(filePath) {
|
||||
return filePath.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
async function computeHash() {
|
||||
const files = [];
|
||||
for (const inputPath of inputPaths) {
|
||||
await walkFiles(inputPath, files);
|
||||
}
|
||||
files.sort((left, right) => normalizePath(left).localeCompare(normalizePath(right)));
|
||||
|
||||
const hash = createHash("sha256");
|
||||
for (const filePath of files) {
|
||||
hash.update(normalizePath(path.relative(rootDir, filePath)));
|
||||
hash.update("\0");
|
||||
hash.update(await fs.readFile(filePath));
|
||||
hash.update("\0");
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
function runStep(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
...options,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
function runPnpm(pnpmArgs) {
|
||||
const runner = resolvePnpmRunner({
|
||||
pnpmArgs,
|
||||
nodeExecPath: process.execPath,
|
||||
npmExecPath: process.env.npm_execpath,
|
||||
comSpec: process.env.ComSpec,
|
||||
platform: process.platform,
|
||||
});
|
||||
runStep(runner.command, runner.args, {
|
||||
shell: runner.shell,
|
||||
windowsVerbatimArguments: runner.windowsVerbatimArguments,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const hasRendererDir = await pathExists(a2uiRendererDir);
|
||||
const hasAppDir = await pathExists(a2uiAppDir);
|
||||
const hasOutputFile = await pathExists(outputFile);
|
||||
if (!hasRendererDir || !hasAppDir) {
|
||||
if (hasOutputFile) {
|
||||
console.log("A2UI sources missing; keeping prebuilt bundle.");
|
||||
return;
|
||||
}
|
||||
if (process.env.OPENCLAW_SPARSE_PROFILE || process.env.OPENCLAW_A2UI_SKIP_MISSING === "1") {
|
||||
console.error(
|
||||
"A2UI sources missing; skipping bundle because OPENCLAW_A2UI_SKIP_MISSING=1 or OPENCLAW_SPARSE_PROFILE is set.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
fail(`A2UI sources missing and no prebuilt bundle found at: ${outputFile}`);
|
||||
}
|
||||
|
||||
const currentHash = await computeHash();
|
||||
if (await pathExists(hashFile)) {
|
||||
const previousHash = (await fs.readFile(hashFile, "utf8")).trim();
|
||||
if (previousHash === currentHash && hasOutputFile) {
|
||||
console.log("A2UI bundle up to date; skipping.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
runPnpm(["-s", "exec", "tsc", "-p", path.join(a2uiRendererDir, "tsconfig.json")]);
|
||||
|
||||
const localRolldownCliCandidates = [
|
||||
path.join(rootDir, "node_modules", ".pnpm", "node_modules", "rolldown", "bin", "cli.mjs"),
|
||||
path.join(
|
||||
rootDir,
|
||||
"node_modules",
|
||||
".pnpm",
|
||||
"rolldown@1.0.0-rc.9",
|
||||
"node_modules",
|
||||
"rolldown",
|
||||
"bin",
|
||||
"cli.mjs",
|
||||
),
|
||||
];
|
||||
const localRolldownCli = (
|
||||
await Promise.all(
|
||||
localRolldownCliCandidates.map(async (candidate) =>
|
||||
(await pathExists(candidate)) ? candidate : null,
|
||||
),
|
||||
)
|
||||
).find(Boolean);
|
||||
|
||||
if (localRolldownCli) {
|
||||
runStep(process.execPath, [
|
||||
localRolldownCli,
|
||||
"-c",
|
||||
path.join(a2uiAppDir, "rolldown.config.mjs"),
|
||||
]);
|
||||
} else {
|
||||
runPnpm(["-s", "dlx", "rolldown", "-c", path.join(a2uiAppDir, "rolldown.config.mjs")]);
|
||||
}
|
||||
|
||||
await fs.writeFile(hashFile, `${currentHash}\n`, "utf8");
|
||||
}
|
||||
|
||||
await main().catch((error) => {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
@@ -1,104 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
on_error() {
|
||||
echo "A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle" >&2
|
||||
echo "If this persists, verify pnpm deps and try again." >&2
|
||||
}
|
||||
trap on_error ERR
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
HASH_FILE="$ROOT_DIR/src/canvas-host/a2ui/.bundle.hash"
|
||||
OUTPUT_FILE="$ROOT_DIR/src/canvas-host/a2ui/a2ui.bundle.js"
|
||||
A2UI_RENDERER_DIR="$ROOT_DIR/vendor/a2ui/renderers/lit"
|
||||
A2UI_APP_DIR="$ROOT_DIR/apps/shared/OpenClawKit/Tools/CanvasA2UI"
|
||||
|
||||
# Docker builds exclude vendor/apps via .dockerignore.
|
||||
# Sparse local builds can also omit these inputs intentionally.
|
||||
if [[ ! -d "$A2UI_RENDERER_DIR" || ! -d "$A2UI_APP_DIR" ]]; then
|
||||
if [[ -f "$OUTPUT_FILE" ]]; then
|
||||
echo "A2UI sources missing; keeping prebuilt bundle."
|
||||
exit 0
|
||||
fi
|
||||
if [[ -n "${OPENCLAW_SPARSE_PROFILE:-}" || "${OPENCLAW_A2UI_SKIP_MISSING:-}" == "1" ]]; then
|
||||
echo "A2UI sources missing; skipping bundle because OPENCLAW_A2UI_SKIP_MISSING=1 or OPENCLAW_SPARSE_PROFILE is set." >&2
|
||||
exit 0
|
||||
fi
|
||||
echo "A2UI sources missing and no prebuilt bundle found at: $OUTPUT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT_PATHS=(
|
||||
"$ROOT_DIR/package.json"
|
||||
"$ROOT_DIR/pnpm-lock.yaml"
|
||||
"$A2UI_RENDERER_DIR"
|
||||
"$A2UI_APP_DIR"
|
||||
)
|
||||
|
||||
compute_hash() {
|
||||
ROOT_DIR="$ROOT_DIR" node --input-type=module --eval '
|
||||
import { createHash } from "node:crypto";
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const rootDir = process.env.ROOT_DIR ?? process.cwd();
|
||||
const inputs = process.argv.slice(1);
|
||||
const files = [];
|
||||
|
||||
async function walk(entryPath) {
|
||||
const st = await fs.stat(entryPath);
|
||||
if (st.isDirectory()) {
|
||||
const entries = await fs.readdir(entryPath);
|
||||
for (const entry of entries) {
|
||||
await walk(path.join(entryPath, entry));
|
||||
}
|
||||
return;
|
||||
}
|
||||
files.push(entryPath);
|
||||
}
|
||||
|
||||
for (const input of inputs) {
|
||||
await walk(input);
|
||||
}
|
||||
|
||||
function normalize(p) {
|
||||
return p.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
files.sort((a, b) => normalize(a).localeCompare(normalize(b)));
|
||||
|
||||
const hash = createHash("sha256");
|
||||
for (const filePath of files) {
|
||||
const rel = normalize(path.relative(rootDir, filePath));
|
||||
hash.update(rel);
|
||||
hash.update("\0");
|
||||
hash.update(await fs.readFile(filePath));
|
||||
hash.update("\0");
|
||||
}
|
||||
|
||||
process.stdout.write(hash.digest("hex"));
|
||||
' "${INPUT_PATHS[@]}"
|
||||
}
|
||||
|
||||
current_hash="$(compute_hash)"
|
||||
if [[ -f "$HASH_FILE" ]]; then
|
||||
previous_hash="$(cat "$HASH_FILE")"
|
||||
if [[ "$previous_hash" == "$current_hash" && -f "$OUTPUT_FILE" ]]; then
|
||||
echo "A2UI bundle up to date; skipping."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
pnpm -s exec tsc -p "$A2UI_RENDERER_DIR/tsconfig.json"
|
||||
if command -v rolldown >/dev/null 2>&1 && rolldown --version >/dev/null 2>&1; then
|
||||
rolldown -c "$A2UI_APP_DIR/rolldown.config.mjs"
|
||||
elif [[ -f "$ROOT_DIR/node_modules/.pnpm/node_modules/rolldown/bin/cli.mjs" ]]; then
|
||||
node "$ROOT_DIR/node_modules/.pnpm/node_modules/rolldown/bin/cli.mjs" -c "$A2UI_APP_DIR/rolldown.config.mjs"
|
||||
elif [[ -f "$ROOT_DIR/node_modules/.pnpm/rolldown@1.0.0-rc.9/node_modules/rolldown/bin/cli.mjs" ]]; then
|
||||
node "$ROOT_DIR/node_modules/.pnpm/rolldown@1.0.0-rc.9/node_modules/rolldown/bin/cli.mjs" \
|
||||
-c "$A2UI_APP_DIR/rolldown.config.mjs"
|
||||
else
|
||||
pnpm -s dlx rolldown -c "$A2UI_APP_DIR/rolldown.config.mjs"
|
||||
fi
|
||||
|
||||
echo "$current_hash" > "$HASH_FILE"
|
||||
exec node "$ROOT_DIR/scripts/bundle-a2ui.mjs" "$@"
|
||||
|
||||
@@ -12,11 +12,33 @@ function relativeSymlinkTarget(sourcePath, targetPath) {
|
||||
return relativeTarget || ".";
|
||||
}
|
||||
|
||||
function ensureSymlink(targetValue, targetPath, type) {
|
||||
function shouldFallbackToCopy(error) {
|
||||
return (
|
||||
process.platform === "win32" &&
|
||||
(error?.code === "EPERM" || error?.code === "EINVAL" || error?.code === "UNKNOWN")
|
||||
);
|
||||
}
|
||||
|
||||
function copyPathFallback(sourcePath, targetPath) {
|
||||
removePathIfExists(targetPath);
|
||||
const stat = fs.statSync(sourcePath);
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
if (stat.isDirectory()) {
|
||||
fs.cpSync(sourcePath, targetPath, { recursive: true, dereference: true });
|
||||
return;
|
||||
}
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
}
|
||||
|
||||
function ensureSymlink(targetValue, targetPath, type, fallbackSourcePath) {
|
||||
try {
|
||||
fs.symlinkSync(targetValue, targetPath, type);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (fallbackSourcePath && shouldFallbackToCopy(error)) {
|
||||
copyPathFallback(fallbackSourcePath, targetPath);
|
||||
return;
|
||||
}
|
||||
if (error?.code !== "EEXIST") {
|
||||
throw error;
|
||||
}
|
||||
@@ -31,11 +53,19 @@ function ensureSymlink(targetValue, targetPath, type) {
|
||||
}
|
||||
|
||||
removePathIfExists(targetPath);
|
||||
fs.symlinkSync(targetValue, targetPath, type);
|
||||
try {
|
||||
fs.symlinkSync(targetValue, targetPath, type);
|
||||
} catch (error) {
|
||||
if (fallbackSourcePath && shouldFallbackToCopy(error)) {
|
||||
copyPathFallback(fallbackSourcePath, targetPath);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function symlinkPath(sourcePath, targetPath, type) {
|
||||
ensureSymlink(relativeSymlinkTarget(sourcePath, targetPath), targetPath, type);
|
||||
ensureSymlink(relativeSymlinkTarget(sourcePath, targetPath), targetPath, type, sourcePath);
|
||||
}
|
||||
|
||||
function shouldWrapRuntimeJsFile(sourcePath) {
|
||||
@@ -85,7 +115,7 @@ function stagePluginRuntimeOverlay(sourceDir, targetDir) {
|
||||
}
|
||||
|
||||
if (dirent.isSymbolicLink()) {
|
||||
ensureSymlink(fs.readlinkSync(sourcePath), targetPath);
|
||||
ensureSymlink(fs.readlinkSync(sourcePath), targetPath, undefined, sourcePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -113,7 +143,12 @@ function linkPluginNodeModules(params) {
|
||||
if (!fs.existsSync(params.sourcePluginNodeModulesDir)) {
|
||||
return;
|
||||
}
|
||||
ensureSymlink(params.sourcePluginNodeModulesDir, runtimeNodeModulesDir, symlinkType());
|
||||
ensureSymlink(
|
||||
params.sourcePluginNodeModulesDir,
|
||||
runtimeNodeModulesDir,
|
||||
symlinkType(),
|
||||
params.sourcePluginNodeModulesDir,
|
||||
);
|
||||
}
|
||||
|
||||
export function stageBundledPluginRuntime(params = {}) {
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { BUNDLED_PLUGIN_PATH_PREFIX } from "./lib/bundled-plugin-paths.mjs";
|
||||
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
|
||||
|
||||
const logLevel = process.env.OPENCLAW_BUILD_VERBOSE ? "info" : "warn";
|
||||
const extraArgs = process.argv.slice(2);
|
||||
@@ -61,42 +63,67 @@ function findFatalUnresolvedImport(lines) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = spawnSync(
|
||||
"pnpm",
|
||||
["exec", "tsdown", "--config-loader", "unrun", "--logLevel", logLevel, ...extraArgs],
|
||||
{
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
shell: process.platform === "win32",
|
||||
},
|
||||
);
|
||||
|
||||
const stdout = result.stdout ?? "";
|
||||
const stderr = result.stderr ?? "";
|
||||
if (stdout) {
|
||||
process.stdout.write(stdout);
|
||||
}
|
||||
if (stderr) {
|
||||
process.stderr.write(stderr);
|
||||
export function resolveTsdownBuildInvocation(params = {}) {
|
||||
const env = params.env ?? process.env;
|
||||
const runner = resolvePnpmRunner({
|
||||
pnpmArgs: ["exec", "tsdown", "--config-loader", "unrun", "--logLevel", logLevel, ...extraArgs],
|
||||
nodeExecPath: params.nodeExecPath ?? process.execPath,
|
||||
npmExecPath: params.npmExecPath ?? env.npm_execpath,
|
||||
comSpec: params.comSpec ?? env.ComSpec,
|
||||
platform: params.platform ?? process.platform,
|
||||
});
|
||||
return {
|
||||
command: runner.command,
|
||||
args: runner.args,
|
||||
options: {
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
shell: runner.shell,
|
||||
windowsVerbatimArguments: runner.windowsVerbatimArguments,
|
||||
env,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (result.status === 0 && INEFFECTIVE_DYNAMIC_IMPORT_RE.test(`${stdout}\n${stderr}`)) {
|
||||
console.error(
|
||||
"Build emitted [INEFFECTIVE_DYNAMIC_IMPORT]. Replace transparent runtime re-export facades with real runtime boundaries.",
|
||||
);
|
||||
function isMainModule() {
|
||||
const argv1 = process.argv[1];
|
||||
if (!argv1) {
|
||||
return false;
|
||||
}
|
||||
return import.meta.url === pathToFileURL(argv1).href;
|
||||
}
|
||||
|
||||
if (isMainModule()) {
|
||||
const invocation = resolveTsdownBuildInvocation();
|
||||
const result = spawnSync(invocation.command, invocation.args, invocation.options);
|
||||
|
||||
const stdout = result.stdout ?? "";
|
||||
const stderr = result.stderr ?? "";
|
||||
if (stdout) {
|
||||
process.stdout.write(stdout);
|
||||
}
|
||||
if (stderr) {
|
||||
process.stderr.write(stderr);
|
||||
}
|
||||
|
||||
if (result.status === 0 && INEFFECTIVE_DYNAMIC_IMPORT_RE.test(`${stdout}\n${stderr}`)) {
|
||||
console.error(
|
||||
"Build emitted [INEFFECTIVE_DYNAMIC_IMPORT]. Replace transparent runtime re-export facades with real runtime boundaries.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fatalUnresolvedImport =
|
||||
result.status === 0 ? findFatalUnresolvedImport(`${stdout}\n${stderr}`.split("\n")) : null;
|
||||
|
||||
if (fatalUnresolvedImport) {
|
||||
console.error(`Build emitted [UNRESOLVED_IMPORT] outside extensions: ${fatalUnresolvedImport}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (typeof result.status === "number") {
|
||||
process.exit(result.status);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fatalUnresolvedImport =
|
||||
result.status === 0 ? findFatalUnresolvedImport(`${stdout}\n${stderr}`.split("\n")) : null;
|
||||
|
||||
if (fatalUnresolvedImport) {
|
||||
console.error(`Build emitted [UNRESOLVED_IMPORT] outside extensions: ${fatalUnresolvedImport}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (typeof result.status === "number") {
|
||||
process.exit(result.status);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
|
||||
@@ -791,6 +791,102 @@ describe("runGatewayUpdate", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("adds heap headroom to windows pnpm build steps during dev updates", async () => {
|
||||
await setupGitPackageManagerFixture();
|
||||
const upstreamSha = "upstream123";
|
||||
const buildNodeOptions: string[] = [];
|
||||
const doctorNodePath = await resolveStableNodePath(process.execPath);
|
||||
const doctorCommand = `${doctorNodePath} ${path.join(tempDir, "openclaw.mjs")} doctor --non-interactive --fix`;
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
|
||||
try {
|
||||
const runCommand = async (
|
||||
argv: string[],
|
||||
options?: { env?: NodeJS.ProcessEnv; cwd?: string; timeoutMs?: number },
|
||||
) => {
|
||||
const key = argv.join(" ");
|
||||
|
||||
if (key === `git -C ${tempDir} rev-parse --show-toplevel`) {
|
||||
return { stdout: tempDir, stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse HEAD`) {
|
||||
return { stdout: "abc123", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse --abbrev-ref HEAD`) {
|
||||
return { stdout: "main", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} status --porcelain -- :!dist/control-ui/`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse --abbrev-ref --symbolic-full-name @{upstream}`) {
|
||||
return { stdout: "origin/main", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} fetch --all --prune --tags`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-parse @{upstream}`) {
|
||||
return { stdout: upstreamSha, stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rev-list --max-count=10 ${upstreamSha}`) {
|
||||
return { stdout: `${upstreamSha}\n`, stderr: "", code: 0 };
|
||||
}
|
||||
if (key === "pnpm --version") {
|
||||
return { stdout: "10.0.0", stderr: "", code: 0 };
|
||||
}
|
||||
if (
|
||||
key.startsWith(`git -C ${tempDir} worktree add --detach /tmp/`) &&
|
||||
key.endsWith(` ${upstreamSha}`) &&
|
||||
preflightPrefixPattern.test(key)
|
||||
) {
|
||||
return { stdout: `HEAD is now at ${upstreamSha}`, stderr: "", code: 0 };
|
||||
}
|
||||
if (
|
||||
key.startsWith("git -C /tmp/") &&
|
||||
preflightPrefixPattern.test(key) &&
|
||||
key.includes(" checkout --detach ") &&
|
||||
key.endsWith(upstreamSha)
|
||||
) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (
|
||||
key === "pnpm install --ignore-scripts" ||
|
||||
key === "pnpm lint" ||
|
||||
key === "pnpm ui:build"
|
||||
) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === "pnpm build") {
|
||||
buildNodeOptions.push(options?.env?.NODE_OPTIONS ?? "");
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (
|
||||
key.startsWith(`git -C ${tempDir} worktree remove --force /tmp/`) &&
|
||||
preflightPrefixPattern.test(key)
|
||||
) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} worktree prune`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === `git -C ${tempDir} rebase ${upstreamSha}`) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (key === doctorCommand) {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
};
|
||||
|
||||
const result = await runWithCommand(runCommand, { channel: "dev" });
|
||||
|
||||
expect(result.status).toBe("ok");
|
||||
expect(buildNodeOptions).toHaveLength(2);
|
||||
expect(buildNodeOptions).toEqual(["--max-old-space-size=4096", "--max-old-space-size=4096"]);
|
||||
} finally {
|
||||
platformSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not fall back to npm scripts when a pnpm repo cannot bootstrap pnpm", async () => {
|
||||
await setupGitPackageManagerFixture();
|
||||
const calls: string[] = [];
|
||||
|
||||
@@ -108,6 +108,7 @@ const PREFLIGHT_TEMP_PREFIX =
|
||||
process.platform === "win32" ? "ocu-pf-" : "openclaw-update-preflight-";
|
||||
const PREFLIGHT_WORKTREE_DIRNAME = process.platform === "win32" ? "wt" : "worktree";
|
||||
const WINDOWS_PREFLIGHT_BASE_DIR = "ocu";
|
||||
const WINDOWS_BUILD_MAX_OLD_SPACE_MB = 4096;
|
||||
|
||||
function normalizeDir(value?: string | null) {
|
||||
if (!value) {
|
||||
@@ -362,6 +363,35 @@ function shouldPreferIgnoreScriptsForWindowsPreflight(manager: "pnpm" | "bun" |
|
||||
return process.platform === "win32" && manager === "pnpm";
|
||||
}
|
||||
|
||||
function resolveWindowsBuildNodeOptions(baseOptions: string | undefined): string {
|
||||
const current = baseOptions?.trim() ?? "";
|
||||
const desired = `--max-old-space-size=${WINDOWS_BUILD_MAX_OLD_SPACE_MB}`;
|
||||
const existingMatch = /(?:^|\s)--max-old-space-size=(\d+)(?=\s|$)/.exec(current);
|
||||
if (!existingMatch) {
|
||||
return current ? `${current} ${desired}` : desired;
|
||||
}
|
||||
const existingValue = Number(existingMatch[1]);
|
||||
if (Number.isFinite(existingValue) && existingValue >= WINDOWS_BUILD_MAX_OLD_SPACE_MB) {
|
||||
return current;
|
||||
}
|
||||
return current.replace(/(?:^|\s)--max-old-space-size=\d+(?=\s|$)/, ` ${desired}`).trim();
|
||||
}
|
||||
|
||||
function resolveWindowsBuildEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv | undefined {
|
||||
if (process.platform !== "win32") {
|
||||
return env;
|
||||
}
|
||||
const currentNodeOptions = env?.NODE_OPTIONS ?? process.env.NODE_OPTIONS;
|
||||
const nextNodeOptions = resolveWindowsBuildNodeOptions(currentNodeOptions);
|
||||
if (nextNodeOptions === currentNodeOptions) {
|
||||
return env;
|
||||
}
|
||||
return {
|
||||
...env,
|
||||
NODE_OPTIONS: nextNodeOptions,
|
||||
};
|
||||
}
|
||||
|
||||
function isSupersededInstallFailure(
|
||||
step: UpdateStepResult,
|
||||
steps: readonly UpdateStepResult[],
|
||||
@@ -720,7 +750,7 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
`preflight build (${shortSha})`,
|
||||
managerScriptArgs(manager.manager, "build"),
|
||||
worktreeDir,
|
||||
manager.env,
|
||||
resolveWindowsBuildEnv(manager.env),
|
||||
),
|
||||
);
|
||||
steps.push(buildStep);
|
||||
@@ -910,7 +940,12 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
||||
}
|
||||
|
||||
const buildStep = await runStep(
|
||||
step("build", managerScriptArgs(manager.manager, "build"), gitRoot, manager.env),
|
||||
step(
|
||||
"build",
|
||||
managerScriptArgs(manager.manager, "build"),
|
||||
gitRoot,
|
||||
resolveWindowsBuildEnv(manager.env),
|
||||
),
|
||||
);
|
||||
steps.push(buildStep);
|
||||
if (buildStep.exitCode !== 0) {
|
||||
|
||||
72
test/scripts/build-all.test.ts
Normal file
72
test/scripts/build-all.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { BUILD_ALL_STEPS, resolveBuildAllStep } from "../../scripts/build-all.mjs";
|
||||
|
||||
describe("resolveBuildAllStep", () => {
|
||||
it("routes pnpm steps through the npm_execpath pnpm runner on Windows", () => {
|
||||
const step = BUILD_ALL_STEPS.find((entry) => entry.label === "canvas:a2ui:bundle");
|
||||
expect(step).toBeTruthy();
|
||||
|
||||
const result = resolveBuildAllStep(step, {
|
||||
platform: "win32",
|
||||
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
|
||||
npmExecPath: "C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs",
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
command: "C:\\Program Files\\nodejs\\node.exe",
|
||||
args: ["C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs", "canvas:a2ui:bundle"],
|
||||
options: {
|
||||
stdio: "inherit",
|
||||
env: {},
|
||||
shell: false,
|
||||
windowsVerbatimArguments: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps node steps on the current node binary", () => {
|
||||
const step = BUILD_ALL_STEPS.find((entry) => entry.label === "runtime-postbuild");
|
||||
expect(step).toBeTruthy();
|
||||
|
||||
const result = resolveBuildAllStep(step, {
|
||||
nodeExecPath: "/custom/node",
|
||||
env: { FOO: "bar" },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
command: "/custom/node",
|
||||
args: ["scripts/runtime-postbuild.mjs"],
|
||||
options: {
|
||||
stdio: "inherit",
|
||||
env: { FOO: "bar" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("adds heap headroom for plugin-sdk dts on Windows", () => {
|
||||
const step = BUILD_ALL_STEPS.find((entry) => entry.label === "build:plugin-sdk:dts");
|
||||
expect(step).toBeTruthy();
|
||||
|
||||
const result = resolveBuildAllStep(step, {
|
||||
platform: "win32",
|
||||
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
|
||||
npmExecPath: "C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs",
|
||||
env: { FOO: "bar" },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
command: "C:\\Program Files\\nodejs\\node.exe",
|
||||
args: ["C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs", "build:plugin-sdk:dts"],
|
||||
options: {
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
FOO: "bar",
|
||||
NODE_OPTIONS: "--max-old-space-size=4096",
|
||||
},
|
||||
shell: false,
|
||||
windowsVerbatimArguments: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
66
test/scripts/stage-bundled-plugin-runtime.test.ts
Normal file
66
test/scripts/stage-bundled-plugin-runtime.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { stageBundledPluginRuntime } from "../../scripts/stage-bundled-plugin-runtime.mjs";
|
||||
|
||||
async function withTempDir(run: (dir: string) => Promise<void>) {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-stage-runtime-"));
|
||||
try {
|
||||
await run(dir);
|
||||
} finally {
|
||||
await fs.promises.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe("stageBundledPluginRuntime", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("copies files when Windows rejects runtime overlay symlinks", async () => {
|
||||
await withTempDir(async (repoRoot) => {
|
||||
const sourceFile = path.join(
|
||||
repoRoot,
|
||||
"dist",
|
||||
"extensions",
|
||||
"acpx",
|
||||
"skills",
|
||||
"acp-router",
|
||||
"SKILL.md",
|
||||
);
|
||||
await fs.promises.mkdir(path.dirname(sourceFile), { recursive: true });
|
||||
await fs.promises.writeFile(sourceFile, "skill-body\n", "utf8");
|
||||
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const symlinkSpy = vi
|
||||
.spyOn(fs, "symlinkSync")
|
||||
.mockImplementation((target, targetPath, type) => {
|
||||
if (
|
||||
String(targetPath).includes(`${path.sep}dist-runtime${path.sep}`) &&
|
||||
type !== "junction"
|
||||
) {
|
||||
const error = new Error("no symlink privilege");
|
||||
Object.assign(error, { code: "EPERM" });
|
||||
throw error;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
stageBundledPluginRuntime({ repoRoot });
|
||||
|
||||
const runtimeFile = path.join(
|
||||
repoRoot,
|
||||
"dist-runtime",
|
||||
"extensions",
|
||||
"acpx",
|
||||
"skills",
|
||||
"acp-router",
|
||||
"SKILL.md",
|
||||
);
|
||||
expect(await fs.promises.readFile(runtimeFile, "utf8")).toBe("skill-body\n");
|
||||
expect(fs.lstatSync(runtimeFile).isSymbolicLink()).toBe(false);
|
||||
expect(symlinkSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
33
test/scripts/tsdown-build.test.ts
Normal file
33
test/scripts/tsdown-build.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveTsdownBuildInvocation } from "../../scripts/tsdown-build.mjs";
|
||||
|
||||
describe("resolveTsdownBuildInvocation", () => {
|
||||
it("routes Windows tsdown builds through the pnpm runner instead of shell=true", () => {
|
||||
const result = resolveTsdownBuildInvocation({
|
||||
platform: "win32",
|
||||
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
|
||||
npmExecPath: "C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs",
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
command: "C:\\Program Files\\nodejs\\node.exe",
|
||||
args: [
|
||||
"C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs",
|
||||
"exec",
|
||||
"tsdown",
|
||||
"--config-loader",
|
||||
"unrun",
|
||||
"--logLevel",
|
||||
"warn",
|
||||
],
|
||||
options: {
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
shell: false,
|
||||
windowsVerbatimArguments: undefined,
|
||||
env: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user