• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import * as cp from "child_process";
2import * as os from "os";
3import * as path from "path";
4import * as readline from "readline";
5import * as vscode from "vscode";
6import { execute, log, memoizeAsync } from "./util";
7
8interface CompilationArtifact {
9    fileName: string;
10    name: string;
11    kind: string;
12    isTest: boolean;
13}
14
15export interface ArtifactSpec {
16    cargoArgs: string[];
17    filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[];
18}
19
20export class Cargo {
21    constructor(
22        readonly rootFolder: string,
23        readonly output: vscode.OutputChannel,
24        readonly env: Record<string, string>
25    ) {}
26
27    // Made public for testing purposes
28    static artifactSpec(args: readonly string[]): ArtifactSpec {
29        const cargoArgs = [...args, "--message-format=json"];
30
31        // arguments for a runnable from the quick pick should be updated.
32        // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens
33        switch (cargoArgs[0]) {
34            case "run":
35                cargoArgs[0] = "build";
36                break;
37            case "test": {
38                if (!cargoArgs.includes("--no-run")) {
39                    cargoArgs.push("--no-run");
40                }
41                break;
42            }
43        }
44
45        const result: ArtifactSpec = { cargoArgs: cargoArgs };
46        if (cargoArgs[0] === "test" || cargoArgs[0] === "bench") {
47            // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests
48            // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"}
49            result.filter = (artifacts) => artifacts.filter((it) => it.isTest);
50        }
51
52        return result;
53    }
54
55    private async getArtifacts(spec: ArtifactSpec): Promise<CompilationArtifact[]> {
56        const artifacts: CompilationArtifact[] = [];
57
58        try {
59            await this.runCargo(
60                spec.cargoArgs,
61                (message) => {
62                    if (message.reason === "compiler-artifact" && message.executable) {
63                        const isBinary = message.target.crate_types.includes("bin");
64                        const isBuildScript = message.target.kind.includes("custom-build");
65                        if ((isBinary && !isBuildScript) || message.profile.test) {
66                            artifacts.push({
67                                fileName: message.executable,
68                                name: message.target.name,
69                                kind: message.target.kind[0],
70                                isTest: message.profile.test,
71                            });
72                        }
73                    } else if (message.reason === "compiler-message") {
74                        this.output.append(message.message.rendered);
75                    }
76                },
77                (stderr) => this.output.append(stderr)
78            );
79        } catch (err) {
80            this.output.show(true);
81            throw new Error(`Cargo invocation has failed: ${err}`);
82        }
83
84        return spec.filter?.(artifacts) ?? artifacts;
85    }
86
87    async executableFromArgs(args: readonly string[]): Promise<string> {
88        const artifacts = await this.getArtifacts(Cargo.artifactSpec(args));
89
90        if (artifacts.length === 0) {
91            throw new Error("No compilation artifacts");
92        } else if (artifacts.length > 1) {
93            throw new Error("Multiple compilation artifacts are not supported.");
94        }
95
96        return artifacts[0].fileName;
97    }
98
99    private async runCargo(
100        cargoArgs: string[],
101        onStdoutJson: (obj: any) => void,
102        onStderrString: (data: string) => void
103    ): Promise<number> {
104        const path = await cargoPath();
105        return await new Promise((resolve, reject) => {
106            const cargo = cp.spawn(path, cargoArgs, {
107                stdio: ["ignore", "pipe", "pipe"],
108                cwd: this.rootFolder,
109                env: this.env,
110            });
111
112            cargo.on("error", (err) => reject(new Error(`could not launch cargo: ${err}`)));
113
114            cargo.stderr.on("data", (chunk) => onStderrString(chunk.toString()));
115
116            const rl = readline.createInterface({ input: cargo.stdout });
117            rl.on("line", (line) => {
118                const message = JSON.parse(line);
119                onStdoutJson(message);
120            });
121
122            cargo.on("exit", (exitCode, _) => {
123                if (exitCode === 0) resolve(exitCode);
124                else reject(new Error(`exit code: ${exitCode}.`));
125            });
126        });
127    }
128}
129
130/** Mirrors `project_model::sysroot::discover_sysroot_dir()` implementation*/
131export async function getSysroot(dir: string): Promise<string> {
132    const rustcPath = await getPathForExecutable("rustc");
133
134    // do not memoize the result because the toolchain may change between runs
135    return await execute(`${rustcPath} --print sysroot`, { cwd: dir });
136}
137
138export async function getRustcId(dir: string): Promise<string> {
139    const rustcPath = await getPathForExecutable("rustc");
140
141    // do not memoize the result because the toolchain may change between runs
142    const data = await execute(`${rustcPath} -V -v`, { cwd: dir });
143    const rx = /commit-hash:\s(.*)$/m;
144
145    return rx.exec(data)![1];
146}
147
148/** Mirrors `toolchain::cargo()` implementation */
149export function cargoPath(): Promise<string> {
150    return getPathForExecutable("cargo");
151}
152
153/** Mirrors `toolchain::get_path_for_executable()` implementation */
154export const getPathForExecutable = memoizeAsync(
155    // We apply caching to decrease file-system interactions
156    async (executableName: "cargo" | "rustc" | "rustup"): Promise<string> => {
157        {
158            const envVar = process.env[executableName.toUpperCase()];
159            if (envVar) return envVar;
160        }
161
162        if (await lookupInPath(executableName)) return executableName;
163
164        const cargoHome = getCargoHome();
165        if (cargoHome) {
166            const standardPath = vscode.Uri.joinPath(cargoHome, "bin", executableName);
167            if (await isFileAtUri(standardPath)) return standardPath.fsPath;
168        }
169        return executableName;
170    }
171);
172
173async function lookupInPath(exec: string): Promise<boolean> {
174    const paths = process.env.PATH ?? "";
175
176    const candidates = paths.split(path.delimiter).flatMap((dirInPath) => {
177        const candidate = path.join(dirInPath, exec);
178        return os.type() === "Windows_NT" ? [candidate, `${candidate}.exe`] : [candidate];
179    });
180
181    for await (const isFile of candidates.map(isFileAtPath)) {
182        if (isFile) {
183            return true;
184        }
185    }
186    return false;
187}
188
189function getCargoHome(): vscode.Uri | null {
190    const envVar = process.env["CARGO_HOME"];
191    if (envVar) return vscode.Uri.file(envVar);
192
193    try {
194        // hmm, `os.homedir()` seems to be infallible
195        // it is not mentioned in docs and cannot be inferred by the type signature...
196        return vscode.Uri.joinPath(vscode.Uri.file(os.homedir()), ".cargo");
197    } catch (err) {
198        log.error("Failed to read the fs info", err);
199    }
200
201    return null;
202}
203
204async function isFileAtPath(path: string): Promise<boolean> {
205    return isFileAtUri(vscode.Uri.file(path));
206}
207
208async function isFileAtUri(uri: vscode.Uri): Promise<boolean> {
209    try {
210        return ((await vscode.workspace.fs.stat(uri)).type & vscode.FileType.File) !== 0;
211    } catch {
212        return false;
213    }
214}
215