fix(crabbox): preflight macOS Swift toolchain

This commit is contained in:
Vincent Koc
2026-06-22 16:34:47 +08:00
parent d9a38130b1
commit f0afbd7e32
2 changed files with 316 additions and 5 deletions

View File

@@ -141,6 +141,14 @@ const jsRuntimeEntrypoints = new Set([
const awsMacosCorepackEntrypoints = new Set(["pnpm", "yarn", "corepack"]);
const awsMacosBunEntrypoints = new Set(["bun", "bunx"]);
const awsMacosBunVersion = "1.3.14";
const awsMacosSwiftEntrypoints = new Set(["swift", "xcodebuild"]);
const awsMacosSwiftScriptTargets = new Set([
"mac:package",
"mac:restart",
"scripts/build-and-run-mac.sh",
"scripts/package-mac-app.sh",
"scripts/restart-mac.sh",
]);
const minimumBlacksmithCrabboxVersion = [0, 22, 0];
const shellControlCommandPrefixes = new Set([
"if",
@@ -955,6 +963,89 @@ function commandNeedsAwsMacosBun(commandArgs) {
return commandNeedsEntrypoint(commandArgs, awsMacosBunEntrypoints);
}
function commandNeedsAwsMacosSwiftToolchain(commandArgs) {
if (commandArgs.length === 1) {
return shellCommandWordCandidates(commandArgs[0]).some(commandWordsNeedAwsMacosSwiftToolchain);
}
return commandWordsNeedAwsMacosSwiftToolchain(normalizedCommandWords(commandArgs));
}
function commandWordsNeedAwsMacosSwiftToolchain(wordsInput) {
let words = wordsInput;
words = normalizeExecutableWords(words);
const first = (words[0] ?? "").split("/").pop();
if (isSupportedSystemEnvCommand(first)) {
const targetWords = [...words];
if (stripEnvCommandOptions(targetWords, { canShimIgnoreEnvironment: true })) {
return commandWordsNeedAwsMacosSwiftToolchain(targetWords);
}
}
if (awsMacosSwiftEntrypoints.has(first)) {
return true;
}
if (first === "pnpm") {
const scriptName = words[1] === "run" ? words[2] : words[1];
if (awsMacosSwiftScriptTargets.has(scriptName)) {
return true;
}
}
if (isAwsMacosSwiftScriptTarget(words[0])) {
return true;
}
if (commandWordsRunAwsMacosSwiftScript(words)) {
return true;
}
const inlineCommand = shellInlineCommand(words);
if (!inlineCommand) {
return false;
}
return shellCommandWordCandidates(inlineCommand).some(commandWordsNeedAwsMacosSwiftToolchain);
}
function isAwsMacosSwiftScriptTarget(word) {
if (!word) {
return false;
}
const normalized = word.replace(/^\.\//u, "");
return (
awsMacosSwiftScriptTargets.has(normalized) ||
awsMacosSwiftScriptTargets.has(normalized.split("/").pop() ?? "")
);
}
function commandWordsRunAwsMacosSwiftScript(words) {
const first = (words[0] ?? "").split("/").pop();
if (!shellInlineCommandInterpreters.has(first)) {
return false;
}
for (let index = 1; index < words.length; index += 1) {
const word = words[index] ?? "";
if (!word) {
return false;
}
if (word === "--") {
continue;
}
if (word === "-c" || /^-[^-]*c/u.test(word)) {
return false;
}
if (shellInlineCommandOptionConsumesNextValue(word)) {
index += 1;
continue;
}
if (word.startsWith("-") || word.startsWith("+")) {
continue;
}
return isAwsMacosSwiftScriptTarget(word);
}
return false;
}
function commandNeedsEntrypoint(commandArgs, entrypoints) {
if (commandArgs.length === 1) {
return shellCommandWordCandidates(commandArgs[0]).some((words) =>
@@ -2078,6 +2169,62 @@ function injectRemoteAwsMacosJsBootstrap(commandArgs, providerName) {
return normalizedArgs;
}
function remoteAwsMacosSwiftBootstrap() {
return [
"openclaw_crabbox_require_macos_swift_62() {",
'openclaw_xcode="";',
'for openclaw_candidate in /Applications/Xcode_26.1.app /Applications/Xcode_26*.app /Applications/Xcode-26*.app; do if [ -d "$openclaw_candidate" ]; then openclaw_xcode="$openclaw_candidate"; fi; done;',
'if [ -n "$openclaw_xcode" ]; then openclaw_developer="$openclaw_xcode/Contents/Developer"; if [ ! -d "$openclaw_developer" ]; then openclaw_developer="$openclaw_xcode"; fi; sudo xcode-select -s "$openclaw_developer" || return 1; fi;',
'openclaw_swift_version="$(swift --version 2>&1)" || { status=$?; printf "%s\\n" "$openclaw_swift_version" >&2; return "$status"; };',
'printf "%s\\n" "$openclaw_swift_version" >&2;',
'openclaw_swift_major_minor="$(printf "%s\\n" "$openclaw_swift_version" | sed -nE "s/.*Apple Swift version ([0-9]+)\\.([0-9]+).*/\\1 \\2/p" | head -n 1)";',
'if [ -z "$openclaw_swift_major_minor" ]; then echo "[crabbox] OpenClaw macOS app proof requires Swift tools 6.2+; unable to parse swift --version." >&2; return 2; fi;',
"set -- $openclaw_swift_major_minor;",
'if [ "$1" -lt 6 ] || { [ "$1" -eq 6 ] && [ "$2" -lt 2 ]; }; then',
'echo "[crabbox] OpenClaw macOS app proof requires Swift tools 6.2+ (Xcode 26.x)." >&2;',
'echo "[crabbox] current Swift is $1.$2; select/install Xcode 26.x or use a Blacksmith macOS runner with Xcode_26.1.app." >&2;',
"return 2;",
"fi;",
"};",
"openclaw_crabbox_require_macos_swift_62",
].join(" ");
}
function injectRemoteAwsMacosSwiftBootstrap(commandArgs, providerName, force = false) {
const runArgs = runCommandArgs(commandArgs);
if (
!isAwsMacosRemoteTarget(commandArgs, providerName) ||
(!force && !commandNeedsAwsMacosSwiftToolchain(runArgs))
) {
return commandArgs;
}
const { start, optionEnd } = runCommandBounds(commandArgs);
if (start < 0) {
return commandArgs;
}
const normalizedArgs = [...commandArgs];
const remoteCommand = normalizedArgs.slice(start);
const originalShellCommand =
hasOption(normalizedArgs, "--shell") && remoteCommand.length === 1
? remoteCommand[0]
: shellJoin(remoteCommand);
const shellCommand = `${remoteAwsMacosSwiftBootstrap()} && { ${originalShellCommand}\n}`;
if (!hasOption(normalizedArgs, "--shell")) {
normalizedArgs.splice(optionEnd, 0, "--shell");
}
const updatedBounds = runCommandBounds(normalizedArgs);
normalizedArgs.splice(
updatedBounds.start,
normalizedArgs.length - updatedBounds.start,
shellCommand,
);
return normalizedArgs;
}
function hasRunOption(commandArgs, name) {
if (commandArgs[0] !== "run") {
return false;
@@ -2136,11 +2283,11 @@ function prepareAwsMacosScriptStdinBootstrap(commandArgs, providerName) {
function createAwsMacosScriptStdinWrapper(script) {
const requirements = awsMacosScriptBootstrapRequirements(script);
if (!script.startsWith("#!")) {
return `${remoteAwsMacosJsBootstrap(requirements)} || exit $?\n${script}`;
return `${remoteAwsMacosScriptBootstrap(requirements)} || exit $?\n${script}`;
}
const delimiterValue = uniqueHereDocDelimiter(script);
return [
`${remoteAwsMacosJsBootstrap(requirements)} || exit $?`,
`${remoteAwsMacosScriptBootstrap(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",
@@ -2153,22 +2300,33 @@ function createAwsMacosScriptStdinWrapper(script) {
].join("\n");
}
function remoteAwsMacosScriptBootstrap(requirements) {
const bootstraps = [remoteAwsMacosJsBootstrap(requirements)];
if (requirements.swift) {
bootstraps.push(remoteAwsMacosSwiftBootstrap());
}
return bootstraps.join(" && ");
}
function awsMacosScriptBootstrapRequirements(script) {
const requirements = { packageManager: false, bun: false };
const requirements = { packageManager: false, bun: false, swift: false };
const firstLine = script.match(/^[^\r\n]*/u)?.[0] ?? "";
if (firstLine.startsWith("#!")) {
const words = firstLine.slice(2).trim().split(/\s+/u).filter(Boolean);
requirements.packageManager = commandWordsNeedEntrypoint(words, awsMacosCorepackEntrypoints);
requirements.bun = commandWordsNeedEntrypoint(words, awsMacosBunEntrypoints);
requirements.swift = commandWordsNeedAwsMacosSwiftToolchain(words);
if (commandWordsShellEntrypoint(words)) {
const body = script.slice(firstLine.length).replace(/^\r?\n/u, "");
requirements.packageManager ||= commandNeedsAwsMacosPackageManager([body]);
requirements.bun ||= commandNeedsAwsMacosBun([body]);
requirements.swift ||= commandNeedsAwsMacosSwiftToolchain([body]);
}
return requirements;
}
requirements.packageManager = commandNeedsAwsMacosPackageManager([script]);
requirements.bun = commandNeedsAwsMacosBun([script]);
requirements.swift = commandNeedsAwsMacosSwiftToolchain([script]);
return requirements;
}
@@ -2677,15 +2835,26 @@ if (
}
const remoteMarkedArgs = injectRemoteChangedGateEnvironment(normalizedArgs);
const remoteMarkedNeedsAwsMacosSwift =
isAwsMacosRemoteTarget(remoteMarkedArgs, provider) &&
commandNeedsAwsMacosSwiftToolchain(runCommandArgs(remoteMarkedArgs));
const childArgs =
childCwd === repoRoot
? injectRemoteWindowsHydratedNodeModulesBootstrap(
injectRemoteAwsMacosJsBootstrap(remoteMarkedArgs, provider),
injectRemoteAwsMacosSwiftBootstrap(
injectRemoteAwsMacosJsBootstrap(remoteMarkedArgs, provider),
provider,
remoteMarkedNeedsAwsMacosSwift,
),
provider,
)
: injectRemoteChangedGateGitBootstrap(
injectRemoteWindowsHydratedNodeModulesBootstrap(
injectRemoteAwsMacosJsBootstrap(absolutizeLocalRunPaths(remoteMarkedArgs), provider),
injectRemoteAwsMacosSwiftBootstrap(
injectRemoteAwsMacosJsBootstrap(absolutizeLocalRunPaths(remoteMarkedArgs), provider),
provider,
remoteMarkedNeedsAwsMacosSwift,
),
provider,
),
remoteChangedGateBase,

View File

@@ -962,6 +962,111 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
expectGroupedShellCommand(remoteCommand, "node --version");
});
it("preflights Swift 6.2 for raw AWS macOS Swift app builds", () => {
const result = runWrapper(
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
[
"run",
"--provider",
"aws",
"--target",
"macos",
"--",
"swift",
"build",
"--package-path",
"apps/macos",
"--product",
"OpenClaw",
],
);
const output = parseFakeCrabboxOutput(result);
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
expect(result.status).toBe(0);
expect(output.args).toContain("--shell");
expect(remoteCommand).toContain("openclaw_crabbox_require_macos_swift_62");
expect(remoteCommand).toContain("/Applications/Xcode_26.1.app");
expect(remoteCommand).toContain("/Applications/Xcode-26*.app");
expect(remoteCommand).toContain('sudo xcode-select -s "$openclaw_developer"');
expect(remoteCommand).toContain("OpenClaw macOS app proof requires Swift tools 6.2+");
expect(remoteCommand).not.toContain("openclaw_crabbox_bootstrap_macos_js");
expectGroupedShellCommand(
remoteCommand,
"swift build --package-path apps/macos --product OpenClaw",
);
});
it("preflights Swift and JS tooling for raw AWS macOS package scripts", () => {
const result = runWrapper(
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
["run", "--provider", "aws", "--target", "macos", "--", "pnpm", "mac:package"],
);
const output = parseFakeCrabboxOutput(result);
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
expect(result.status).toBe(0);
expect(output.args).toContain("--shell");
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
expect(remoteCommand).toContain("pnpm --version >&2");
expect(remoteCommand).toContain("openclaw_crabbox_require_macos_swift_62");
expect(remoteCommand).toContain("OpenClaw macOS app proof requires Swift tools 6.2+");
expectGroupedShellCommand(remoteCommand, "pnpm mac:package");
});
it("preserves sanitized env pnpm package commands when Swift preflight is needed", () => {
const result = runWrapper(
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
["run", "--provider", "aws", "--target", "macos", "--", "env", "-i", "pnpm", "mac:package"],
);
const output = parseFakeCrabboxOutput(result);
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
expect(result.status).toBe(0);
expect(output.args).toContain("--shell");
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
expect(remoteCommand).toContain("openclaw_crabbox_require_macos_swift_62");
expectGroupedShellCommand(remoteCommand, "openclaw_crabbox_env -i pnpm mac:package");
});
it("preflights Swift for raw AWS macOS shell-launched package scripts", () => {
const result = runWrapper(
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
["run", "--provider", "aws", "--target", "macos", "--", "bash", "scripts/package-mac-app.sh"],
);
const output = parseFakeCrabboxOutput(result);
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
expect(result.status).toBe(0);
expect(output.args).toContain("--shell");
expect(remoteCommand).toContain("openclaw_crabbox_require_macos_swift_62");
expect(remoteCommand).not.toContain("openclaw_crabbox_bootstrap_macos_js");
expectGroupedShellCommand(remoteCommand, "bash scripts/package-mac-app.sh");
});
it("does not preflight Swift for raw AWS macOS commands that only mention package scripts", () => {
const result = runWrapper(
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
["run", "--provider", "aws", "--target", "macos", "--", "echo", "scripts/package-mac-app.sh"],
);
const output = parseFakeCrabboxOutput(result);
expect(result.status).toBe(0);
expect(output.args).not.toContain("--shell");
expect(output.args).toEqual([
"run",
"--provider",
"aws",
"--target",
"macos",
"--market",
"on-demand",
"--",
"echo",
"scripts/package-mac-app.sh",
]);
});
it("normalizes inherited Linux UTF-8 locale names for raw AWS macOS bootstrap", () => {
const result = runWrapper(
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
@@ -1545,6 +1650,43 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
expect(output.scriptContent).toContain(`\n${script}\n`);
});
it("preflights Swift for AWS macOS script-stdin Swift builds", () => {
const script = [
"set -euo pipefail",
"swift build --package-path apps/macos --product OpenClaw",
].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("openclaw_crabbox_bootstrap_macos_js");
expect(output.scriptContent).toContain("openclaw_crabbox_require_macos_swift_62");
expect(output.scriptContent).toContain("openclaw_crabbox_require_macos_swift_62 || exit $?");
expect(output.scriptContent).toContain("OpenClaw macOS app proof requires Swift tools 6.2+");
expect(output.scriptContent).toContain(`\n${script}`);
});
it("preflights Swift and JS for AWS macOS script-stdin package scripts", () => {
const script = ["#!/usr/bin/env bash", "set -euo pipefail", "pnpm mac:package"].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("openclaw_crabbox_bootstrap_macos_js");
expect(output.scriptContent).toContain("pnpm --version >&2");
expect(output.scriptContent).toContain("openclaw_crabbox_require_macos_swift_62");
expect(output.scriptContent).toContain("openclaw_crabbox_require_macos_swift_62 || exit $?");
expect(output.scriptContent).toContain(`\n${script}\n`);
});
it("bootstraps Corepack for AWS macOS script-stdin env shebangs with option values", () => {
const script = ["#!/usr/bin/env -C /tmp -u OPENCLAW_FAKE_VAR pnpm", "--version"].join("\n");
const result = runWrapper(