aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/installation
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src/installation')
-rw-r--r--editors/code/src/installation/download_file.ts34
-rw-r--r--editors/code/src/installation/fetch_latest_artifact_metadata.ts46
-rw-r--r--editors/code/src/installation/interfaces.ts55
-rw-r--r--editors/code/src/installation/language_server.ts141
4 files changed, 276 insertions, 0 deletions
diff --git a/editors/code/src/installation/download_file.ts b/editors/code/src/installation/download_file.ts
new file mode 100644
index 000000000..b51602ef9
--- /dev/null
+++ b/editors/code/src/installation/download_file.ts
@@ -0,0 +1,34 @@
1import fetch from "node-fetch";
2import * as fs from "fs";
3import { strict as assert } from "assert";
4
5/**
6 * Downloads file from `url` and stores it at `destFilePath`.
7 * `onProgress` callback is called on recieveing each chunk of bytes
8 * to track the progress of downloading, it gets the already read and total
9 * amount of bytes to read as its parameters.
10 */
11export async function downloadFile(
12 url: string,
13 destFilePath: fs.PathLike,
14 onProgress: (readBytes: number, totalBytes: number) => void
15): Promise<void> {
16 const response = await fetch(url);
17
18 const totalBytes = Number(response.headers.get('content-length'));
19 assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol");
20
21 let readBytes = 0;
22
23 console.log("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath);
24
25 return new Promise<void>((resolve, reject) => response.body
26 .on("data", (chunk: Buffer) => {
27 readBytes += chunk.length;
28 onProgress(readBytes, totalBytes);
29 })
30 .on("end", resolve)
31 .on("error", reject)
32 .pipe(fs.createWriteStream(destFilePath))
33 );
34}
diff --git a/editors/code/src/installation/fetch_latest_artifact_metadata.ts b/editors/code/src/installation/fetch_latest_artifact_metadata.ts
new file mode 100644
index 000000000..7e3700603
--- /dev/null
+++ b/editors/code/src/installation/fetch_latest_artifact_metadata.ts
@@ -0,0 +1,46 @@
1import fetch from "node-fetch";
2import { GithubRepo, ArtifactMetadata } from "./interfaces";
3
4const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
5
6/**
7 * Fetches the latest release from GitHub `repo` and returns metadata about
8 * `artifactFileName` shipped with this release or `null` if no such artifact was published.
9 */
10export async function fetchLatestArtifactMetadata(
11 repo: GithubRepo, artifactFileName: string
12): Promise<null | ArtifactMetadata> {
13
14 const repoOwner = encodeURIComponent(repo.owner);
15 const repoName = encodeURIComponent(repo.name);
16
17 const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`;
18 const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
19
20 // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`)
21
22 console.log("Issuing request for released artifacts metadata to", requestUrl);
23
24 const response: GithubRelease = await fetch(requestUrl, {
25 headers: { Accept: "application/vnd.github.v3+json" }
26 })
27 .then(res => res.json());
28
29 const artifact = response.assets.find(artifact => artifact.name === artifactFileName);
30
31 if (!artifact) return null;
32
33 return {
34 releaseName: response.name,
35 downloadUrl: artifact.browser_download_url
36 };
37
38 // We omit declaration of tremendous amount of fields that we are not using here
39 interface GithubRelease {
40 name: string;
41 assets: Array<{
42 name: string;
43 browser_download_url: string;
44 }>;
45 }
46}
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts
new file mode 100644
index 000000000..8039d0b90
--- /dev/null
+++ b/editors/code/src/installation/interfaces.ts
@@ -0,0 +1,55 @@
1export interface GithubRepo {
2 name: string;
3 owner: string;
4}
5
6/**
7 * Metadata about particular artifact retrieved from GitHub releases.
8 */
9export interface ArtifactMetadata {
10 releaseName: string;
11 downloadUrl: string;
12}
13
14/**
15 * Represents the source of a binary artifact which is either specified by the user
16 * explicitly, or bundled by this extension from GitHub releases.
17 */
18export type BinarySource = BinarySource.ExplicitPath | BinarySource.GithubRelease;
19
20export namespace BinarySource {
21 /**
22 * Type tag for `BinarySource` discriminated union.
23 */
24 export const enum Type { ExplicitPath, GithubRelease }
25
26 export interface ExplicitPath {
27 type: Type.ExplicitPath;
28
29 /**
30 * Filesystem path to the binary specified by the user explicitly.
31 */
32 path: string;
33 }
34
35 export interface GithubRelease {
36 type: Type.GithubRelease;
37
38 /**
39 * Repository where the binary is stored.
40 */
41 repo: GithubRepo;
42
43 /**
44 * Directory on the filesystem where the bundled binary is stored.
45 */
46 dir: string;
47
48 /**
49 * Name of the binary file. It is stored under the same name on GitHub releases
50 * and in local `.dir`.
51 */
52 file: string;
53 }
54
55}
diff --git a/editors/code/src/installation/language_server.ts b/editors/code/src/installation/language_server.ts
new file mode 100644
index 000000000..1ce67b8b2
--- /dev/null
+++ b/editors/code/src/installation/language_server.ts
@@ -0,0 +1,141 @@
1import * as vscode from "vscode";
2import * as path from "path";
3import { strict as assert } from "assert";
4import { promises as fs } from "fs";
5import { promises as dns } from "dns";
6import { spawnSync } from "child_process";
7import { throttle } from "throttle-debounce";
8
9import { BinarySource } from "./interfaces";
10import { fetchLatestArtifactMetadata } from "./fetch_latest_artifact_metadata";
11import { downloadFile } from "./download_file";
12
13export async function downloadLatestLanguageServer(
14 {file: artifactFileName, dir: installationDir, repo}: BinarySource.GithubRelease
15) {
16 const { releaseName, downloadUrl } = (await fetchLatestArtifactMetadata(
17 repo, artifactFileName
18 ))!;
19
20 await fs.mkdir(installationDir).catch(err => assert.strictEqual(
21 err?.code,
22 "EEXIST",
23 `Couldn't create directory "${installationDir}" to download `+
24 `language server binary: ${err.message}`
25 ));
26
27 const installationPath = path.join(installationDir, artifactFileName);
28
29 console.time("Downloading ra_lsp_server");
30 await vscode.window.withProgress(
31 {
32 location: vscode.ProgressLocation.Notification,
33 cancellable: false, // FIXME: add support for canceling download?
34 title: `Downloading language server (${releaseName})`
35 },
36 async (progress, _cancellationToken) => {
37 let lastPrecentage = 0;
38 await downloadFile(downloadUrl, installationPath, throttle(
39 200,
40 /* noTrailing: */ true,
41 (readBytes, totalBytes) => {
42 const newPercentage = (readBytes / totalBytes) * 100;
43 progress.report({
44 message: newPercentage.toFixed(0) + "%",
45 increment: newPercentage - lastPrecentage
46 });
47
48 lastPrecentage = newPercentage;
49 })
50 );
51 }
52 );
53 console.timeEnd("Downloading ra_lsp_server");
54
55 await fs.chmod(installationPath, 0o755); // Set (rwx, r_x, r_x) permissions
56}
57export async function ensureLanguageServerBinary(
58 langServerSource: null | BinarySource
59): Promise<null | string> {
60
61 if (!langServerSource) {
62 vscode.window.showErrorMessage(
63 "Unfortunately we don't ship binaries for your platform yet. " +
64 "You need to manually clone rust-analyzer repository and " +
65 "run `cargo xtask install --server` to build the language server from sources. " +
66 "If you feel that your platform should be supported, please create an issue " +
67 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
68 "will consider it."
69 );
70 return null;
71 }
72
73 switch (langServerSource.type) {
74 case BinarySource.Type.ExplicitPath: {
75 if (isBinaryAvailable(langServerSource.path)) {
76 return langServerSource.path;
77 }
78
79 vscode.window.showErrorMessage(
80 `Unable to run ${langServerSource.path} binary. ` +
81 `To use the pre-built language server, set "rust-analyzer.raLspServerPath" ` +
82 "value to `null` or remove it from the settings to use it by default."
83 );
84 return null;
85 }
86 case BinarySource.Type.GithubRelease: {
87 const prebuiltBinaryPath = path.join(langServerSource.dir, langServerSource.file);
88
89 if (isBinaryAvailable(prebuiltBinaryPath)) {
90 return prebuiltBinaryPath;
91 }
92
93 const userResponse = await vscode.window.showInformationMessage(
94 "Language server binary for rust-analyzer was not found. " +
95 "Do you want to download it now?",
96 "Download now", "Cancel"
97 );
98 if (userResponse !== "Download now") return null;
99
100 try {
101 await downloadLatestLanguageServer(langServerSource);
102 } catch (err) {
103 await vscode.window.showErrorMessage(
104 `Failed to download language server from ${langServerSource.repo.name} ` +
105 `GitHub repository: ${err.message}`
106 );
107
108 await dns.resolve('www.google.com').catch(err => {
109 console.error("DNS resolution failed, there might be an issue with Internet availability");
110 console.error(err);
111 });
112
113 return null;
114 }
115
116 if (!isBinaryAvailable(prebuiltBinaryPath)) assert(false,
117 `Downloaded language server binary is not functional.` +
118 `Downloaded from: ${JSON.stringify(langServerSource)}`
119 );
120
121
122 vscode.window.showInformationMessage(
123 "Rust analyzer language server was successfully installed 🦀"
124 );
125
126 return prebuiltBinaryPath;
127 }
128 }
129
130 function isBinaryAvailable(binaryPath: string) {
131 const res = spawnSync(binaryPath, ["--version"]);
132
133 // ACHTUNG! `res` type declaration is inherently wrong, see
134 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221
135
136 console.log("Checked binary availablity via --version", res);
137 console.log(binaryPath, "--version output:", res.output?.map(String));
138
139 return res.status === 0;
140 }
141}