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';
9 interface CompilationArtifact {
16 export interface ArtifactSpec {
18 filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[];
22 constructor(readonly rootFolder: string, readonly output: OutputChannel) { }
24 // Made public for testing purposes
25 static artifactSpec(args: readonly string[]): ArtifactSpec {
26 const cargoArgs = [...args, "--message-format=json"];
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;
33 if (!cargoArgs.includes("--no-run")) {
34 cargoArgs.push("--no-run");
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);
50 private async getArtifacts(spec: ArtifactSpec): Promise<CompilationArtifact[]> {
51 const artifacts: CompilationArtifact[] = [];
54 await this.runCargo(spec.cargoArgs,
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) {
61 fileName: message.executable,
62 name: message.target.name,
63 kind: message.target.kind[0],
64 isTest: message.profile.test
67 } else if (message.reason === 'compiler-message') {
68 this.output.append(message.message.rendered);
71 stderr => this.output.append(stderr),
74 this.output.show(true);
75 throw new Error(`Cargo invocation has failed: ${err}`);
78 return spec.filter?.(artifacts) ?? artifacts;
81 async executableFromArgs(args: readonly string[]): Promise<string> {
82 const artifacts = await this.getArtifacts(Cargo.artifactSpec(args));
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.');
90 return artifacts[0].fileName;
95 onStdoutJson: (obj: any) => void,
96 onStderrString: (data: string) => void
98 return new Promise((resolve, reject) => {
99 const cargo = cp.spawn(cargoPath(), cargoArgs, {
100 stdio: ['ignore', 'pipe', 'pipe'],
104 cargo.on('error', err => reject(new Error(`could not launch cargo: ${err}`)));
106 cargo.stderr.on('data', chunk => onStderrString(chunk.toString()));
108 const rl = readline.createInterface({ input: cargo.stdout });
109 rl.on('line', line => {
110 const message = JSON.parse(line);
111 onStdoutJson(message);
114 cargo.on('exit', (exitCode, _) => {
118 reject(new Error(`exit code: ${exitCode}.`));
124 /** Mirrors `project_model::sysroot::discover_sysroot_dir()` implementation*/
125 export function sysrootForDir(dir: string): Promise<string> {
126 const rustc_path = getPathForExecutable("rustc");
128 return new Promise((resolve, reject) => {
129 cp.exec(`${rustc_path} --print sysroot`, { cwd: dir }, (err, stdout, stderr) => {
136 reject(new Error(stderr));
140 resolve(stdout.trimEnd());
145 /** Mirrors `toolchain::cargo()` implementation */
146 export function cargoPath(): string {
147 return getPathForExecutable("cargo");
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 => {
155 const envVar = process.env[executableName.toUpperCase()];
156 if (envVar) return envVar;
159 if (lookupInPath(executableName)) return executableName;
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);
166 if (isFile(standardPath)) return standardPath;
168 log.error("Failed to read the fs info", err);
170 return executableName;
174 function lookupInPath(exec: string): boolean {
175 const paths = process.env.PATH ?? "";;
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`]
184 return candidates.some(isFile);
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
191 return fs.statSync(suspectPath).isFile();