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:
bors[bot] 2020-06-02 11:58:28 +00:00 committed by GitHub
commit 131ccd9540
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 140 additions and 81 deletions

View file

@ -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");

View file

@ -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 };

View file

@ -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),
};

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
};
}