mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(scripts): resolve Crabbox shims on Windows
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
||||
import { delimiter, dirname, extname, isAbsolute, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolvePathEnvKey } from "./windows-cmd-helpers.mjs";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repoLocal = resolve(repoRoot, "../crabbox/bin/crabbox");
|
||||
const binary = existsSync(repoLocal) ? repoLocal : "crabbox";
|
||||
const repoLocal = resolveCrabboxBinary(process.env, process.platform);
|
||||
const binary = repoLocal ?? resolvePathBinary("crabbox", process.env, process.platform);
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args[0] === "--") {
|
||||
@@ -18,11 +19,95 @@ if (args[userArgStart] === "--") {
|
||||
args.splice(userArgStart, 1);
|
||||
}
|
||||
|
||||
function commandCandidates(command, platform) {
|
||||
if (platform !== "win32") {
|
||||
return [command];
|
||||
}
|
||||
if (extname(command)) {
|
||||
return [command];
|
||||
}
|
||||
return [`${command}.exe`, `${command}.cmd`, `${command}.bat`, `${command}.com`, command];
|
||||
}
|
||||
|
||||
function resolveCrabboxBinary(env, platform) {
|
||||
const base = resolve(repoRoot, "../crabbox/bin/crabbox");
|
||||
for (const candidate of commandCandidates(base, platform)) {
|
||||
if (isFile(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvePathBinary(command, env, platform) {
|
||||
if (platform !== "win32") {
|
||||
return command;
|
||||
}
|
||||
for (const candidate of commandCandidates(command, platform)) {
|
||||
if (isFile(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
const pathValue = env[resolvePathEnvKey(env)] ?? "";
|
||||
for (const dir of pathValue.split(delimiter).filter(Boolean)) {
|
||||
for (const candidate of commandCandidates(command, platform)) {
|
||||
const fullPath = resolve(dir, candidate);
|
||||
if (isFile(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
function isFile(path) {
|
||||
try {
|
||||
return statSync(path).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function spawnInvocation(command, commandArgs, env, platform) {
|
||||
const extension = extname(command).toLowerCase();
|
||||
if (platform === "win32" && (extension === ".cmd" || extension === ".bat")) {
|
||||
return {
|
||||
command: env.ComSpec ?? "cmd.exe",
|
||||
args: ["/d", "/s", "/c", buildBatchCommandLine(command, commandArgs)],
|
||||
windowsVerbatimArguments: true,
|
||||
};
|
||||
}
|
||||
return { command, args: commandArgs };
|
||||
}
|
||||
|
||||
const cmdMetaCharactersRe = /([()\][%!^"`<>&|;, *?])/g;
|
||||
|
||||
function escapeBatchCommand(command) {
|
||||
return `${command}`.replace(cmdMetaCharactersRe, "^$1");
|
||||
}
|
||||
|
||||
function escapeBatchArgument(arg) {
|
||||
let escaped = `${arg}`;
|
||||
escaped = escaped.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"');
|
||||
escaped = escaped.replace(/(?=(\\+?)?)\1$/, "$1$1");
|
||||
escaped = `"${escaped}"`;
|
||||
escaped = escaped.replace(cmdMetaCharactersRe, "^$1");
|
||||
return escaped.replace(cmdMetaCharactersRe, "^$1");
|
||||
}
|
||||
|
||||
function buildBatchCommandLine(command, commandArgs) {
|
||||
const escapedCommand = escapeBatchCommand(command);
|
||||
const escapedArgs = commandArgs.map(escapeBatchArgument);
|
||||
return `"${[escapedCommand, ...escapedArgs].join(" ")}"`;
|
||||
}
|
||||
|
||||
function checkedOutput(command, commandArgs) {
|
||||
const result = spawnSync(command, commandArgs, {
|
||||
const invocation = spawnInvocation(command, commandArgs, process.env, process.platform);
|
||||
const result = spawnSync(invocation.command, invocation.args, {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
||||
});
|
||||
return {
|
||||
status: result.status ?? 1,
|
||||
@@ -31,10 +116,13 @@ function checkedOutput(command, commandArgs) {
|
||||
}
|
||||
|
||||
function gitOutput(commandArgs) {
|
||||
const result = spawnSync("git", commandArgs, {
|
||||
const gitBinary = resolvePathBinary("git", process.env, process.platform);
|
||||
const invocation = spawnInvocation(gitBinary, commandArgs, process.env, process.platform);
|
||||
const result = spawnSync(invocation.command, invocation.args, {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
||||
});
|
||||
return {
|
||||
status: result.status ?? 1,
|
||||
@@ -609,10 +697,12 @@ if (
|
||||
}
|
||||
|
||||
const childArgs = childCwd === repoRoot ? args : absolutizeLocalRunPaths(args);
|
||||
const child = spawn(binary, childArgs, {
|
||||
const childInvocation = spawnInvocation(binary, childArgs, childEnv, process.platform);
|
||||
const child = spawn(childInvocation.command, childInvocation.args, {
|
||||
cwd: childCwd,
|
||||
stdio: "inherit",
|
||||
env: childEnv,
|
||||
windowsVerbatimArguments: childInvocation.windowsVerbatimArguments,
|
||||
});
|
||||
|
||||
const signalExitCodes = new Map([
|
||||
|
||||
@@ -54,6 +54,7 @@ function makeFakeGit(responses: Record<string, { status?: number; stdout?: strin
|
||||
"process.exit(response.status ?? 0);",
|
||||
].join("\n");
|
||||
writeFileSync(gitPath, `${script}\n`, "utf8");
|
||||
writeFileSync(`${gitPath}.cmd`, `@echo off\r\n"${process.execPath}" "%~dp0git" %*\r\n`, "utf8");
|
||||
chmodSync(gitPath, 0o755);
|
||||
return binDir;
|
||||
}
|
||||
@@ -61,7 +62,10 @@ function makeFakeGit(responses: Record<string, { status?: number; stdout?: strin
|
||||
function runWrapper(
|
||||
helpText: string,
|
||||
args: string[],
|
||||
options: { gitResponses?: Record<string, { status?: number; stdout?: string; stderr?: string }> } = {},
|
||||
options: {
|
||||
extraPathEntries?: string[];
|
||||
gitResponses?: Record<string, { status?: number; stdout?: string; stderr?: string }>;
|
||||
} = {},
|
||||
) {
|
||||
const binDir = makeFakeCrabbox(helpText);
|
||||
const gitBinDir = options.gitResponses ? makeFakeGit(options.gitResponses) : "";
|
||||
@@ -70,7 +74,9 @@ function runWrapper(
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: [binDir, gitBinDir, process.env.PATH ?? ""].filter(Boolean).join(path.delimiter),
|
||||
PATH: [...(options.extraPathEntries ?? []), binDir, gitBinDir, process.env.PATH ?? ""]
|
||||
.filter(Boolean)
|
||||
.join(path.delimiter),
|
||||
...(options.gitResponses
|
||||
? { OPENCLAW_FAKE_GIT_RESPONSES: JSON.stringify(options.gitResponses) }
|
||||
: {}),
|
||||
@@ -116,6 +122,32 @@ describe("scripts/crabbox-wrapper", () => {
|
||||
);
|
||||
});
|
||||
|
||||
if (process.platform === "win32") {
|
||||
it("preserves shell metacharacters through Windows Crabbox command shims", () => {
|
||||
const remoteCommand = "pnpm build && pnpm test | more < in.txt > out.txt %PATH%";
|
||||
const result = runWrapper("provider: aws\n", ["run", "--shell", "--", remoteCommand]);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toEqual(["run", "--shell", "--", remoteCommand]);
|
||||
});
|
||||
}
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
it("keeps POSIX PATH lookup semantics for non-executable entries", () => {
|
||||
const staleBinDir = mkdtempSync(path.join(tmpdir(), "openclaw-stale-crabbox-"));
|
||||
tempDirs.push(staleBinDir);
|
||||
writeFileSync(path.join(staleBinDir, "crabbox"), "not executable\n", "utf8");
|
||||
const result = runWrapper(
|
||||
"provider: aws\n",
|
||||
["run", "--provider", "aws", "--", "echo ok"],
|
||||
{ extraPathEntries: [staleBinDir] },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toContain("aws");
|
||||
});
|
||||
}
|
||||
|
||||
it("accepts Crabbox provider aliases when their canonical provider is advertised", () => {
|
||||
const helpText = [
|
||||
"provider: hetzner, aws, gcp, local-container, blacksmith-testbox,",
|
||||
|
||||
Reference in New Issue
Block a user