aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/toolchain.ts
diff options
context:
space:
mode:
authorveetaha <[email protected]>2020-05-31 03:13:08 +0100
committerveetaha <[email protected]>2020-05-31 03:21:45 +0100
commitd605ec9c321392d9c7ee4b440c560e1e405d92e6 (patch)
tree58d16996d1d1a05733dcc85ae4efddc563b3d3b1 /editors/code/src/toolchain.ts
parenta419cedb1cc661349a022262c8b03993e063252f (diff)
Change Runnable.bin -> Runnable.kind
As per matklad, we now pass the responsibility for finding the binary to the frontend. Also, added caching for finding the binary path to reduce the amount of filesystem interactions.
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}