mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 02:22:46 +00:00
fix(crabbox): bootstrap bun on aws macos
This commit is contained in:
@@ -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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user