2020-04-28 14:30:49 +00:00
|
|
|
import * as cp from 'child_process';
|
2020-05-05 22:42:04 +00:00
|
|
|
import * as os from 'os';
|
|
|
|
import * as path from 'path';
|
2020-05-31 02:13:08 +00:00
|
|
|
import * as fs from 'fs';
|
2020-04-28 14:30:49 +00:00
|
|
|
import * as readline from 'readline';
|
2020-04-30 12:25:04 +00:00
|
|
|
import { OutputChannel } from 'vscode';
|
2020-05-31 02:13:08 +00:00
|
|
|
import { log, memoize } from './util';
|
2020-04-28 14:30:49 +00:00
|
|
|
|
|
|
|
interface CompilationArtifact {
|
|
|
|
fileName: string;
|
|
|
|
name: string;
|
|
|
|
kind: string;
|
|
|
|
isTest: boolean;
|
|
|
|
}
|
|
|
|
|
2020-05-20 18:03:49 +00:00
|
|
|
export interface ArtifactSpec {
|
|
|
|
cargoArgs: string[];
|
|
|
|
filter?: (artifacts: CompilationArtifact[]) => CompilationArtifact[];
|
|
|
|
}
|
|
|
|
|
2020-05-31 02:13:08 +00:00
|
|
|
export class Cargo {
|
|
|
|
constructor(readonly rootFolder: string, readonly output: OutputChannel) { }
|
2020-05-20 18:03:49 +00:00
|
|
|
|
2020-05-31 02:13:08 +00:00
|
|
|
// Made public for testing purposes
|
|
|
|
static artifactSpec(args: readonly string[]): ArtifactSpec {
|
|
|
|
const cargoArgs = [...args, "--message-format=json"];
|
|
|
|
|
|
|
|
// arguments for a runnable from the quick pick should be updated.
|
|
|
|
// see crates\rust-analyzer\src\main_loop\handlers.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;
|
2020-05-20 18:03:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-31 02:13:08 +00:00
|
|
|
const result: ArtifactSpec = { cargoArgs: cargoArgs };
|
|
|
|
if (cargoArgs[0] === "test") {
|
|
|
|
// 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);
|
|
|
|
}
|
2020-05-20 18:03:49 +00:00
|
|
|
|
2020-05-31 02:13:08 +00:00
|
|
|
return result;
|
|
|
|
}
|
2020-04-28 14:30:49 +00:00
|
|
|
|
2020-05-20 18:03:49 +00:00
|
|
|
private async getArtifacts(spec: ArtifactSpec): Promise<CompilationArtifact[]> {
|
2020-05-21 08:34:34 +00:00
|
|
|
const artifacts: CompilationArtifact[] = [];
|
2020-04-28 14:30:49 +00:00
|
|
|
|
|
|
|
try {
|
2020-05-20 18:03:49 +00:00
|
|
|
await this.runCargo(spec.cargoArgs,
|
2020-04-28 14:30:49 +00:00
|
|
|
message => {
|
2020-04-30 15:41:48 +00:00
|
|
|
if (message.reason === 'compiler-artifact' && message.executable) {
|
|
|
|
const isBinary = message.target.crate_types.includes('bin');
|
|
|
|
const isBuildScript = message.target.kind.includes('custom-build');
|
2020-04-28 14:30:49 +00:00
|
|
|
if ((isBinary && !isBuildScript) || message.profile.test) {
|
|
|
|
artifacts.push({
|
|
|
|
fileName: message.executable,
|
|
|
|
name: message.target.name,
|
|
|
|
kind: message.target.kind[0],
|
|
|
|
isTest: message.profile.test
|
2020-04-30 15:41:48 +00:00
|
|
|
});
|
2020-04-28 14:30:49 +00:00
|
|
|
}
|
2020-05-05 22:22:02 +00:00
|
|
|
} else if (message.reason === 'compiler-message') {
|
2020-04-30 12:25:04 +00:00
|
|
|
this.output.append(message.message.rendered);
|
|
|
|
}
|
2020-04-28 14:30:49 +00:00
|
|
|
},
|
2020-05-05 22:22:02 +00:00
|
|
|
stderr => this.output.append(stderr),
|
2020-04-28 14:30:49 +00:00
|
|
|
);
|
2020-05-05 22:22:02 +00:00
|
|
|
} catch (err) {
|
2020-04-30 12:25:04 +00:00
|
|
|
this.output.show(true);
|
2020-04-28 14:30:49 +00:00
|
|
|
throw new Error(`Cargo invocation has failed: ${err}`);
|
|
|
|
}
|
|
|
|
|
2020-05-21 08:34:34 +00:00
|
|
|
return spec.filter?.(artifacts) ?? artifacts;
|
2020-04-28 14:30:49 +00:00
|
|
|
}
|
|
|
|
|
2020-05-05 22:22:02 +00:00
|
|
|
async executableFromArgs(args: readonly string[]): Promise<string> {
|
2020-05-31 02:13:08 +00:00
|
|
|
const artifacts = await this.getArtifacts(Cargo.artifactSpec(args));
|
2020-04-28 14:30:49 +00:00
|
|
|
|
2020-04-30 15:41:48 +00:00
|
|
|
if (artifacts.length === 0) {
|
2020-04-28 14:30:49 +00:00
|
|
|
throw new Error('No compilation artifacts');
|
|
|
|
} else if (artifacts.length > 1) {
|
|
|
|
throw new Error('Multiple compilation artifacts are not supported.');
|
|
|
|
}
|
|
|
|
|
|
|
|
return artifacts[0].fileName;
|
|
|
|
}
|
|
|
|
|
2020-05-05 22:22:02 +00:00
|
|
|
private runCargo(
|
2020-04-28 14:30:49 +00:00
|
|
|
cargoArgs: string[],
|
|
|
|
onStdoutJson: (obj: any) => void,
|
|
|
|
onStderrString: (data: string) => void
|
|
|
|
): Promise<number> {
|
2020-05-05 22:22:02 +00:00
|
|
|
return new Promise((resolve, reject) => {
|
2020-05-31 02:13:08 +00:00
|
|
|
const cargo = cp.spawn(cargoPath(), cargoArgs, {
|
2020-04-28 14:30:49 +00:00
|
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
2020-05-05 22:22:02 +00:00
|
|
|
cwd: this.rootFolder
|
2020-04-28 14:30:49 +00:00
|
|
|
});
|
|
|
|
|
2020-05-05 22:22:02 +00:00
|
|
|
cargo.on('error', err => reject(new Error(`could not launch cargo: ${err}`)));
|
|
|
|
|
|
|
|
cargo.stderr.on('data', chunk => onStderrString(chunk.toString()));
|
2020-04-28 14:30:49 +00:00
|
|
|
|
2020-04-30 15:41:48 +00:00
|
|
|
const rl = readline.createInterface({ input: cargo.stdout });
|
2020-04-28 14:30:49 +00:00
|
|
|
rl.on('line', line => {
|
2020-04-30 15:41:48 +00:00
|
|
|
const message = JSON.parse(line);
|
2020-04-28 14:30:49 +00:00
|
|
|
onStdoutJson(message);
|
|
|
|
});
|
|
|
|
|
|
|
|
cargo.on('exit', (exitCode, _) => {
|
2020-04-30 15:41:48 +00:00
|
|
|
if (exitCode === 0)
|
2020-04-28 14:30:49 +00:00
|
|
|
resolve(exitCode);
|
|
|
|
else
|
|
|
|
reject(new Error(`exit code: ${exitCode}.`));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2020-05-05 22:22:02 +00:00
|
|
|
}
|
2020-05-05 22:42:04 +00:00
|
|
|
|
2020-05-31 02:13:08 +00:00
|
|
|
/** Mirrors `ra_toolchain::cargo()` implementation */
|
|
|
|
export function cargoPath(): string {
|
|
|
|
return getPathForExecutable("cargo");
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Mirrors `ra_toolchain::get_path_for_executable()` implementation */
|
|
|
|
export const getPathForExecutable = memoize(
|
|
|
|
// We apply caching to decrease file-system interactions
|
|
|
|
(executableName: "cargo" | "rustc" | "rustup"): string => {
|
|
|
|
{
|
|
|
|
const envVar = process.env[executableName.toUpperCase()];
|
|
|
|
if (envVar) return envVar;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lookupInPath(executableName)) return executableName;
|
2020-05-05 22:42:04 +00:00
|
|
|
|
2020-05-31 02:13:08 +00:00
|
|
|
try {
|
|
|
|
// hmm, `os.homedir()` seems to be infallible
|
|
|
|
// it is not mentioned in docs and cannot be infered by the type signature...
|
|
|
|
const standardPath = path.join(os.homedir(), ".cargo", "bin", executableName);
|
2020-05-05 22:42:04 +00:00
|
|
|
|
2020-05-31 02:13:08 +00:00
|
|
|
if (isFile(standardPath)) return standardPath;
|
|
|
|
} catch (err) {
|
|
|
|
log.error("Failed to read the fs info", err);
|
|
|
|
}
|
|
|
|
return executableName;
|
2020-05-05 22:42:04 +00:00
|
|
|
}
|
2020-05-31 02:13:08 +00:00
|
|
|
);
|
2020-05-05 22:42:04 +00:00
|
|
|
|
2020-05-31 02:13:08 +00:00
|
|
|
function lookupInPath(exec: string): boolean {
|
|
|
|
const paths = process.env.PATH ?? "";;
|
2020-05-05 22:42:04 +00:00
|
|
|
|
2020-05-31 02:13:08 +00:00
|
|
|
const candidates = paths.split(path.delimiter).flatMap(dirInPath => {
|
|
|
|
const candidate = path.join(dirInPath, exec);
|
|
|
|
return os.type() === "Windows_NT"
|
|
|
|
? [candidate, `${candidate}.exe`]
|
|
|
|
: [candidate];
|
|
|
|
});
|
2020-05-05 22:42:04 +00:00
|
|
|
|
2020-05-31 02:13:08 +00:00
|
|
|
return candidates.some(isFile);
|
|
|
|
}
|
2020-05-05 22:42:04 +00:00
|
|
|
|
2020-05-31 02:13:08 +00:00
|
|
|
function isFile(suspectPath: string): boolean {
|
|
|
|
// It is not mentionned in docs, but `statSync()` throws an error when
|
|
|
|
// the path doesn't exist
|
|
|
|
try {
|
|
|
|
return fs.statSync(suspectPath).isFile();
|
|
|
|
} catch {
|
|
|
|
return false;
|
|
|
|
}
|
2020-05-05 22:42:04 +00:00
|
|
|
}
|