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