mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-08-22 11:24:24 +00:00
Merge #4580
4580: Fix invoking cargo without consulting CARGO env var or standard installation paths r=matklad a=Veetaha Followup for #4329 The pr essentially fixes [this bug](https://youtu.be/EzQ7YIIo1rY?t=2189) cc @lefticus Co-authored-by: veetaha <veetaha2@gmail.com>
This commit is contained in:
commit
131ccd9540
14 changed files with 140 additions and 81 deletions
|
@ -3,7 +3,7 @@ import * as vscode from 'vscode';
|
|||
import * as path from 'path';
|
||||
import * as ra from './lsp_ext';
|
||||
|
||||
import { Cargo } from './cargo';
|
||||
import { Cargo } from './toolchain';
|
||||
import { Ctx } from "./ctx";
|
||||
|
||||
const debugOutput = vscode.window.createOutputChannel("Debug");
|
||||
|
|
|
@ -45,10 +45,13 @@ export interface RunnablesParams {
|
|||
textDocument: lc.TextDocumentIdentifier;
|
||||
position: lc.Position | null;
|
||||
}
|
||||
|
||||
export type RunnableKind = "cargo" | "rustc" | "rustup";
|
||||
|
||||
export interface Runnable {
|
||||
range: lc.Range;
|
||||
label: string;
|
||||
bin: string;
|
||||
kind: RunnableKind;
|
||||
args: string[];
|
||||
extraArgs: string[];
|
||||
env: { [key: string]: string };
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as lc from 'vscode-languageclient';
|
||||
import * as ra from './lsp_ext';
|
||||
import * as toolchain from "./toolchain";
|
||||
|
||||
import { Ctx, Cmd } from './ctx';
|
||||
import { startDebugSession, getDebugConfiguration } from './debug';
|
||||
|
@ -175,7 +176,7 @@ export function createTask(spec: ra.Runnable): vscode.Task {
|
|||
const definition: CargoTaskDefinition = {
|
||||
type: 'cargo',
|
||||
label: spec.label,
|
||||
command: spec.bin,
|
||||
command: toolchain.getPathForExecutable(spec.kind),
|
||||
args: spec.extraArgs ? [...spec.args, '--', ...spec.extraArgs] : spec.args,
|
||||
env: Object.assign({}, process.env, spec.env),
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as toolchain from "./toolchain";
|
||||
|
||||
// This ends up as the `type` key in tasks.json. RLS also uses `cargo` and
|
||||
// our configuration should be compatible with it so use the same key.
|
||||
|
@ -24,6 +25,8 @@ class CargoTaskProvider implements vscode.TaskProvider {
|
|||
// set of tasks that always exist. These tasks cannot be removed in
|
||||
// tasks.json - only tweaked.
|
||||
|
||||
const cargoPath = toolchain.cargoPath();
|
||||
|
||||
return [
|
||||
{ command: 'build', group: vscode.TaskGroup.Build },
|
||||
{ command: 'check', group: vscode.TaskGroup.Build },
|
||||
|
@ -46,7 +49,7 @@ class CargoTaskProvider implements vscode.TaskProvider {
|
|||
`cargo ${command}`,
|
||||
'rust',
|
||||
// What to do when this command is executed.
|
||||
new vscode.ShellExecution('cargo', [command]),
|
||||
new vscode.ShellExecution(cargoPath, [command]),
|
||||
// Problem matchers.
|
||||
['$rustc'],
|
||||
);
|
||||
|
@ -80,4 +83,4 @@ class CargoTaskProvider implements vscode.TaskProvider {
|
|||
export function activateTaskProvider(target: vscode.WorkspaceFolder): vscode.Disposable {
|
||||
const provider = new CargoTaskProvider(target);
|
||||
return vscode.tasks.registerTaskProvider(TASK_TYPE, provider);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import * as cp from 'child_process';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as readline from 'readline';
|
||||
import { OutputChannel } from 'vscode';
|
||||
import { isValidExecutable } from './util';
|
||||
import { log, memoize } from './util';
|
||||
|
||||
interface CompilationArtifact {
|
||||
fileName: string;
|
||||
|
@ -17,34 +18,35 @@ export interface ArtifactSpec {
|
|||
filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[];
|
||||
}
|
||||
|
||||
export function artifactSpec(args: readonly string[]): ArtifactSpec {
|
||||
const cargoArgs = [...args, "--message-format=json"];
|
||||
|
||||
// arguments for a runnable from the quick pick should be updated.
|
||||
// see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens
|
||||
switch (cargoArgs[0]) {
|
||||
case "run": cargoArgs[0] = "build"; break;
|
||||
case "test": {
|
||||
if (!cargoArgs.includes("--no-run")) {
|
||||
cargoArgs.push("--no-run");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const result: ArtifactSpec = { cargoArgs: cargoArgs };
|
||||
if (cargoArgs[0] === "test") {
|
||||
// for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests
|
||||
// produce 2 artifacts: {"kind": "bin"} and {"kind": "test"}
|
||||
result.filter = (artifacts) => artifacts.filter(it => it.isTest);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export class Cargo {
|
||||
constructor(readonly rootFolder: string, readonly output: OutputChannel) { }
|
||||
|
||||
// Made public for testing purposes
|
||||
static artifactSpec(args: readonly string[]): ArtifactSpec {
|
||||
const cargoArgs = [...args, "--message-format=json"];
|
||||
|
||||
// arguments for a runnable from the quick pick should be updated.
|
||||
// see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens
|
||||
switch (cargoArgs[0]) {
|
||||
case "run": cargoArgs[0] = "build"; break;
|
||||
case "test": {
|
||||
if (!cargoArgs.includes("--no-run")) {
|
||||
cargoArgs.push("--no-run");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const result: ArtifactSpec = { cargoArgs: cargoArgs };
|
||||
if (cargoArgs[0] === "test") {
|
||||
// for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests
|
||||
// produce 2 artifacts: {"kind": "bin"} and {"kind": "test"}
|
||||
result.filter = (artifacts) => artifacts.filter(it => it.isTest);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async getArtifacts(spec: ArtifactSpec): Promise<CompilationArtifact[]> {
|
||||
const artifacts: CompilationArtifact[] = [];
|
||||
|
||||
|
@ -77,7 +79,7 @@ export class Cargo {
|
|||
}
|
||||
|
||||
async executableFromArgs(args: readonly string[]): Promise<string> {
|
||||
const artifacts = await this.getArtifacts(artifactSpec(args));
|
||||
const artifacts = await this.getArtifacts(Cargo.artifactSpec(args));
|
||||
|
||||
if (artifacts.length === 0) {
|
||||
throw new Error('No compilation artifacts');
|
||||
|
@ -94,14 +96,7 @@ export class Cargo {
|
|||
onStderrString: (data: string) => void
|
||||
): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let cargoPath;
|
||||
try {
|
||||
cargoPath = getCargoPathOrFail();
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
const cargo = cp.spawn(cargoPath, cargoArgs, {
|
||||
const cargo = cp.spawn(cargoPath(), cargoArgs, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
cwd: this.rootFolder
|
||||
});
|
||||
|
@ -126,26 +121,54 @@ export class Cargo {
|
|||
}
|
||||
}
|
||||
|
||||
// Mirrors `ra_env::get_path_for_executable` implementation
|
||||
function getCargoPathOrFail(): string {
|
||||
const envVar = process.env.CARGO;
|
||||
const executableName = "cargo";
|
||||
|
||||
if (envVar) {
|
||||
if (isValidExecutable(envVar)) return envVar;
|
||||
|
||||
throw new Error(`\`${envVar}\` environment variable points to something that's not a valid executable`);
|
||||
}
|
||||
|
||||
if (isValidExecutable(executableName)) return executableName;
|
||||
|
||||
const standardLocation = path.join(os.homedir(), '.cargo', 'bin', executableName);
|
||||
|
||||
if (isValidExecutable(standardLocation)) return standardLocation;
|
||||
|
||||
throw new Error(
|
||||
`Failed to find \`${executableName}\` executable. ` +
|
||||
`Make sure \`${executableName}\` is in \`$PATH\`, ` +
|
||||
`or set \`${envVar}\` to point to a valid executable.`
|
||||
);
|
||||
/** Mirrors `ra_toolchain::cargo()` implementation */
|
||||
export function cargoPath(): string {
|
||||
return getPathForExecutable("cargo");
|
||||
}
|
||||
|
||||
/** Mirrors `ra_toolchain::get_path_for_executable()` implementation */
|
||||
export const getPathForExecutable = memoize(
|
||||
// We apply caching to decrease file-system interactions
|
||||
(executableName: "cargo" | "rustc" | "rustup"): string => {
|
||||
{
|
||||
const envVar = process.env[executableName.toUpperCase()];
|
||||
if (envVar) return envVar;
|
||||
}
|
||||
|
||||
if (lookupInPath(executableName)) return executableName;
|
||||
|
||||
try {
|
||||
// hmm, `os.homedir()` seems to be infallible
|
||||
// it is not mentioned in docs and cannot be infered by the type signature...
|
||||
const standardPath = path.join(os.homedir(), ".cargo", "bin", executableName);
|
||||
|
||||
if (isFile(standardPath)) return standardPath;
|
||||
} catch (err) {
|
||||
log.error("Failed to read the fs info", err);
|
||||
}
|
||||
return executableName;
|
||||
}
|
||||
);
|
||||
|
||||
function lookupInPath(exec: string): boolean {
|
||||
const paths = process.env.PATH ?? "";;
|
||||
|
||||
const candidates = paths.split(path.delimiter).flatMap(dirInPath => {
|
||||
const candidate = path.join(dirInPath, exec);
|
||||
return os.type() === "Windows_NT"
|
||||
? [candidate, `${candidate}.exe`]
|
||||
: [candidate];
|
||||
});
|
||||
|
||||
return candidates.some(isFile);
|
||||
}
|
||||
|
||||
function isFile(suspectPath: string): boolean {
|
||||
// It is not mentionned in docs, but `statSync()` throws an error when
|
||||
// the path doesn't exist
|
||||
try {
|
||||
return fs.statSync(suspectPath).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -99,3 +99,21 @@ export function isValidExecutable(path: string): boolean {
|
|||
export function setContextValue(key: string, value: any): Thenable<void> {
|
||||
return vscode.commands.executeCommand('setContext', key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a higher-order function that caches the results of invoking the
|
||||
* underlying function.
|
||||
*/
|
||||
export function memoize<Ret, TThis, Param extends string>(func: (this: TThis, arg: Param) => Ret) {
|
||||
const cache = new Map<string, Ret>();
|
||||
|
||||
return function(this: TThis, arg: Param) {
|
||||
const cached = cache.get(arg);
|
||||
if (cached) return cached;
|
||||
|
||||
const result = func.call(this, arg);
|
||||
cache.set(arg, result);
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue