| import * as cp from "child_process"; |
| import * as os from "os"; |
| import * as path from "path"; |
| import * as readline from "readline"; |
| import * as vscode from "vscode"; |
| import { log, memoizeAsync, unwrapUndefinable } from "./util"; |
| import type { CargoRunnableArgs } from "./lsp_ext"; |
| |
| interface CompilationArtifact { |
| fileName: string; |
| name: string; |
| kind: string; |
| isTest: boolean; |
| } |
| |
| export interface ArtifactSpec { |
| cargoArgs: string[]; |
| filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[]; |
| } |
| |
| export class Cargo { |
| constructor( |
| readonly rootFolder: string, |
| readonly output: vscode.OutputChannel, |
| readonly env: Record<string, string>, |
| ) {} |
| |
| // Made public for testing purposes |
| static artifactSpec(cargoArgs: string[], executableArgs?: string[]): ArtifactSpec { |
| cargoArgs = [...cargoArgs, "--message-format=json"]; |
| // arguments for a runnable from the quick pick should be updated. |
| // see crates\rust-analyzer\src\handlers\request.rs, handle_code_lens |
| switch (cargoArgs[0]) { |
| case "run": |
| cargoArgs[0] = "build"; |
| break; |
| case "test": { |
| if (!cargoArgs.includes("--no-run")) { |
| cargoArgs.push("--no-run"); |
| } |
| break; |
| } |
| } |
| |
| const result: ArtifactSpec = { cargoArgs: cargoArgs }; |
| if (cargoArgs[0] === "test" || cargoArgs[0] === "bench") { |
| // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests |
| // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"} |
| result.filter = (artifacts) => artifacts.filter((it) => it.isTest); |
| } |
| if (executableArgs) { |
| cargoArgs.push("--", ...executableArgs); |
| } |
| |
| return result; |
| } |
| |
| private async getArtifacts( |
| spec: ArtifactSpec, |
| env?: Record<string, string>, |
| ): Promise<CompilationArtifact[]> { |
| const artifacts: CompilationArtifact[] = []; |
| |
| try { |
| await this.runCargo( |
| spec.cargoArgs, |
| (message) => { |
| if (message.reason === "compiler-artifact" && message.executable) { |
| const isBinary = message.target.crate_types.includes("bin"); |
| const isBuildScript = message.target.kind.includes("custom-build"); |
| if ((isBinary && !isBuildScript) || message.profile.test) { |
| artifacts.push({ |
| fileName: message.executable, |
| name: message.target.name, |
| kind: message.target.kind[0], |
| isTest: message.profile.test, |
| }); |
| } |
| } else if (message.reason === "compiler-message") { |
| this.output.append(message.message.rendered); |
| } |
| }, |
| (stderr) => this.output.append(stderr), |
| env, |
| ); |
| } catch (err) { |
| this.output.show(true); |
| throw new Error(`Cargo invocation has failed: ${err}`); |
| } |
| |
| return spec.filter?.(artifacts) ?? artifacts; |
| } |
| |
| async executableFromArgs(runnableArgs: CargoRunnableArgs): Promise<string> { |
| const artifacts = await this.getArtifacts( |
| Cargo.artifactSpec(runnableArgs.cargoArgs, runnableArgs.executableArgs), |
| runnableArgs.environment, |
| ); |
| |
| if (artifacts.length === 0) { |
| throw new Error("No compilation artifacts"); |
| } else if (artifacts.length > 1) { |
| throw new Error("Multiple compilation artifacts are not supported."); |
| } |
| |
| const artifact = unwrapUndefinable(artifacts[0]); |
| return artifact.fileName; |
| } |
| |
| private async runCargo( |
| cargoArgs: string[], |
| onStdoutJson: (obj: any) => void, |
| onStderrString: (data: string) => void, |
| env?: Record<string, string>, |
| ): Promise<number> { |
| const path = await cargoPath(env); |
| return await new Promise((resolve, reject) => { |
| const cargo = cp.spawn(path, cargoArgs, { |
| stdio: ["ignore", "pipe", "pipe"], |
| cwd: this.rootFolder, |
| env: this.env, |
| }); |
| |
| cargo.on("error", (err) => reject(new Error(`could not launch cargo: ${err}`))); |
| |
| cargo.stderr.on("data", (chunk) => onStderrString(chunk.toString())); |
| |
| const rl = readline.createInterface({ input: cargo.stdout }); |
| rl.on("line", (line) => { |
| const message = JSON.parse(line); |
| onStdoutJson(message); |
| }); |
| |
| cargo.on("exit", (exitCode, _) => { |
| if (exitCode === 0) resolve(exitCode); |
| else reject(new Error(`exit code: ${exitCode}.`)); |
| }); |
| }); |
| } |
| } |
| |
| /** Mirrors `toolchain::cargo()` implementation */ |
| // FIXME: The server should provide this |
| export function cargoPath(env?: Record<string, string>): Promise<string> { |
| if (env?.["RUSTC_TOOLCHAIN"]) { |
| return Promise.resolve("cargo"); |
| } |
| return getPathForExecutable("cargo"); |
| } |
| |
| /** Mirrors `toolchain::get_path_for_executable()` implementation */ |
| const getPathForExecutable = memoizeAsync( |
| // We apply caching to decrease file-system interactions |
| async (executableName: "cargo" | "rustc" | "rustup"): Promise<string> => { |
| { |
| const envVar = process.env[executableName.toUpperCase()]; |
| if (envVar) return envVar; |
| } |
| |
| if (await lookupInPath(executableName)) return executableName; |
| |
| const cargoHome = getCargoHome(); |
| if (cargoHome) { |
| const standardPath = vscode.Uri.joinPath(cargoHome, "bin", executableName); |
| if (await isFileAtUri(standardPath)) return standardPath.fsPath; |
| } |
| return executableName; |
| }, |
| ); |
| |
| async function lookupInPath(exec: string): Promise<boolean> { |
| const paths = process.env["PATH"] ?? ""; |
| |
| const candidates = paths.split(path.delimiter).flatMap((dirInPath) => { |
| const candidate = path.join(dirInPath, exec); |
| return os.type() === "Windows_NT" ? [candidate, `${candidate}.exe`] : [candidate]; |
| }); |
| |
| for await (const isFile of candidates.map(isFileAtPath)) { |
| if (isFile) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| function getCargoHome(): vscode.Uri | null { |
| const envVar = process.env["CARGO_HOME"]; |
| if (envVar) return vscode.Uri.file(envVar); |
| |
| try { |
| // hmm, `os.homedir()` seems to be infallible |
| // it is not mentioned in docs and cannot be inferred by the type signature... |
| return vscode.Uri.joinPath(vscode.Uri.file(os.homedir()), ".cargo"); |
| } catch (err) { |
| log.error("Failed to read the fs info", err); |
| } |
| |
| return null; |
| } |
| |
| async function isFileAtPath(path: string): Promise<boolean> { |
| return isFileAtUri(vscode.Uri.file(path)); |
| } |
| |
| async function isFileAtUri(uri: vscode.Uri): Promise<boolean> { |
| try { |
| return ((await vscode.workspace.fs.stat(uri)).type & vscode.FileType.File) !== 0; |
| } catch { |
| return false; |
| } |
| } |