fix(scripts): run Windows check commands through shims

This commit is contained in:
Vincent Koc
2026-05-23 17:24:11 +02:00
parent f4b5e58231
commit 8a94e825cd
10 changed files with 246 additions and 22 deletions

View File

@@ -1,6 +1,7 @@
import { execFileSync } from "node:child_process";
import { appendFileSync, existsSync, readFileSync } from "node:fs";
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
const GIT_OUTPUT_MAX_BUFFER = 64 * 1024 * 1024;
@@ -445,8 +446,7 @@ function parseArgs(argv) {
}
function isDirectRun() {
const direct = process.argv[1];
return Boolean(direct && import.meta.url.endsWith(direct));
return isDirectRunUrl(process.argv[1], import.meta.url);
}
function printHuman(result) {

View File

@@ -11,6 +11,7 @@ import {
import { shrinkwrapPackageDirsForChangedPaths } from "./generate-npm-shrinkwrap.mjs";
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
import { printTimingSummary } from "./lib/check-timing-summary.mjs";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import {
acquireLocalHeavyCheckLockSync,
resolveLocalHeavyCheckEnv,
@@ -472,8 +473,7 @@ function parseArgs(argv) {
}
function isDirectRun() {
const direct = process.argv[1];
return Boolean(direct && import.meta.url.endsWith(direct));
return isDirectRunUrl(process.argv[1], import.meta.url);
}
if (isDirectRun()) {

View File

@@ -1,5 +1,6 @@
import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
/** @typedef {{ runNode: boolean; runMacos: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; runChangedSmoke: boolean; runControlUiI18n: boolean }} ChangedScope */
/** @typedef {{ runFastOnly: boolean; runPluginContracts: boolean; runCiRouting: boolean }} NodeFastScope */
@@ -286,8 +287,7 @@ export function writeGitHubOutput(
}
function isDirectRun() {
const direct = process.argv[1];
return Boolean(direct && import.meta.url.endsWith(direct));
return isDirectRunUrl(process.argv[1], import.meta.url);
}
/** @param {string[]} argv */

View File

@@ -2,10 +2,38 @@ import { spawn, spawnSync, type SpawnOptions } from "node:child_process";
import { writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveNpmRunner } from "../../npm-runner.mjs";
import { resolvePnpmRunner } from "../../pnpm-runner.mjs";
import { buildCmdExeCommandLine } from "../../windows-cmd-helpers.mjs";
import type { CommandResult, RunOptions } from "./types.ts";
export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
type HostCommandInvocation = {
args: string[];
command: string;
env?: NodeJS.ProcessEnv;
shell?: boolean;
windowsVerbatimArguments?: boolean;
};
type ResolveHostCommandOptions = {
comSpec?: string;
env?: NodeJS.ProcessEnv;
execPath?: string;
existsSync?: (path: string) => boolean;
platform?: NodeJS.Platform;
};
function hostInvocationFromRunner(runner: HostCommandInvocation): HostCommandInvocation {
if (runner.env === undefined) {
const invocation = { ...runner };
delete invocation.env;
return invocation;
}
return runner;
}
export function say(message: string): void {
process.stdout.write(`==> ${message}\n`);
}
@@ -23,15 +51,81 @@ export function shellQuote(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
function portableBasename(value: string): string {
return value.split(/[/\\]/u).at(-1) ?? value;
}
function portableExtension(value: string): string {
return path.posix.extname(portableBasename(value)).toLowerCase();
}
function isBareCommand(command: string, name: "npm" | "pnpm"): boolean {
return portableBasename(command) === command && command.toLowerCase() === name;
}
function resolveEnvValue(env: NodeJS.ProcessEnv, name: string): string | undefined {
const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
return key === undefined ? undefined : env[key];
}
export function resolveHostCommandInvocation(
command: string,
args: string[],
options: ResolveHostCommandOptions = {},
): HostCommandInvocation {
const env = options.env ?? process.env;
const platform = options.platform ?? process.platform;
const comSpec = options.comSpec ?? resolveEnvValue(env, "ComSpec") ?? "cmd.exe";
if (isBareCommand(command, "pnpm")) {
const runner = resolvePnpmRunner({
comSpec,
npmExecPath: env.npm_execpath,
nodeExecPath: options.execPath ?? process.execPath,
platform,
pnpmArgs: args,
});
return hostInvocationFromRunner(runner);
}
if (isBareCommand(command, "npm")) {
const runner = resolveNpmRunner({
comSpec,
env,
execPath: options.execPath ?? process.execPath,
existsSync: options.existsSync,
npmArgs: args,
platform,
});
return hostInvocationFromRunner(runner);
}
const extension = portableExtension(command);
if (platform === "win32" && (extension === ".cmd" || extension === ".bat")) {
return {
args: ["/d", "/s", "/c", buildCmdExeCommandLine(command, args)],
command: comSpec,
shell: false,
windowsVerbatimArguments: true,
};
}
return { args, command, shell: false };
}
export function run(command: string, args: string[], options: RunOptions = {}): CommandResult {
const result = spawnSync(command, args, {
const env = { ...process.env, ...options.env };
const invocation = resolveHostCommandInvocation(command, args, { env });
const result = spawnSync(invocation.command, invocation.args, {
cwd: options.cwd ?? repoRoot,
encoding: "utf8",
env: { ...process.env, ...options.env },
env: invocation.env ?? env,
input: options.input,
maxBuffer: 50 * 1024 * 1024,
stdio: options.quiet ? ["pipe", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
shell: invocation.shell,
timeout: options.timeoutMs,
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
});
const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
@@ -67,10 +161,14 @@ export async function runStreaming(
options: RunOptions & { logPath?: string } = {},
): Promise<number> {
return await new Promise((resolve, reject) => {
const child = spawn(command, args, {
const env = { ...process.env, ...options.env };
const invocation = resolveHostCommandInvocation(command, args, { env });
const child = spawn(invocation.command, invocation.args, {
cwd: options.cwd ?? repoRoot,
env: { ...process.env, ...options.env },
env: invocation.env ?? env,
shell: invocation.shell,
stdio: ["pipe", "pipe", "pipe"],
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
} satisfies SpawnOptions);
let log = "";

View File

@@ -6,6 +6,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { parse as parseYaml } from "yaml";
import { listChangedPathsFromGit, listStagedChangedPaths } from "./changed-lanes.mjs";
import { resolveNpmRunner } from "./npm-runner.mjs";
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u;
@@ -17,10 +18,6 @@ function usage() {
].join("\n");
}
function npmCommand() {
return process.platform === "win32" ? "npm.cmd" : "npm";
}
function normalizeOverrideValue(value) {
if (value === null || value === undefined) {
return value;
@@ -144,10 +141,25 @@ function packageJsonForShrinkwrap(packageJson, shrinkwrapOverrides) {
return normalized;
}
export function createNpmShrinkwrapCommand(args, options = {}) {
return resolveNpmRunner({
comSpec: options.comSpec,
env: options.env,
execPath: options.execPath,
existsSync: options.existsSync,
npmArgs: args,
platform: options.platform,
});
}
function runNpm(args, cwd) {
execFileSync(npmCommand(), args, {
const npm = createNpmShrinkwrapCommand(args);
execFileSync(npm.command, npm.args, {
cwd,
env: npm.env ?? process.env,
shell: npm.shell,
stdio: ["ignore", "pipe", "pipe"],
windowsVerbatimArguments: npm.windowsVerbatimArguments,
});
}
@@ -393,7 +405,7 @@ function listPublishablePluginPackageDirs() {
const extensionsDir = path.join(ROOT_DIR, "extensions");
return readdirSync(extensionsDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join("extensions", entry.name))
.map((entry) => path.posix.join("extensions", entry.name))
.filter((packageDir) => {
const packageJsonPath = path.join(ROOT_DIR, packageDir, "package.json");
if (!existsSync(packageJsonPath)) {

View File

@@ -0,0 +1,18 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
export function isDirectRunPath(directPath, modulePath, platform = process.platform) {
if (!directPath || !modulePath) {
return false;
}
const pathImpl = platform === "win32" ? path.win32 : path;
const normalize =
platform === "win32"
? (value) => pathImpl.resolve(value).toLowerCase()
: (value) => pathImpl.resolve(value);
return normalize(directPath) === normalize(modulePath);
}
export function isDirectRunUrl(directPath, moduleUrl, platform = process.platform) {
return isDirectRunPath(directPath, fileURLToPath(moduleUrl), platform);
}