aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/cargo.ts
blob: 2a2c2e0e1bb8fc7ebc7824e7490221c43e188c08 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import * as cp from 'child_process';
import * as os from 'os';
import * as path from 'path';
import * as readline from 'readline';
import { OutputChannel } from 'vscode';
import { isValidExecutable } from './util';

interface CompilationArtifact {
    fileName: string;
    name: string;
    kind: string;
    isTest: boolean;
}

export class Cargo {
    constructor(readonly rootFolder: string, readonly output: OutputChannel) { }

    private async artifactsFromArgs(cargoArgs: string[]): Promise<CompilationArtifact[]> {
        const artifacts: CompilationArtifact[] = [];

        try {
            await this.runCargo(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),
            );
        } catch (err) {
            this.output.show(true);
            throw new Error(`Cargo invocation has failed: ${err}`);
        }

        return artifacts;
    }

    async executableFromArgs(args: readonly string[]): Promise<string> {
        const cargoArgs = [...args, "--message-format=json"];

        const artifacts = await this.artifactsFromArgs(cargoArgs);

        if (artifacts.length === 0) {
            throw new Error('No compilation artifacts');
        } else if (artifacts.length > 1) {
            throw new Error('Multiple compilation artifacts are not supported.');
        }

        return artifacts[0].fileName;
    }

    private runCargo(
        cargoArgs: string[],
        onStdoutJson: (obj: any) => void,
        onStderrString: (data: string) => void
    ): Promise<number> {
        return new Promise((resolve, reject) => {
            let cargoPath;
            try {
                cargoPath = getCargoPathOrFail();
            } catch (err) {
                return reject(err);
            }

            const cargo = cp.spawn(cargoPath, cargoArgs, {
                stdio: ['ignore', 'pipe', 'pipe'],
                cwd: this.rootFolder
            });

            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 `ra_env::get_path_for_executable` implementation
function getCargoPathOrFail(): string {
    const envVar = process.env.CARGO;
    const executableName = "cargo";

    if (envVar) {
        if (isValidExecutable(envVar)) return envVar;

        throw new Error(`\`${envVar}\` environment variable points to something that's not a valid executable`);
    }

    if (isValidExecutable(executableName)) return executableName;

    const standardLocation = path.join(os.homedir(), '.cargo', 'bin', executableName);

    if (isValidExecutable(standardLocation)) return standardLocation;

    throw new Error(
        `Failed to find \`${executableName}\` executable. ` +
        `Make sure \`${executableName}\` is in \`$PATH\`, ` +
        `or set \`${envVar}\` to point to a valid executable.`
    );
}