mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(build): respect PATH-less pnpm environments
This commit is contained in:
@@ -25,6 +25,8 @@ export function resolveGeneratedModuleFormatter(params) {
|
||||
|
||||
return resolvePnpmRunner({
|
||||
comSpec: params.comSpec,
|
||||
cwd: params.repoRoot,
|
||||
env: params.env,
|
||||
npmExecPath: params.npmExecPath,
|
||||
nodeExecPath: params.nodeExecPath,
|
||||
platform,
|
||||
|
||||
+48
-29
@@ -2,7 +2,7 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { accessSync, closeSync, constants, openSync, readSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
|
||||
import { buildCmdExeCommandLine, resolvePathEnvKey } from "./windows-cmd-helpers.mjs";
|
||||
|
||||
function getPortableBasename(value) {
|
||||
return value.split(/[/\\]/).at(-1) ?? value;
|
||||
@@ -55,22 +55,47 @@ function isFile(value) {
|
||||
}
|
||||
}
|
||||
|
||||
function findExecutableOnPath(command, envPath) {
|
||||
function findExecutableOnPath(command, envPath, platform, env, cwd) {
|
||||
if (typeof envPath !== "string" || envPath.length === 0) {
|
||||
return null;
|
||||
}
|
||||
for (const directory of envPath.split(path.delimiter)) {
|
||||
const extensions =
|
||||
platform === "win32"
|
||||
? (env[Object.keys(env).find((key) => key.toLowerCase() === "pathext") ?? "PATHEXT"] ??
|
||||
".COM;.EXE;.BAT;.CMD")
|
||||
.split(";")
|
||||
.filter(Boolean)
|
||||
.map((extension) => extension.toLowerCase())
|
||||
: [""];
|
||||
const pathDelimiter = platform === "win32" ? ";" : path.delimiter;
|
||||
for (const directory of envPath.split(pathDelimiter)) {
|
||||
if (!directory) {
|
||||
continue;
|
||||
}
|
||||
const candidate = path.join(directory, command);
|
||||
if (isExecutableFile(candidate)) {
|
||||
return candidate;
|
||||
const resolvedDirectory = path.isAbsolute(directory) ? directory : path.resolve(cwd, directory);
|
||||
for (const extension of extensions) {
|
||||
const candidate = path.join(resolvedDirectory, `${command}${extension}`);
|
||||
if ((platform === "win32" ? isFile(candidate) : isExecutableFile(candidate))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function createWindowsRunner(command, args, comSpec) {
|
||||
const extension = getPortableExtension(command);
|
||||
if (extension === ".cmd" || extension === ".bat") {
|
||||
return {
|
||||
command: comSpec,
|
||||
args: ["/d", "/s", "/c", buildCmdExeCommandLine(command, args)],
|
||||
shell: false,
|
||||
windowsVerbatimArguments: true,
|
||||
};
|
||||
}
|
||||
return { command, args, shell: false };
|
||||
}
|
||||
|
||||
function isNodeRunnablePnpmExecPath(value) {
|
||||
if (!isPnpmExecPath(value)) {
|
||||
return false;
|
||||
@@ -95,7 +120,9 @@ export function resolvePnpmRunner(params = {}) {
|
||||
const nodeExecPath = params.nodeExecPath ?? process.execPath;
|
||||
const platform = params.platform ?? process.platform;
|
||||
const comSpec = params.comSpec ?? process.env.ComSpec ?? "cmd.exe";
|
||||
const envPath = params.env?.PATH ?? process.env.PATH;
|
||||
const env = params.env ?? process.env;
|
||||
const envPath = env[platform === "win32" ? resolvePathEnvKey(env) : "PATH"];
|
||||
const cwd = params.cwd ?? process.cwd();
|
||||
|
||||
if (typeof npmExecPath === "string" && npmExecPath.length > 0 && isPnpmExecPath(npmExecPath)) {
|
||||
if (isNodeRunnablePnpmExecPath(npmExecPath)) {
|
||||
@@ -131,30 +158,22 @@ export function resolvePnpmRunner(params = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === "win32") {
|
||||
return {
|
||||
command: comSpec,
|
||||
args: ["/d", "/s", "/c", buildCmdExeCommandLine("pnpm.cmd", pnpmArgs)],
|
||||
shell: false,
|
||||
windowsVerbatimArguments: true,
|
||||
};
|
||||
const pnpmPath = findExecutableOnPath("pnpm", envPath, platform, env, cwd);
|
||||
if (pnpmPath) {
|
||||
return platform === "win32"
|
||||
? createWindowsRunner(pnpmPath, pnpmArgs, comSpec)
|
||||
: { command: pnpmPath, args: pnpmArgs, shell: false };
|
||||
}
|
||||
const corepackPath = findExecutableOnPath("corepack", envPath, platform, env, cwd);
|
||||
if (corepackPath) {
|
||||
const args = ["pnpm", ...pnpmArgs];
|
||||
return platform === "win32"
|
||||
? createWindowsRunner(corepackPath, args, comSpec)
|
||||
: { command: corepackPath, args, shell: false };
|
||||
}
|
||||
|
||||
const pnpmPath = findExecutableOnPath("pnpm", envPath);
|
||||
if (pnpmPath) {
|
||||
return {
|
||||
command: pnpmPath,
|
||||
args: pnpmArgs,
|
||||
shell: false,
|
||||
};
|
||||
}
|
||||
const corepackPath = findExecutableOnPath("corepack", envPath);
|
||||
if (corepackPath) {
|
||||
return {
|
||||
command: corepackPath,
|
||||
args: ["pnpm", ...pnpmArgs],
|
||||
shell: false,
|
||||
};
|
||||
if (platform === "win32") {
|
||||
return createWindowsRunner("pnpm.cmd", pnpmArgs, comSpec);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+3
-1
@@ -67,7 +67,9 @@ export function resolveSpawnCall(cmd, args, envOverride, params = {}) {
|
||||
export function resolvePnpmSpawnCall(pnpmArgs, envOverride, params = {}) {
|
||||
const env = envOverride ?? process.env;
|
||||
const platform = params.platform ?? process.platform;
|
||||
const cwd = params.cwd ?? uiDir;
|
||||
const runner = resolvePnpmRunner({
|
||||
cwd,
|
||||
env,
|
||||
pnpmArgs,
|
||||
nodeExecPath: params.nodeExecPath ?? process.execPath,
|
||||
@@ -79,7 +81,7 @@ export function resolvePnpmSpawnCall(pnpmArgs, envOverride, params = {}) {
|
||||
command: runner.command,
|
||||
args: runner.args,
|
||||
options: {
|
||||
cwd: params.cwd ?? uiDir,
|
||||
cwd,
|
||||
stdio: "inherit",
|
||||
env,
|
||||
shell: runner.shell,
|
||||
|
||||
@@ -49,6 +49,7 @@ describe("resolveGeneratedModuleFormatter", () => {
|
||||
expect(
|
||||
resolveGeneratedModuleFormatter({
|
||||
comSpec: "C:\\Windows\\System32\\cmd.exe",
|
||||
env: { PATH: "" },
|
||||
existsSync: () => false,
|
||||
npmExecPath: "",
|
||||
outputPath: "C:\\Users\\test\\AppData\\Local\\Temp\\generated output.ts",
|
||||
|
||||
@@ -4,6 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createPnpmRunnerSpawnSpec, resolvePnpmRunner } from "../../scripts/pnpm-runner.mjs";
|
||||
import { buildCmdExeCommandLine } from "../../scripts/windows-cmd-helpers.mjs";
|
||||
|
||||
describe("resolvePnpmRunner", () => {
|
||||
const posixIt = process.platform === "win32" ? it.skip : it;
|
||||
@@ -171,6 +172,7 @@ describe("resolvePnpmRunner", () => {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
comSpec: "C:\\Windows\\System32\\cmd.exe",
|
||||
env: { PATH: "" },
|
||||
npmExecPath:
|
||||
"C:\\Users\\test\\AppData\\Local\\Temp\\cache\\corepack\\v1\\pnpm\\10.32.1\\bin\\pnpm.mjs",
|
||||
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
|
||||
@@ -221,6 +223,43 @@ describe("resolvePnpmRunner", () => {
|
||||
});
|
||||
});
|
||||
|
||||
posixIt("does not resolve executables from the parent PATH for an explicit empty env", () => {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
npmExecPath: "",
|
||||
env: {},
|
||||
pnpmArgs: ["exec", "vitest", "run"],
|
||||
platform: "linux",
|
||||
}),
|
||||
).toEqual({
|
||||
command: "pnpm",
|
||||
args: ["exec", "vitest", "run"],
|
||||
shell: false,
|
||||
});
|
||||
});
|
||||
|
||||
posixIt("resolves relative PATH entries from the child working directory", () => {
|
||||
const childDir = mkdtempSync(path.join(os.tmpdir(), "pnpm-runner-child-"));
|
||||
|
||||
try {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
cwd: childDir,
|
||||
npmExecPath: "",
|
||||
env: { PATH: "node_modules/.bin" },
|
||||
pnpmArgs: ["exec", "vitest", "run"],
|
||||
platform: "linux",
|
||||
}),
|
||||
).toEqual({
|
||||
command: "pnpm",
|
||||
args: ["exec", "vitest", "run"],
|
||||
shell: false,
|
||||
});
|
||||
} finally {
|
||||
rmSync(childDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
posixIt("uses Corepack when pnpm is not directly available on PATH", () => {
|
||||
const tempDir = mkdtempSync(path.join(os.tmpdir(), "pnpm-runner-corepack-"));
|
||||
const corepackPath = path.join(tempDir, "corepack");
|
||||
@@ -276,8 +315,8 @@ describe("resolvePnpmRunner", () => {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
comSpec: "C:\\Windows\\System32\\cmd.exe",
|
||||
npmExecPath: "",
|
||||
env: { PATH: "" },
|
||||
npmExecPath: "",
|
||||
pnpmArgs: ["exec", "vitest", "run", "-t", "path with spaces"],
|
||||
platform: "win32",
|
||||
}),
|
||||
@@ -289,10 +328,41 @@ describe("resolvePnpmRunner", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses Corepack on Windows when no pnpm shim is available", () => {
|
||||
const tempDir = mkdtempSync(path.join(os.tmpdir(), "pnpm-runner-corepack-"));
|
||||
const corepackPath = path.join(tempDir, "corepack.cmd");
|
||||
writeFileSync(corepackPath, "@exit /b 0\r\n");
|
||||
|
||||
try {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
comSpec: "C:\\Windows\\System32\\cmd.exe",
|
||||
npmExecPath: "",
|
||||
env: { Path: tempDir, PATHEXT: ".CMD;.EXE" },
|
||||
pnpmArgs: ["exec", "vitest", "run"],
|
||||
platform: "win32",
|
||||
}),
|
||||
).toEqual({
|
||||
command: "C:\\Windows\\System32\\cmd.exe",
|
||||
args: [
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
buildCmdExeCommandLine(corepackPath, ["pnpm", "exec", "vitest", "run"]),
|
||||
],
|
||||
shell: false,
|
||||
windowsVerbatimArguments: true,
|
||||
});
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("escapes caret arguments for Windows cmd.exe", () => {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
comSpec: "C:\\Windows\\System32\\cmd.exe",
|
||||
env: { PATH: "" },
|
||||
npmExecPath: "",
|
||||
pnpmArgs: ["exec", "vitest", "-t", "@scope/pkg@^1.2.3"],
|
||||
platform: "win32",
|
||||
|
||||
Reference in New Issue
Block a user