Skip to content

Commit e4763b0

Browse files
committed
fix(crabbox): bootstrap WSL2 package proof
1 parent af2b0a6 commit e4763b0

2 files changed

Lines changed: 263 additions & 43 deletions

File tree

scripts/crabbox-wrapper.mjs

Lines changed: 197 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,8 +1056,8 @@ function commandNeedsAwsMacosPackageManager(commandArgs, options = {}) {
10561056
return true;
10571057
}
10581058
if (commandArgs.length === 1) {
1059-
return shellCommandWordCandidates(commandArgs[0]).some(
1060-
(words) => commandWordsNeedAwsMacosPackageManager(words, options),
1059+
return shellCommandWordCandidates(commandArgs[0]).some((words) =>
1060+
commandWordsNeedAwsMacosPackageManager(words, options),
10611061
);
10621062
}
10631063
return commandWordsNeedAwsMacosPackageManager(normalizedCommandWords(commandArgs), options);
@@ -1964,7 +1964,7 @@ function remoteGitBootstrapForChangedGate(changedGateBase) {
19641964
}
19651965

19661966
function injectRemoteChangedGateEnvironment(commandArgs) {
1967-
if (commandArgs[0] !== "run" || isWindowsRemoteTarget(commandArgs)) {
1967+
if (commandArgs[0] !== "run" || isNativeWindowsRemoteTarget(commandArgs)) {
19681968
return commandArgs;
19691969
}
19701970

@@ -2055,6 +2055,16 @@ function isAwsMacosRemoteTarget(commandArgs, providerName) {
20552055
);
20562056
}
20572057

2058+
function isBrokeredWsl2RemoteTarget(commandArgs, providerName) {
2059+
const canonicalProvider = providerAliases.get(providerName) ?? providerName;
2060+
return (
2061+
commandArgs[0] === "run" &&
2062+
(canonicalProvider === "aws" || canonicalProvider === "azure") &&
2063+
isWindowsRemoteTarget(commandArgs) &&
2064+
optionValue(commandArgs, "--windows-mode") === "wsl2"
2065+
);
2066+
}
2067+
20582068
function isHydratedNativeWindowsProvider(providerName) {
20592069
return providerName === "aws" || providerName === "azure";
20602070
}
@@ -2141,6 +2151,31 @@ function injectRemoteChangedGateGitBootstrap(commandArgs, changedGateBase) {
21412151
return normalizedArgs;
21422152
}
21432153

2154+
function remotePosixJsEnvBootstrap() {
2155+
return [
2156+
"openclaw_crabbox_env() {",
2157+
"openclaw_env_args=();",
2158+
"openclaw_env_ignore=0;",
2159+
"openclaw_env_path_seen=0;",
2160+
'while [ "$#" -gt 0 ]; do',
2161+
'case "$1" in',
2162+
'-i|--ignore-environment) openclaw_env_ignore=1; openclaw_env_args+=("$1"); shift ;;',
2163+
'-S|--split-string|-S*|--split-string=*) command env "${openclaw_env_args[@]}" "$@"; return ;;',
2164+
'-[!-]*i*) openclaw_env_ignore=1; openclaw_env_args+=("$1"); shift ;;',
2165+
'-u|--unset|-C|--chdir) openclaw_env_args+=("$1"); shift; if [ "$#" -gt 0 ]; then openclaw_env_args+=("$1"); shift; fi ;;',
2166+
'--unset=*|--chdir=*) openclaw_env_args+=("$1"); shift ;;',
2167+
'PATH=*) if [ "$openclaw_env_ignore" = "1" ]; then openclaw_env_args+=("PATH=${OPENCLAW_CRABBOX_BOOTSTRAP_PATH:-$PATH}:${1#PATH=}"); else openclaw_env_args+=("$1"); fi; openclaw_env_path_seen=1; shift ;;',
2168+
'[A-Za-z_]*=*) openclaw_env_args+=("$1"); shift ;;',
2169+
'--) openclaw_env_args+=("--"); shift; break ;;',
2170+
"*) break ;;",
2171+
"esac;",
2172+
"done;",
2173+
'if [ "$openclaw_env_ignore" = "1" ] && [ "$openclaw_env_path_seen" = "0" ]; then openclaw_env_args+=("PATH=${OPENCLAW_CRABBOX_BOOTSTRAP_PATH:-$PATH}"); fi;',
2174+
'command env "${openclaw_env_args[@]}" "$@";',
2175+
"};",
2176+
];
2177+
}
2178+
21442179
function remoteAwsMacosJsBootstrap({ packageManager = false, bun = false } = {}) {
21452180
const nodeVersion = process.env.OPENCLAW_CRABBOX_MACOS_NODE_VERSION?.trim() || "24.15.0";
21462181
const bootstrap = [
@@ -2192,26 +2227,7 @@ function remoteAwsMacosJsBootstrap({ packageManager = false, bun = false } = {})
21922227
"release_install_lock;",
21932228
"fi;",
21942229
"node --version >&2 || return 1;",
2195-
"openclaw_crabbox_env() {",
2196-
"openclaw_env_args=();",
2197-
"openclaw_env_ignore=0;",
2198-
"openclaw_env_path_seen=0;",
2199-
'while [ "$#" -gt 0 ]; do',
2200-
'case "$1" in',
2201-
'-i|--ignore-environment) openclaw_env_ignore=1; openclaw_env_args+=("$1"); shift ;;',
2202-
'-S|--split-string|-S*|--split-string=*) command env "${openclaw_env_args[@]}" "$@"; return ;;',
2203-
'-[!-]*i*) openclaw_env_ignore=1; openclaw_env_args+=("$1"); shift ;;',
2204-
'-u|--unset|-C|--chdir) openclaw_env_args+=("$1"); shift; if [ "$#" -gt 0 ]; then openclaw_env_args+=("$1"); shift; fi ;;',
2205-
'--unset=*|--chdir=*) openclaw_env_args+=("$1"); shift ;;',
2206-
'PATH=*) if [ "$openclaw_env_ignore" = "1" ]; then openclaw_env_args+=("PATH=${OPENCLAW_CRABBOX_BOOTSTRAP_PATH:-$PATH}:${1#PATH=}"); else openclaw_env_args+=("$1"); fi; openclaw_env_path_seen=1; shift ;;',
2207-
'[A-Za-z_]*=*) openclaw_env_args+=("$1"); shift ;;',
2208-
'--) openclaw_env_args+=("--"); shift; break ;;',
2209-
"*) break ;;",
2210-
"esac;",
2211-
"done;",
2212-
'if [ "$openclaw_env_ignore" = "1" ] && [ "$openclaw_env_path_seen" = "0" ]; then openclaw_env_args+=("PATH=${OPENCLAW_CRABBOX_BOOTSTRAP_PATH:-$PATH}"); fi;',
2213-
'command env "${openclaw_env_args[@]}" "$@";',
2214-
"};",
2230+
...remotePosixJsEnvBootstrap(),
22152231
];
22162232
if (packageManager) {
22172233
bootstrap.push(
@@ -2264,6 +2280,71 @@ function remoteAwsMacosJsBootstrap({ packageManager = false, bun = false } = {})
22642280
return bootstrap.join(" ");
22652281
}
22662282

2283+
function remoteWsl2JsBootstrap({ packageManager = false } = {}) {
2284+
const nodeVersion = process.env.OPENCLAW_CRABBOX_WSL2_NODE_VERSION?.trim() || "24.15.0";
2285+
const bootstrap = [
2286+
"openclaw_crabbox_bootstrap_wsl2_js() {",
2287+
'tool_root="${OPENCLAW_CRABBOX_WSL2_TOOLCHAIN_DIR:-$HOME/.openclaw-crabbox-toolchain}";',
2288+
`node_version=${shellQuote(nodeVersion)};`,
2289+
'arch="$(uname -m)";',
2290+
'case "$arch" in arm64|aarch64) node_arch=arm64 ;; x86_64|amd64) node_arch=x64 ;; *) echo "unsupported WSL2 arch: $arch" >&2; return 2 ;; esac;',
2291+
'if [ -z "${TMPDIR:-}" ]; then export TMPDIR="/tmp"; fi;',
2292+
'if [ ! -d "$TMPDIR" ]; then mkdir -p "$TMPDIR" 2>/dev/null || export TMPDIR="/tmp"; fi;',
2293+
'if [ ! -d "$TMPDIR" ]; then echo "usable TMPDIR not found: $TMPDIR" >&2; return 1; fi;',
2294+
'node_dir="$tool_root/node-v${node_version}-linux-${node_arch}";',
2295+
'ready_marker="$node_dir/.openclaw-crabbox-node-ready";',
2296+
'export PATH="$node_dir/bin:$PATH";',
2297+
'if [ ! -x "$node_dir/bin/node" ] || [ ! -f "$ready_marker" ]; then',
2298+
'mkdir -p "$tool_root" || { status=$?; return "$status"; };',
2299+
'install_lock="$tool_root/.node-${node_version}-${node_arch}.lock";',
2300+
"lock_acquired=0;",
2301+
"lock_deadline=$((SECONDS + 300));",
2302+
"while true; do",
2303+
'if mkdir "$install_lock" 2>/dev/null; then lock_acquired=1; printf "%s\\n" "$$" >"$install_lock/pid" || { status=$?; rm -rf "$install_lock"; return "$status"; }; break; fi;',
2304+
'if [ -x "$node_dir/bin/node" ] && [ -f "$ready_marker" ]; then break; fi;',
2305+
'if [ "$SECONDS" -ge "$lock_deadline" ]; then',
2306+
'lock_pid="$(cat "$install_lock/pid" 2>/dev/null || true)";',
2307+
'if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then echo "timed out waiting for active WSL2 Node toolchain install lock: $install_lock pid=$lock_pid" >&2; return 1; fi;',
2308+
'echo "reclaiming stale WSL2 Node toolchain install lock: $install_lock" >&2;',
2309+
'rm -rf "$install_lock" || return 1;',
2310+
"lock_deadline=$((SECONDS + 300));",
2311+
"fi;",
2312+
"sleep 1;",
2313+
"done;",
2314+
'release_install_lock() { if [ "$lock_acquired" = "1" ]; then rm -rf "$install_lock" 2>/dev/null || true; fi; };',
2315+
'if [ ! -x "$node_dir/bin/node" ] || [ ! -f "$ready_marker" ]; then',
2316+
'tmp_dir="$(mktemp -d)" || { release_install_lock; return 1; };',
2317+
'pkg="node-v${node_version}-linux-${node_arch}.tar.gz";',
2318+
'base_url="https://nodejs.org/dist/v${node_version}";',
2319+
'curl -fsSL --connect-timeout 10 --max-time 300 --retry 2 --retry-delay 2 -o "$tmp_dir/$pkg" "$base_url/$pkg" || { status=$?; release_install_lock; rm -rf "$tmp_dir"; return "$status"; };',
2320+
'curl -fsSL --connect-timeout 10 --max-time 60 --retry 2 --retry-delay 2 -o "$tmp_dir/SHASUMS256.txt" "$base_url/SHASUMS256.txt" || { status=$?; release_install_lock; rm -rf "$tmp_dir"; return "$status"; };',
2321+
'(cd "$tmp_dir" && grep " $pkg$" SHASUMS256.txt | sha256sum -c -) || { status=$?; release_install_lock; rm -rf "$tmp_dir"; return "$status"; };',
2322+
'rm -rf "$node_dir" || { status=$?; release_install_lock; rm -rf "$tmp_dir"; return "$status"; };',
2323+
'tar -xzf "$tmp_dir/$pkg" -C "$tool_root" || { status=$?; release_install_lock; rm -rf "$tmp_dir"; return "$status"; };',
2324+
'touch "$ready_marker" || { status=$?; release_install_lock; rm -rf "$tmp_dir"; return "$status"; };',
2325+
'rm -rf "$tmp_dir";',
2326+
"fi;",
2327+
"release_install_lock;",
2328+
"fi;",
2329+
"node --version >&2 || return 1;",
2330+
...remotePosixJsEnvBootstrap(),
2331+
];
2332+
if (packageManager) {
2333+
bootstrap.push(
2334+
'export COREPACK_HOME="${COREPACK_HOME:-$tool_root/corepack}";',
2335+
'export PNPM_HOME="${PNPM_HOME:-$tool_root/pnpm-home}";',
2336+
'mkdir -p "$COREPACK_HOME" "$PNPM_HOME" || return 1;',
2337+
'export PATH="$PNPM_HOME:$PATH";',
2338+
'corepack enable --install-directory "$PNPM_HOME" || return 1;',
2339+
"pnpm --version >&2;",
2340+
"if [ -f pnpm-lock.yaml ] && [ ! -f node_modules/.modules.yaml ]; then pnpm install --frozen-lockfile || return 1; fi;",
2341+
);
2342+
}
2343+
bootstrap.push('export OPENCLAW_CRABBOX_BOOTSTRAP_PATH="$PATH";');
2344+
bootstrap.push("};", "openclaw_crabbox_bootstrap_wsl2_js");
2345+
return bootstrap.join(" ");
2346+
}
2347+
22672348
function scopedAwsMacosEnvCommand(commandArgs) {
22682349
if (commandArgs.length <= 1 || !isSupportedSystemEnvCommand(commandArgs[0])) {
22692350
return null;
@@ -2280,11 +2361,7 @@ function scopedAwsMacosEnvCommand(commandArgs) {
22802361
commandWordsNeedAwsMacosPackageManager(targetWords);
22812362
const needsRuntime = jsRuntimeEntrypoints.has(targetEntrypoint);
22822363
const needsBun = awsMacosBunEntrypoints.has(targetEntrypoint);
2283-
if (
2284-
!needsRuntime &&
2285-
!needsPackageManager &&
2286-
!needsBun
2287-
) {
2364+
if (!needsRuntime && !needsPackageManager && !needsBun) {
22882365
return null;
22892366
}
22902367

@@ -2521,6 +2598,73 @@ function readLeadingShellWord(command, start) {
25212598
return word ? { word, end: command.length } : null;
25222599
}
25232600

2601+
function remoteWsl2JsBootstrapRequirements(commandArgs) {
2602+
const runArgs = runCommandArgs(commandArgs);
2603+
const directScopedEnvCommand = hasOption(commandArgs, "--shell")
2604+
? null
2605+
: scopedAwsMacosEnvCommand(runArgs);
2606+
const shellScopedEnvCommand =
2607+
hasOption(commandArgs, "--shell") && runArgs.length === 1
2608+
? scopedAwsMacosShellEnvCommand(runArgs[0])
2609+
: null;
2610+
const scopedEnvCommand = directScopedEnvCommand ?? shellScopedEnvCommand;
2611+
const packageManagerFallbackNeeded = scopedEnvCommand
2612+
? commandNeedsAwsMacosPackageManager(runArgs)
2613+
: commandNeedsAwsMacosPackageManager(runArgs, { canShimIgnoreEnvironment: false });
2614+
const packageManagerNeeded = scopedEnvCommand?.packageManager || packageManagerFallbackNeeded;
2615+
const runtimeEntrypoint =
2616+
scopedEnvCommand?.runtimeEntrypoint || commandRuntimeEntrypoint(runArgs);
2617+
const runtimeNeeded =
2618+
runtimeEntrypoint && !awsMacosBunEntrypoints.has(runtimeEntrypoint) ? runtimeEntrypoint : "";
2619+
2620+
return {
2621+
scopedEnvCommand,
2622+
packageManager: packageManagerNeeded,
2623+
runtimeEntrypoint: runtimeNeeded,
2624+
};
2625+
}
2626+
2627+
function prepareRemoteWsl2JsBootstrapScript(commandArgs, providerName) {
2628+
const requirements = remoteWsl2JsBootstrapRequirements(commandArgs);
2629+
if (
2630+
!isBrokeredWsl2RemoteTarget(commandArgs, providerName) ||
2631+
(!requirements.runtimeEntrypoint && !requirements.packageManager)
2632+
) {
2633+
return { args: commandArgs, cleanup: () => {}, prepared: false };
2634+
}
2635+
2636+
const { start, optionEnd } = runCommandBounds(commandArgs);
2637+
if (start < 0) {
2638+
return { args: commandArgs, cleanup: () => {}, prepared: false };
2639+
}
2640+
2641+
const scriptRoot = mkdtempSync(resolve(tmpdir(), "openclaw-crabbox-wsl2-script-"));
2642+
const scriptPath = resolve(scriptRoot, "script.sh");
2643+
const remoteCommand = commandArgs.slice(start);
2644+
const originalShellCommand =
2645+
requirements.scopedEnvCommand?.shellCommand ??
2646+
(hasOption(commandArgs, "--shell") && remoteCommand.length === 1
2647+
? remoteCommand[0]
2648+
: shellJoin(remoteCommand));
2649+
const script = `${remoteWsl2JsBootstrap({
2650+
packageManager: requirements.packageManager,
2651+
})} || exit $?\n{ ${originalShellCommand}\n}\n`;
2652+
writeFileSync(scriptPath, script, "utf8");
2653+
chmodSync(scriptPath, 0o700);
2654+
2655+
const normalizedArgs = commandArgs.slice(0, optionEnd);
2656+
if (!hasOption(normalizedArgs, "--no-hydrate")) {
2657+
normalizedArgs.push("--no-hydrate");
2658+
}
2659+
normalizedArgs.push("--script", scriptPath);
2660+
2661+
return {
2662+
args: normalizedArgs,
2663+
cleanup: () => rmSync(scriptRoot, { recursive: true, force: true }),
2664+
prepared: true,
2665+
};
2666+
}
2667+
25242668
function injectRemoteAwsMacosJsBootstrap(commandArgs, providerName) {
25252669
const runArgs = runCommandArgs(commandArgs);
25262670
const directScopedEnvCommand = hasOption(commandArgs, "--shell")
@@ -2531,12 +2675,10 @@ function injectRemoteAwsMacosJsBootstrap(commandArgs, providerName) {
25312675
? scopedAwsMacosShellEnvCommand(runArgs[0])
25322676
: null;
25332677
const scopedEnvCommand = directScopedEnvCommand ?? shellScopedEnvCommand;
2534-
const packageManagerFallbackNeeded =
2535-
scopedEnvCommand
2536-
? commandNeedsAwsMacosPackageManager(runArgs)
2537-
: commandNeedsAwsMacosPackageManager(runArgs, { canShimIgnoreEnvironment: false });
2538-
const packageManagerNeeded =
2539-
scopedEnvCommand?.packageManager || packageManagerFallbackNeeded;
2678+
const packageManagerFallbackNeeded = scopedEnvCommand
2679+
? commandNeedsAwsMacosPackageManager(runArgs)
2680+
: commandNeedsAwsMacosPackageManager(runArgs, { canShimIgnoreEnvironment: false });
2681+
const packageManagerNeeded = scopedEnvCommand?.packageManager || packageManagerFallbackNeeded;
25402682
const bunNeeded = scopedEnvCommand?.bun || commandNeedsAwsMacosBun(runArgs);
25412683
const runtimeEntrypoint =
25422684
scopedEnvCommand?.runtimeEntrypoint || commandRuntimeEntrypoint(runArgs);
@@ -3182,6 +3324,7 @@ let remoteChangedGateBase = "";
31823324
const scriptBootstrap = prepareAwsMacosScriptStdinBootstrap(normalizedArgs, provider);
31833325
normalizedArgs = scriptBootstrap.args;
31843326
const scriptStdinPrepared = scriptBootstrap.prepared;
3327+
let wsl2ScriptBootstrap = { args: normalizedArgs, cleanup: () => {}, prepared: false };
31853328
try {
31863329
if (shouldUseFullCheckoutForCleanRemoteSync(normalizedArgs, provider)) {
31873330
const runWords = runCommandArgs(normalizedArgs);
@@ -3212,6 +3355,7 @@ function cleanupOnce() {
32123355
}
32133356
cleanupDone = true;
32143357
stopFullCheckoutKeepalive();
3358+
wsl2ScriptBootstrap.cleanup();
32153359
scriptBootstrap.cleanup();
32163360
preserveTemporaryCrabboxRuns();
32173361
cleanupChildCwd();
@@ -3237,6 +3381,14 @@ if (
32373381
);
32383382
}
32393383
}
3384+
if (normalizedArgs[0] === "run" && isBrokeredWsl2RemoteTarget(normalizedArgs, provider)) {
3385+
const wsl2Requirements = remoteWsl2JsBootstrapRequirements(normalizedArgs);
3386+
if (wsl2Requirements.runtimeEntrypoint || wsl2Requirements.packageManager) {
3387+
console.error(
3388+
`[crabbox] provider=${provider} WSL2 raw boxes may lack Node/Corepack/pnpm for ${wsl2Requirements.runtimeEntrypoint || "package-manager"}; using no-hydrate pinned user-local JavaScript tooling before the command`,
3389+
);
3390+
}
3391+
}
32403392

32413393
const childEnv = { ...process.env };
32423394
if (
@@ -3265,11 +3417,20 @@ const remoteMarkedArgs = injectRemoteChangedGateEnvironment(normalizedArgs);
32653417
const remoteMarkedNeedsAwsMacosSwift =
32663418
isAwsMacosRemoteTarget(remoteMarkedArgs, provider) &&
32673419
commandNeedsAwsMacosSwiftToolchain(runCommandArgs(remoteMarkedArgs));
3420+
try {
3421+
wsl2ScriptBootstrap = prepareRemoteWsl2JsBootstrapScript(
3422+
childCwd === repoRoot ? remoteMarkedArgs : absolutizeLocalRunPaths(remoteMarkedArgs),
3423+
provider,
3424+
);
3425+
} catch (error) {
3426+
cleanupOnce();
3427+
throw error;
3428+
}
32683429
const childArgs =
32693430
childCwd === repoRoot
32703431
? injectRemoteWindowsHydratedNodeModulesBootstrap(
32713432
injectRemoteAwsMacosSwiftBootstrap(
3272-
injectRemoteAwsMacosJsBootstrap(remoteMarkedArgs, provider),
3433+
injectRemoteAwsMacosJsBootstrap(wsl2ScriptBootstrap.args, provider),
32733434
provider,
32743435
remoteMarkedNeedsAwsMacosSwift,
32753436
),
@@ -3278,7 +3439,7 @@ const childArgs =
32783439
: injectRemoteChangedGateGitBootstrap(
32793440
injectRemoteWindowsHydratedNodeModulesBootstrap(
32803441
injectRemoteAwsMacosSwiftBootstrap(
3281-
injectRemoteAwsMacosJsBootstrap(absolutizeLocalRunPaths(remoteMarkedArgs), provider),
3442+
injectRemoteAwsMacosJsBootstrap(wsl2ScriptBootstrap.args, provider),
32823443
provider,
32833444
remoteMarkedNeedsAwsMacosSwift,
32843445
),

0 commit comments

Comments
 (0)