Skip to content

Commit 8b34b85

Browse files
committed
fix(crabbox): bootstrap bun on aws macos
1 parent 4e03762 commit 8b34b85

2 files changed

Lines changed: 136 additions & 19 deletions

File tree

scripts/crabbox-wrapper.mjs

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,19 @@ function spawnInvocation(command, commandArgs, env, platform) {
128128
}
129129

130130
const cmdMetaCharactersRe = /([()\][%!^"`<>&|;, *?])/g;
131-
const jsRuntimeEntrypoints = new Set(["pnpm", "npm", "npx", "corepack", "node", "yarn", "bun"]);
131+
const jsRuntimeEntrypoints = new Set([
132+
"pnpm",
133+
"npm",
134+
"npx",
135+
"corepack",
136+
"node",
137+
"yarn",
138+
"bun",
139+
"bunx",
140+
]);
132141
const awsMacosCorepackEntrypoints = new Set(["pnpm", "yarn", "corepack"]);
142+
const awsMacosBunEntrypoints = new Set(["bun", "bunx"]);
143+
const awsMacosBunVersion = "1.3.14";
133144
const minimumBlacksmithCrabboxVersion = [0, 22, 0];
134145
const shellControlCommandPrefixes = new Set([
135146
"if",
@@ -785,25 +796,37 @@ function commandNeedsAwsMacosPackageManager(commandArgs) {
785796
if (isChangedGateCommand(commandArgs)) {
786797
return true;
787798
}
799+
return commandNeedsEntrypoint(commandArgs, awsMacosCorepackEntrypoints);
800+
}
801+
802+
function commandNeedsAwsMacosBun(commandArgs) {
803+
return commandNeedsEntrypoint(commandArgs, awsMacosBunEntrypoints);
804+
}
805+
806+
function commandNeedsEntrypoint(commandArgs, entrypoints) {
788807
if (commandArgs.length === 1) {
789-
return shellCommandWordCandidates(commandArgs[0]).some(commandWordsNeedAwsMacosPackageManager);
808+
return shellCommandWordCandidates(commandArgs[0]).some((words) =>
809+
commandWordsNeedEntrypoint(words, entrypoints),
810+
);
790811
}
791-
return commandWordsNeedAwsMacosPackageManager(normalizedCommandWords(commandArgs));
812+
return commandWordsNeedEntrypoint(normalizedCommandWords(commandArgs), entrypoints);
792813
}
793814

794-
function commandWordsNeedAwsMacosPackageManager(wordsInput) {
815+
function commandWordsNeedEntrypoint(wordsInput, entrypoints) {
795816
let words = wordsInput;
796817
words = normalizeExecutableWords(words);
797818
const first = (words[0] ?? "").split("/").pop();
798-
if (awsMacosCorepackEntrypoints.has(first)) {
819+
if (entrypoints.has(first)) {
799820
return true;
800821
}
801822

802823
const inlineCommand = shellInlineCommand(words);
803824
if (!inlineCommand) {
804825
return false;
805826
}
806-
return shellCommandWordCandidates(inlineCommand).some(commandWordsNeedAwsMacosPackageManager);
827+
return shellCommandWordCandidates(inlineCommand).some((candidateWords) =>
828+
commandWordsNeedEntrypoint(candidateWords, entrypoints),
829+
);
807830
}
808831

809832
function isChangedGateCommand(commandArgs) {
@@ -1668,7 +1691,7 @@ function injectRemoteChangedGateGitBootstrap(commandArgs, changedGateBase) {
16681691
return normalizedArgs;
16691692
}
16701693

1671-
function remoteAwsMacosJsBootstrap({ packageManager = false } = {}) {
1694+
function remoteAwsMacosJsBootstrap({ packageManager = false, bun = false } = {}) {
16721695
const nodeVersion = process.env.OPENCLAW_CRABBOX_MACOS_NODE_VERSION?.trim() || "24.15.0";
16731696
const bootstrap = [
16741697
"openclaw_crabbox_bootstrap_macos_js() {",
@@ -1750,6 +1773,42 @@ function remoteAwsMacosJsBootstrap({ packageManager = false } = {}) {
17501773
"pnpm --version >&2;",
17511774
);
17521775
}
1776+
// Raw AWS macOS boxes skip setup-node-env, so Bun needs its own user-local pin.
1777+
if (bun) {
1778+
bootstrap.push(
1779+
`bun_version=${shellQuote(awsMacosBunVersion)};`,
1780+
'bun_root="$tool_root/bun-v${bun_version}";',
1781+
'bun_ready_marker="$bun_root/.openclaw-crabbox-bun-ready";',
1782+
'export PATH="$bun_root/bin:$PATH";',
1783+
'if [ ! -x "$bun_root/bin/bun" ] || [ ! -f "$bun_ready_marker" ]; then',
1784+
'mkdir -p "$tool_root" || { status=$?; return "$status"; };',
1785+
'bun_install_lock="$tool_root/.bun-${bun_version}.lock";',
1786+
"bun_lock_acquired=0;",
1787+
"bun_lock_deadline=$((SECONDS + 300));",
1788+
"while true; do",
1789+
'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;',
1790+
'if [ -x "$bun_root/bin/bun" ] && [ -f "$bun_ready_marker" ]; then break; fi;',
1791+
'if [ "$SECONDS" -ge "$bun_lock_deadline" ]; then',
1792+
'bun_lock_pid="$(cat "$bun_install_lock/pid" 2>/dev/null || true)";',
1793+
'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;',
1794+
'echo "reclaiming stale macOS Bun install lock: $bun_install_lock" >&2;',
1795+
'rm -rf "$bun_install_lock" || return 1;',
1796+
"bun_lock_deadline=$((SECONDS + 300));",
1797+
"fi;",
1798+
"sleep 1;",
1799+
"done;",
1800+
'release_bun_install_lock() { if [ "$bun_lock_acquired" = "1" ]; then rm -rf "$bun_install_lock" 2>/dev/null || true; fi; };',
1801+
'if [ ! -x "$bun_root/bin/bun" ] || [ ! -f "$bun_ready_marker" ]; then',
1802+
'rm -rf "$bun_root" || { status=$?; release_bun_install_lock; return "$status"; };',
1803+
'mkdir -p "$bun_root" || { status=$?; release_bun_install_lock; return "$status"; };',
1804+
'npm install --global --prefix "$bun_root" "bun@${bun_version}" || { status=$?; release_bun_install_lock; return "$status"; };',
1805+
'touch "$bun_ready_marker" || { status=$?; release_bun_install_lock; return "$status"; };',
1806+
"fi;",
1807+
"release_bun_install_lock;",
1808+
"fi;",
1809+
"bun --version >&2 || return 1;",
1810+
);
1811+
}
17531812
bootstrap.push("};", "openclaw_crabbox_bootstrap_macos_js");
17541813
return bootstrap.join(" ");
17551814
}
@@ -1775,6 +1834,7 @@ function scopedAwsMacosEnvCommand(commandArgs) {
17751834
return {
17761835
runtimeEntrypoint: targetEntrypoint,
17771836
packageManager: awsMacosCorepackEntrypoints.has(targetEntrypoint),
1837+
bun: awsMacosBunEntrypoints.has(targetEntrypoint),
17781838
shellCommand: `openclaw_crabbox_env ${shellJoin(commandArgs.slice(1))}`,
17791839
};
17801840
}
@@ -1805,6 +1865,7 @@ function injectRemoteAwsMacosJsBootstrap(commandArgs, providerName) {
18051865
const shellCommand = `${remoteAwsMacosJsBootstrap({
18061866
packageManager:
18071867
directScopedEnvCommand?.packageManager || commandNeedsAwsMacosPackageManager(runArgs),
1868+
bun: directScopedEnvCommand?.bun || commandNeedsAwsMacosBun(runArgs),
18081869
})} && { ${originalShellCommand}\n}`;
18091870

18101871
if (!hasOption(normalizedArgs, "--shell")) {
@@ -1876,13 +1937,13 @@ function prepareAwsMacosScriptStdinBootstrap(commandArgs, providerName) {
18761937
}
18771938

18781939
function createAwsMacosScriptStdinWrapper(script) {
1879-
const packageManager = scriptNeedsAwsMacosPackageManager(script);
1940+
const requirements = awsMacosScriptBootstrapRequirements(script);
18801941
if (!script.startsWith("#!")) {
1881-
return `${remoteAwsMacosJsBootstrap({ packageManager })} || exit $?\n${script}`;
1942+
return `${remoteAwsMacosJsBootstrap(requirements)} || exit $?\n${script}`;
18821943
}
18831944
const delimiterValue = uniqueHereDocDelimiter(script);
18841945
return [
1885-
`${remoteAwsMacosJsBootstrap({ packageManager })} || exit $?`,
1946+
`${remoteAwsMacosJsBootstrap(requirements)} || exit $?`,
18861947
'tmp_script="$(mktemp "${TMPDIR:-/tmp}/openclaw-crabbox-script.XXXXXX")" || exit $?',
18871948
'cleanup_openclaw_crabbox_script() { rm -f "$tmp_script"; }',
18881949
"trap cleanup_openclaw_crabbox_script EXIT",
@@ -1895,7 +1956,8 @@ function createAwsMacosScriptStdinWrapper(script) {
18951956
].join("\n");
18961957
}
18971958

1898-
function scriptNeedsAwsMacosPackageManager(script) {
1959+
function awsMacosScriptBootstrapRequirements(script) {
1960+
const requirements = { packageManager: false, bun: false };
18991961
const firstLine = script.match(/^[^\r\n]*/u)?.[0] ?? "";
19001962
if (firstLine.startsWith("#!")) {
19011963
let words = firstLine.slice(2).trim().split(/\s+/u).filter(Boolean);
@@ -1905,11 +1967,13 @@ function scriptNeedsAwsMacosPackageManager(script) {
19051967
words = words.slice(1);
19061968
}
19071969
}
1908-
if (commandWordsNeedAwsMacosPackageManager(words)) {
1909-
return true;
1910-
}
1970+
requirements.packageManager = commandWordsNeedEntrypoint(words, awsMacosCorepackEntrypoints);
1971+
requirements.bun = commandWordsNeedEntrypoint(words, awsMacosBunEntrypoints);
1972+
return requirements;
19111973
}
1912-
return commandNeedsAwsMacosPackageManager([script]);
1974+
requirements.packageManager = commandNeedsAwsMacosPackageManager([script]);
1975+
requirements.bun = commandNeedsAwsMacosBun([script]);
1976+
return requirements;
19131977
}
19141978

19151979
function uniqueHereDocDelimiter(script) {
@@ -2369,15 +2433,15 @@ if (
23692433
) {
23702434
if (isAwsMacosRemoteTarget(normalizedArgs, provider)) {
23712435
console.error(
2372-
`[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`,
2436+
`[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`,
23732437
);
23742438
} else {
23752439
const id = optionValue(normalizedArgs, "--id");
23762440
const hydrate = id
23772441
? `pnpm crabbox:hydrate -- --id ${id}`
23782442
: "pnpm crabbox:warmup, then pnpm crabbox:hydrate -- --id <id>";
23792443
console.error(
2380-
`[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`,
2444+
`[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`,
23812445
);
23822446
}
23832447
}

test/scripts/crabbox-wrapper.test.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,41 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
780780
expectGroupedShellCommand(remoteCommand, "node --version");
781781
});
782782

783+
it("bootstraps Bun for raw AWS macOS bun commands", () => {
784+
const result = runWrapper(
785+
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
786+
["run", "--provider", "aws", "--target", "macos", "--", "bun", "--version"],
787+
);
788+
789+
const output = parseFakeCrabboxOutput(result);
790+
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
791+
expect(result.status).toBe(0);
792+
expect(output.args).toContain("--shell");
793+
expect(result.stderr).toContain("Node/Corepack/pnpm/Bun");
794+
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
795+
expect(remoteCommand).toContain("bun_version=1.3.14");
796+
expect(remoteCommand).toContain('bun_root="$tool_root/bun-v${bun_version}"');
797+
expect(remoteCommand).toContain(
798+
'npm install --global --prefix "$bun_root" "bun@${bun_version}"',
799+
);
800+
expect(remoteCommand).toContain("bun --version >&2 || return 1");
801+
expect(remoteCommand).not.toContain("corepack enable");
802+
expectGroupedShellCommand(remoteCommand, "bun --version");
803+
});
804+
805+
it("bootstraps Bun for raw AWS macOS env-prefixed bun commands", () => {
806+
const result = runWrapper(
807+
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
808+
["run", "--provider", "aws", "--target", "macos", "--", "env", "-i", "bun", "--version"],
809+
);
810+
811+
const output = parseFakeCrabboxOutput(result);
812+
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
813+
expect(result.status).toBe(0);
814+
expect(remoteCommand).toContain("bun --version >&2 || return 1");
815+
expectGroupedShellCommand(remoteCommand, "openclaw_crabbox_env -i bun --version");
816+
});
817+
783818
it("bootstraps Corepack for raw AWS macOS pnpm commands", () => {
784819
const result = runWrapper(
785820
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
@@ -791,7 +826,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
791826
expect(result.status).toBe(0);
792827
expect(output.args).toContain("--shell");
793828
expect(result.stderr).toContain(
794-
"bootstrapping a pinned user-local Node toolchain before the command",
829+
"bootstrapping pinned user-local JavaScript tooling before the command",
795830
);
796831
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
797832
expect(remoteCommand).toContain("node-v${node_version}-darwin-${node_arch}.tar.gz");
@@ -1178,7 +1213,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
11781213
expect(output.args).not.toContain("--script-stdin");
11791214
expect(output.args).toContain("--script");
11801215
expect(result.stderr).toContain(
1181-
"bootstrapping a pinned user-local Node toolchain before the command",
1216+
"bootstrapping pinned user-local JavaScript tooling before the command",
11821217
);
11831218
expect(output.scriptContent).toContain("openclaw_crabbox_bootstrap_macos_js");
11841219
expect(output.scriptContent).toContain('if [ ! -d "$TMPDIR" ]; then mkdir -p "$TMPDIR"');
@@ -1210,6 +1245,24 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
12101245
expect(output.args.at(-1)).toBe("arg1");
12111246
});
12121247

1248+
it("bootstraps Bun for AWS macOS script-stdin bun shebangs", () => {
1249+
const script = ["#!/usr/bin/env bun", "console.log(Bun.version);"].join("\n");
1250+
const result = runWrapper(
1251+
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
1252+
["run", "--provider", "aws", "--target", "macos", "--script-stdin"],
1253+
{ input: script },
1254+
);
1255+
1256+
const output = parseFakeCrabboxOutput(result);
1257+
expect(result.status).toBe(0);
1258+
expect(output.scriptContent).toContain("bun_version=1.3.14");
1259+
expect(output.scriptContent).toContain(
1260+
'npm install --global --prefix "$bun_root" "bun@${bun_version}"',
1261+
);
1262+
expect(output.scriptContent).toContain("bun --version >&2 || return 1");
1263+
expect(output.scriptContent).not.toContain("corepack enable");
1264+
});
1265+
12131266
it("does not treat run option values as AWS macOS script-stdin flags", () => {
12141267
const result = runWrapper(
12151268
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",

0 commit comments

Comments
 (0)