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