Unnamed repository; edit this file 'description' to name the repository.
Fix VS Code cargo executable probing
Handle Windows executable suffixes when the VS Code extension probes
for cargo through PATH or CARGO_HOME, and preserve case-insensitive
environment variable lookup for supplied env objects so copied Windows
`Path` entries still resolve cargo.
Normalize and document the Windows CARGO_HOME test comparison as well,
because VS Code path resolution can lowercase the drive letter in
`fsPath` without changing which executable was found.
This keeps the extension-side lookup aligned with the Rust-side
toolchain helper behavior and avoids breaking create-project on common
Windows setups.
AI-assisted: OpenAI Codex was used to identify and apply this change.
| -rw-r--r-- | editors/code/src/toolchain.ts | 41 | ||||
| -rw-r--r-- | editors/code/tests/unit/launch_config.test.ts | 24 |
2 files changed, 43 insertions, 22 deletions
diff --git a/editors/code/src/toolchain.ts b/editors/code/src/toolchain.ts index 784ee260d0..6ae365c71c 100644 --- a/editors/code/src/toolchain.ts +++ b/editors/code/src/toolchain.ts @@ -3,7 +3,7 @@ import * as os from "os"; import * as path from "path"; import * as readline from "readline"; import * as vscode from "vscode"; -import { Env, log, memoizeAsync, unwrapUndefinable } from "./util"; +import { Env, isWindows, log, memoizeAsync, unwrapUndefinable } from "./util"; import type { CargoRunnableArgs } from "./lsp_ext"; interface CompilationArtifact { @@ -178,20 +178,22 @@ async function getPathForExecutableWithEnv( executableName: "cargo" | "rustc" | "rustup", env: Env, ): Promise<string> { - const envVar = env[executableName.toUpperCase()]; + const envVar = getEnvVar(env, executableName.toUpperCase()); if (envVar) { return envVar; } - if (await lookupInPath(executableName, env["PATH"] ?? "")) { + if (await lookupInPath(executableName, getEnvVar(env, "PATH") ?? "")) { return executableName; } - const cargoHome = getCargoHomeFromPath(env["CARGO_HOME"]); + const cargoHome = getCargoHomeFromPath(getEnvVar(env, "CARGO_HOME")); if (cargoHome) { - const standardPath = vscode.Uri.joinPath(cargoHome, "bin", executableName); - if (await isFileAtUri(standardPath)) { - return standardPath.fsPath; + for (const candidate of executableCandidates(executableName)) { + const standardPath = vscode.Uri.joinPath(cargoHome, "bin", candidate); + if (await isFileAtUri(standardPath)) { + return standardPath.fsPath; + } } } @@ -211,8 +213,10 @@ const getPathForExecutable = memoizeAsync( const cargoHome = getCargoHome(); if (cargoHome) { - const standardPath = vscode.Uri.joinPath(cargoHome, "bin", executableName); - if (await isFileAtUri(standardPath)) return standardPath.fsPath; + for (const candidate of executableCandidates(executableName)) { + const standardPath = vscode.Uri.joinPath(cargoHome, "bin", candidate); + if (await isFileAtUri(standardPath)) return standardPath.fsPath; + } } return executableName; }, @@ -220,8 +224,7 @@ const getPathForExecutable = memoizeAsync( async function lookupInPath(exec: string, paths: string): Promise<boolean> { const candidates = paths.split(path.delimiter).flatMap((dirInPath) => { - const candidate = path.join(dirInPath, exec); - return os.type() === "Windows_NT" ? [candidate, `${candidate}.exe`] : [candidate]; + return executableCandidates(exec).map((candidate) => path.join(dirInPath, candidate)); }); for await (const isFile of candidates.map(isFileAtPath)) { @@ -232,6 +235,22 @@ async function lookupInPath(exec: string, paths: string): Promise<boolean> { return false; } +function executableCandidates(executableName: string): string[] { + // Keep the extension-side probe aligned with `crates/toolchain::probe_for_binary()`, which + // checks both the bare executable name and the platform suffix such as `.exe` on Windows. + // That matters for both PATH lookups and `$CARGO_HOME/bin/<tool>` fallbacks. + return isWindows ? [executableName, `${executableName}.exe`] : [executableName]; +} + +function getEnvVar(env: Env, name: string): string | undefined { + if (!isWindows) { + return env[name]; + } + + const foldedName = name.toLowerCase(); + return Object.entries(env).find(([key]) => key.toLowerCase() === foldedName)?.[1]; +} + function getCargoHome(): vscode.Uri | null { const envVar = process.env["CARGO_HOME"]; return getCargoHomeFromPath(envVar); diff --git a/editors/code/tests/unit/launch_config.test.ts b/editors/code/tests/unit/launch_config.test.ts index 9bba33f8e9..8da62cd241 100644 --- a/editors/code/tests/unit/launch_config.test.ts +++ b/editors/code/tests/unit/launch_config.test.ts @@ -3,6 +3,7 @@ import * as os from "os"; import * as path from "path"; import { mkdtemp, mkdir, rm, writeFile } from "fs/promises"; import { Cargo, cargoPath } from "../../src/toolchain"; +import { isWindows, normalizeDriveLetter } from "../../src/util"; import type { Context } from "."; export async function getTests(ctx: Context) { @@ -109,13 +110,11 @@ export async function getTests(ctx: Context) { suite.addTest("resolves cargo from provided PATH", async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), "ra-cargo-path-")); try { - const cargoBinary = path.join( - tempDir, - process.platform === "win32" ? "cargo.exe" : "cargo", - ); + const cargoBinary = path.join(tempDir, isWindows ? "cargo.exe" : "cargo"); await writeFile(cargoBinary, ""); - assert.strictEqual(await cargoPath({ PATH: tempDir }), "cargo"); + const pathKey = isWindows ? "Path" : "PATH"; + assert.strictEqual(await cargoPath({ [pathKey]: tempDir }), "cargo"); } finally { await rm(tempDir, { recursive: true, force: true }); } @@ -126,15 +125,12 @@ export async function getTests(ctx: Context) { try { const binDir = path.join(cargoHome, "bin"); await mkdir(binDir); - const cargoBinary = path.join( - binDir, - process.platform === "win32" ? "cargo.exe" : "cargo", - ); + const cargoBinary = path.join(binDir, isWindows ? "cargo.exe" : "cargo"); await writeFile(cargoBinary, ""); assert.strictEqual( - await cargoPath({ PATH: "", CARGO_HOME: cargoHome }), - cargoBinary, + normalizeComparablePath(await cargoPath({ PATH: "", CARGO_HOME: cargoHome })), + normalizeComparablePath(cargoBinary), ); } finally { await rm(cargoHome, { recursive: true, force: true }); @@ -142,3 +138,9 @@ export async function getTests(ctx: Context) { }); }); } + +function normalizeComparablePath(filePath: string): string { + // Windows path comparisons should ignore drive-letter casing because `fsPath` + // may normalize it differently while still pointing to the same file. + return normalizeDriveLetter(filePath, isWindows); +} |