aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/installation/server.ts
blob: 05d32613158bcd90b40571cc4c42ce9eab721c7b (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
124
125
126
127
128
129
130
131
import * as vscode from "vscode";
import * as path from "path";
import { spawnSync } from "child_process";

import { ArtifactSource } from "./interfaces";
import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
import { downloadArtifactWithProgressUi } from "./downloads";
import { log, assert, notReentrant } from "../util";
import { Config, NIGHTLY_TAG } from "../config";
import { PersistentState } from "../persistent_state";

export async function ensureServerBinary(config: Config, state: PersistentState): Promise<null | string> {
    const source = config.serverSource;

    if (!source) {
        vscode.window.showErrorMessage(
            "Unfortunately we don't ship binaries for your platform yet. " +
            "You need to manually clone rust-analyzer repository and " +
            "run `cargo xtask install --server` to build the language server from sources. " +
            "If you feel that your platform should be supported, please create an issue " +
            "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
            "will consider it."
        );
        return null;
    }

    switch (source.type) {
        case ArtifactSource.Type.ExplicitPath: {
            if (isBinaryAvailable(source.path)) {
                return source.path;
            }

            vscode.window.showErrorMessage(
                `Unable to run ${source.path} binary. ` +
                `To use the pre-built language server, set "rust-analyzer.serverPath" ` +
                "value to `null` or remove it from the settings to use it by default."
            );
            return null;
        }
        case ArtifactSource.Type.GithubRelease: {
            if (!shouldDownloadServer(state, source)) {
                return path.join(source.dir, source.file);
            }

            if (config.askBeforeDownload) {
                const userResponse = await vscode.window.showInformationMessage(
                    `Language server version ${source.tag} for rust-analyzer is not installed. ` +
                    "Do you want to download it now?",
                    "Download now", "Cancel"
                );
                if (userResponse !== "Download now") return null;
            }

            return await downloadServer(state, source);
        }
    }
}

function shouldDownloadServer(
    state: PersistentState,
    source: ArtifactSource.GithubRelease,
): boolean {
    if (!isBinaryAvailable(path.join(source.dir, source.file))) return true;

    const installed = {
        tag: state.serverReleaseTag.get(),
        date: state.serverReleaseDate.get()
    };
    const required = {
        tag: source.tag,
        date: state.installedNightlyExtensionReleaseDate.get()
    };

    log.debug("Installed server:", installed, "required:", required);

    if (required.tag !== NIGHTLY_TAG || installed.tag !== NIGHTLY_TAG) {
        return required.tag !== installed.tag;
    }

    assert(required.date !== null, "Extension release date should have been saved during its installation");
    assert(installed.date !== null, "Server release date should have been saved during its installation");

    return installed.date.getTime() !== required.date.getTime();
}

/**
 * Enforcing no reentrancy for this is best-effort.
 */
const downloadServer = notReentrant(async (
    state: PersistentState,
    source: ArtifactSource.GithubRelease,
): Promise<null | string> => {
    try {
        const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag);

        await downloadArtifactWithProgressUi(releaseInfo, source.file, source.dir, "language server");
        await Promise.all([
            state.serverReleaseTag.set(releaseInfo.releaseName),
            state.serverReleaseDate.set(releaseInfo.releaseDate)
        ]);
    } catch (err) {
        log.downloadError(err, "language server", source.repo.name);
        return null;
    }

    const binaryPath = path.join(source.dir, source.file);

    assert(isBinaryAvailable(binaryPath),
        `Downloaded language server binary is not functional.` +
        `Downloaded from GitHub repo ${source.repo.owner}/${source.repo.name} ` +
        `to ${binaryPath}`
    );

    vscode.window.showInformationMessage(
        "Rust analyzer language server was successfully installed 🦀"
    );

    return binaryPath;
});

function isBinaryAvailable(binaryPath: string): boolean {
    const res = spawnSync(binaryPath, ["--version"]);

    // ACHTUNG! `res` type declaration is inherently wrong, see
    // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221

    log.debug("Checked binary availablity via --version", res);
    log.debug(binaryPath, "--version output:", res.output?.map(String));

    return res.status === 0;
}