fix(crabbox): bootstrap bun on aws macos

This commit is contained in:
Vincent Koc
2026-06-18 19:00:00 +02:00
parent 4e0376255f
commit 8b34b8537a
2 changed files with 138 additions and 21 deletions

View File

@@ -128,8 +128,19 @@ function spawnInvocation(command, commandArgs, env, platform) {
}
const cmdMetaCharactersRe = /([()\][%!^"`<>&|;, *?])/g;
const jsRuntimeEntrypoints = new Set(["pnpm", "npm", "npx", "corepack", "node", "yarn", "bun"]);
const jsRuntimeEntrypoints = new Set([
"pnpm",
"npm",
"npx",
"corepack",
"node",
"yarn",
"bun",
"bunx",
]);
const awsMacosCorepackEntrypoints = new Set(["pnpm", "yarn", "corepack"]);
const awsMacosBunEntrypoints = new Set(["bun", "bunx"]);
const awsMacosBunVersion = "1.3.14";
const minimumBlacksmithCrabboxVersion = [0, 22, 0];
const shellControlCommandPrefixes = new Set([
"if",
@@ -785,17 +796,27 @@ function commandNeedsAwsMacosPackageManager(commandArgs) {
if (isChangedGateCommand(commandArgs)) {
return true;
}
if (commandArgs.length === 1) {
return shellCommandWordCandidates(commandArgs[0]).some(commandWordsNeedAwsMacosPackageManager);
}
return commandWordsNeedAwsMacosPackageManager(normalizedCommandWords(commandArgs));
return commandNeedsEntrypoint(commandArgs, awsMacosCorepackEntrypoints);
}
function commandWordsNeedAwsMacosPackageManager(wordsInput) {
function commandNeedsAwsMacosBun(commandArgs) {
return commandNeedsEntrypoint(commandArgs, awsMacosBunEntrypoints);
}
function commandNeedsEntrypoint(commandArgs, entrypoints) {
if (commandArgs.length === 1) {
return shellCommandWordCandidates(commandArgs[0]).some((words) =>
commandWordsNeedEntrypoint(words, entrypoints),
);
}
return commandWordsNeedEntrypoint(normalizedCommandWords(commandArgs), entrypoints);
}
function commandWordsNeedEntrypoint(wordsInput, entrypoints) {
let words = wordsInput;
words = normalizeExecutableWords(words);
const first = (words[0] ?? "").split("/").pop();
if (awsMacosCorepackEntrypoints.has(first)) {
if (entrypoints.has(first)) {
return true;
}
@@ -803,7 +824,9 @@ function commandWordsNeedAwsMacosPackageManager(wordsInput) {
if (!inlineCommand) {
return false;
}
return shellCommandWordCandidates(inlineCommand).some(commandWordsNeedAwsMacosPackageManager);
return shellCommandWordCandidates(inlineCommand).some((candidateWords) =>
commandWordsNeedEntrypoint(candidateWords, entrypoints),
);
}
function isChangedGateCommand(commandArgs) {
@@ -1668,7 +1691,7 @@ function injectRemoteChangedGateGitBootstrap(commandArgs, changedGateBase) {
return normalizedArgs;
}
function remoteAwsMacosJsBootstrap({ packageManager = false } = {}) {
function remoteAwsMacosJsBootstrap({ packageManager = false, bun = false } = {}) {
const nodeVersion = process.env.OPENCLAW_CRABBOX_MACOS_NODE_VERSION?.trim() || "24.15.0";
const bootstrap = [
"openclaw_crabbox_bootstrap_macos_js() {",
@@ -1750,6 +1773,42 @@ function remoteAwsMacosJsBootstrap({ packageManager = false } = {}) {
"pnpm --version >&2;",
);
}
// Raw AWS macOS boxes skip setup-node-env, so Bun needs its own user-local pin.
if (bun) {
bootstrap.push(
`bun_version=${shellQuote(awsMacosBunVersion)};`,
'bun_root="$tool_root/bun-v${bun_version}";',
'bun_ready_marker="$bun_root/.openclaw-crabbox-bun-ready";',
'export PATH="$bun_root/bin:$PATH";',
'if [ ! -x "$bun_root/bin/bun" ] || [ ! -f "$bun_ready_marker" ]; then',
'mkdir -p "$tool_root" || { status=$?; return "$status"; };',
'bun_install_lock="$tool_root/.bun-${bun_version}.lock";',
"bun_lock_acquired=0;",
"bun_lock_deadline=$((SECONDS + 300));",
"while true; do",
'if mkdir "$bun_install_lock" 2>/dev/null; then bun_lock_acquired=1; printf "%s\\n" "$$" >"$bun_install_lock/pid" || { status=$?; rm -rf "$bun_install_lock"; return "$status"; }; break; fi;',
'if [ -x "$bun_root/bin/bun" ] && [ -f "$bun_ready_marker" ]; then break; fi;',
'if [ "$SECONDS" -ge "$bun_lock_deadline" ]; then',
'bun_lock_pid="$(cat "$bun_install_lock/pid" 2>/dev/null || true)";',
'if [ -n "$bun_lock_pid" ] && kill -0 "$bun_lock_pid" 2>/dev/null; then echo "timed out waiting for active macOS Bun install lock: $bun_install_lock pid=$bun_lock_pid" >&2; return 1; fi;',
'echo "reclaiming stale macOS Bun install lock: $bun_install_lock" >&2;',
'rm -rf "$bun_install_lock" || return 1;',
"bun_lock_deadline=$((SECONDS + 300));",
"fi;",
"sleep 1;",
"done;",
'release_bun_install_lock() { if [ "$bun_lock_acquired" = "1" ]; then rm -rf "$bun_install_lock" 2>/dev/null || true; fi; };',
'if [ ! -x "$bun_root/bin/bun" ] || [ ! -f "$bun_ready_marker" ]; then',
'rm -rf "$bun_root" || { status=$?; release_bun_install_lock; return "$status"; };',
'mkdir -p "$bun_root" || { status=$?; release_bun_install_lock; return "$status"; };',
'npm install --global --prefix "$bun_root" "bun@${bun_version}" || { status=$?; release_bun_install_lock; return "$status"; };',
'touch "$bun_ready_marker" || { status=$?; release_bun_install_lock; return "$status"; };',
"fi;",
"release_bun_install_lock;",
"fi;",
"bun --version >&2 || return 1;",
);
}
bootstrap.push("};", "openclaw_crabbox_bootstrap_macos_js");
return bootstrap.join(" ");
}
@@ -1775,6 +1834,7 @@ function scopedAwsMacosEnvCommand(commandArgs) {
return {
runtimeEntrypoint: targetEntrypoint,
packageManager: awsMacosCorepackEntrypoints.has(targetEntrypoint),
bun: awsMacosBunEntrypoints.has(targetEntrypoint),
shellCommand: `openclaw_crabbox_env ${shellJoin(commandArgs.slice(1))}`,
};
}
@@ -1805,6 +1865,7 @@ function injectRemoteAwsMacosJsBootstrap(commandArgs, providerName) {
const shellCommand = `${remoteAwsMacosJsBootstrap({
packageManager:
directScopedEnvCommand?.packageManager || commandNeedsAwsMacosPackageManager(runArgs),
bun: directScopedEnvCommand?.bun || commandNeedsAwsMacosBun(runArgs),
})} && { ${originalShellCommand}\n}`;
if (!hasOption(normalizedArgs, "--shell")) {
@@ -1876,13 +1937,13 @@ function prepareAwsMacosScriptStdinBootstrap(commandArgs, providerName) {
}
function createAwsMacosScriptStdinWrapper(script) {
const packageManager = scriptNeedsAwsMacosPackageManager(script);
const requirements = awsMacosScriptBootstrapRequirements(script);
if (!script.startsWith("#!")) {
return `${remoteAwsMacosJsBootstrap({ packageManager })} || exit $?\n${script}`;
return `${remoteAwsMacosJsBootstrap(requirements)} || exit $?\n${script}`;
}
const delimiterValue = uniqueHereDocDelimiter(script);
return [
`${remoteAwsMacosJsBootstrap({ packageManager })} || exit $?`,
`${remoteAwsMacosJsBootstrap(requirements)} || exit $?`,
'tmp_script="$(mktemp "${TMPDIR:-/tmp}/openclaw-crabbox-script.XXXXXX")" || exit $?',
'cleanup_openclaw_crabbox_script() { rm -f "$tmp_script"; }',
"trap cleanup_openclaw_crabbox_script EXIT",
@@ -1895,7 +1956,8 @@ function createAwsMacosScriptStdinWrapper(script) {
].join("\n");
}
function scriptNeedsAwsMacosPackageManager(script) {
function awsMacosScriptBootstrapRequirements(script) {
const requirements = { packageManager: false, bun: false };
const firstLine = script.match(/^[^\r\n]*/u)?.[0] ?? "";
if (firstLine.startsWith("#!")) {
let words = firstLine.slice(2).trim().split(/\s+/u).filter(Boolean);
@@ -1905,11 +1967,13 @@ function scriptNeedsAwsMacosPackageManager(script) {
words = words.slice(1);
}
}
if (commandWordsNeedAwsMacosPackageManager(words)) {
return true;
}
requirements.packageManager = commandWordsNeedEntrypoint(words, awsMacosCorepackEntrypoints);
requirements.bun = commandWordsNeedEntrypoint(words, awsMacosBunEntrypoints);
return requirements;
}
return commandNeedsAwsMacosPackageManager([script]);
requirements.packageManager = commandNeedsAwsMacosPackageManager([script]);
requirements.bun = commandNeedsAwsMacosBun([script]);
return requirements;
}
function uniqueHereDocDelimiter(script) {
@@ -2369,7 +2433,7 @@ if (
) {
if (isAwsMacosRemoteTarget(normalizedArgs, provider)) {
console.error(
`[crabbox] provider=aws macOS raw boxes may lack Node/Corepack/pnpm for ${runtimeEntrypoint || "--script-stdin"}; bootstrapping a pinned user-local Node toolchain before the command`,
`[crabbox] provider=aws macOS raw boxes may lack Node/Corepack/pnpm/Bun for ${runtimeEntrypoint || "--script-stdin"}; bootstrapping pinned user-local JavaScript tooling before the command`,
);
} else {
const id = optionValue(normalizedArgs, "--id");
@@ -2377,7 +2441,7 @@ if (
? `pnpm crabbox:hydrate -- --id ${id}`
: "pnpm crabbox:warmup, then pnpm crabbox:hydrate -- --id <id>";
console.error(
`[crabbox] warning: provider=aws raw boxes may lack Node/Corepack/pnpm for ${runtimeEntrypoint}; hydrate first (${hydrate}) or pass --provider blacksmith-testbox for OpenClaw CI-like proof; not switching providers automatically`,
`[crabbox] warning: provider=aws raw boxes may lack Node/Corepack/pnpm/Bun for ${runtimeEntrypoint}; hydrate first (${hydrate}) or pass --provider blacksmith-testbox for OpenClaw CI-like proof; not switching providers automatically`,
);
}
}

View File

@@ -780,6 +780,41 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
expectGroupedShellCommand(remoteCommand, "node --version");
});
it("bootstraps Bun for raw AWS macOS bun commands", () => {
const result = runWrapper(
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
["run", "--provider", "aws", "--target", "macos", "--", "bun", "--version"],
);
const output = parseFakeCrabboxOutput(result);
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
expect(result.status).toBe(0);
expect(output.args).toContain("--shell");
expect(result.stderr).toContain("Node/Corepack/pnpm/Bun");
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
expect(remoteCommand).toContain("bun_version=1.3.14");
expect(remoteCommand).toContain('bun_root="$tool_root/bun-v${bun_version}"');
expect(remoteCommand).toContain(
'npm install --global --prefix "$bun_root" "bun@${bun_version}"',
);
expect(remoteCommand).toContain("bun --version >&2 || return 1");
expect(remoteCommand).not.toContain("corepack enable");
expectGroupedShellCommand(remoteCommand, "bun --version");
});
it("bootstraps Bun for raw AWS macOS env-prefixed bun commands", () => {
const result = runWrapper(
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
["run", "--provider", "aws", "--target", "macos", "--", "env", "-i", "bun", "--version"],
);
const output = parseFakeCrabboxOutput(result);
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
expect(result.status).toBe(0);
expect(remoteCommand).toContain("bun --version >&2 || return 1");
expectGroupedShellCommand(remoteCommand, "openclaw_crabbox_env -i bun --version");
});
it("bootstraps Corepack for raw AWS macOS pnpm commands", () => {
const result = runWrapper(
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
@@ -791,7 +826,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
expect(result.status).toBe(0);
expect(output.args).toContain("--shell");
expect(result.stderr).toContain(
"bootstrapping a pinned user-local Node toolchain before the command",
"bootstrapping pinned user-local JavaScript tooling before the command",
);
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
expect(remoteCommand).toContain("node-v${node_version}-darwin-${node_arch}.tar.gz");
@@ -1178,7 +1213,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
expect(output.args).not.toContain("--script-stdin");
expect(output.args).toContain("--script");
expect(result.stderr).toContain(
"bootstrapping a pinned user-local Node toolchain before the command",
"bootstrapping pinned user-local JavaScript tooling before the command",
);
expect(output.scriptContent).toContain("openclaw_crabbox_bootstrap_macos_js");
expect(output.scriptContent).toContain('if [ ! -d "$TMPDIR" ]; then mkdir -p "$TMPDIR"');
@@ -1210,6 +1245,24 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
expect(output.args.at(-1)).toBe("arg1");
});
it("bootstraps Bun for AWS macOS script-stdin bun shebangs", () => {
const script = ["#!/usr/bin/env bun", "console.log(Bun.version);"].join("\n");
const result = runWrapper(
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
["run", "--provider", "aws", "--target", "macos", "--script-stdin"],
{ input: script },
);
const output = parseFakeCrabboxOutput(result);
expect(result.status).toBe(0);
expect(output.scriptContent).toContain("bun_version=1.3.14");
expect(output.scriptContent).toContain(
'npm install --global --prefix "$bun_root" "bun@${bun_version}"',
);
expect(output.scriptContent).toContain("bun --version >&2 || return 1");
expect(output.scriptContent).not.toContain("corepack enable");
});
it("does not treat run option values as AWS macOS script-stdin flags", () => {
const result = runWrapper(
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",