aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/toolchain.ts
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src/toolchain.ts')
-rw-r--r--editors/code/src/toolchain.ts174
1 files changed, 174 insertions, 0 deletions
diff --git a/editors/code/src/toolchain.ts b/editors/code/src/toolchain.ts
new file mode 100644
index 000000000..80a7915e9
--- /dev/null
+++ b/editors/code/src/toolchain.ts
@@ -0,0 +1,174 @@
1import * as cp from 'child_process';
2import * as os from 'os';
3import * as path from 'path';
4import * as fs from 'fs';
5import * as readline from 'readline';
6import { OutputChannel } from 'vscode';
7import { log, memoize } from './util';
8
9interface CompilationArtifact {
10 fileName: string;
11 name: string;
12 kind: string;
13 isTest: boolean;
14}
15
16export interface ArtifactSpec {
17 cargoArgs: string[];
18 filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[];
19}
20
21export 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 `ra_toolchain::cargo()` implementation */
125export function cargoPath(): string {
126 return getPathForExecutable("cargo");
127}
128
129/** Mirrors `ra_toolchain::get_path_for_executable()` implementation */
130export const getPathForExecutable = memoize(
131 // We apply caching to decrease file-system interactions
132 (executableName: "cargo" | "rustc" | "rustup"): string => {
133 {
134 const envVar = process.env[executableName.toUpperCase()];
135 if (envVar) return envVar;
136 }
137
138 if (lookupInPath(executableName)) return executableName;
139
140 try {
141 // hmm, `os.homedir()` seems to be infallible
142 // it is not mentioned in docs and cannot be infered by the type signature...
143 const standardPath = path.join(os.homedir(), ".cargo", "bin", executableName);
144
145 if (isFile(standardPath)) return standardPath;
146 } catch (err) {
147 log.error("Failed to read the fs info", err);
148 }
149 return executableName;
150 }
151);
152
153function lookupInPath(exec: string): boolean {
154 const paths = process.env.PATH ?? "";;
155
156 const candidates = paths.split(path.delimiter).flatMap(dirInPath => {
157 const candidate = path.join(dirInPath, exec);
158 return os.type() === "Windows_NT"
159 ? [candidate, `${candidate}.exe`]
160 : [candidate];
161 });
162
163 return candidates.some(isFile);
164}
165
166function isFile(suspectPath: string): boolean {
167 // It is not mentionned in docs, but `statSync()` throws an error when
168 // the path doesn't exist
169 try {
170 return fs.statSync(suspectPath).isFile();
171 } catch {
172 return false;
173 }
174}