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.ts44
-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.ts146
4 files changed, 291 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..f1f9f4a25
--- /dev/null
+++ b/editors/code/src/installation/download_file.ts
@@ -0,0 +1,44 @@
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` with `destFilePermissions`.
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 destFilePermissions: number,
15 onProgress: (readBytes: number, totalBytes: number) => void
16): Promise<void> {
17 const res = await fetch(url);
18
19 if (!res.ok) {
20 console.log("Error", res.status, "while downloading file from", url);
21 console.dir({ body: await res.text(), headers: res.headers }, { depth: 3 });
22
23 throw new Error(`Got response ${res.status} when trying to download a file`);
24 }
25
26 const totalBytes = Number(res.headers.get('content-length'));
27 assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol");
28
29 let readBytes = 0;
30
31 console.log("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath);
32
33 return new Promise<void>((resolve, reject) => res.body
34 .on("data", (chunk: Buffer) => {
35 readBytes += chunk.length;
36 onProgress(readBytes, totalBytes);
37 })
38 .on("error", reject)
39 .pipe(fs
40 .createWriteStream(destFilePath, { mode: destFilePermissions })
41 .on("close", resolve)
42 )
43 );
44}
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..52c5cbe7d
--- /dev/null
+++ b/editors/code/src/installation/language_server.ts
@@ -0,0 +1,146 @@
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 const filePermissions = 0o755; // (rwx, r_x, r_x)
39 await downloadFile(downloadUrl, installationPath, filePermissions, throttle(
40 200,
41 /* noTrailing: */ true,
42 (readBytes, totalBytes) => {
43 const newPercentage = (readBytes / totalBytes) * 100;
44 progress.report({
45 message: newPercentage.toFixed(0) + "%",
46 increment: newPercentage - lastPrecentage
47 });
48
49 lastPrecentage = newPercentage;
50 })
51 );
52 }
53 );
54 console.timeEnd("Downloading ra_lsp_server");
55}
56export async function ensureLanguageServerBinary(
57 langServerSource: null | BinarySource
58): Promise<null | string> {
59
60 if (!langServerSource) {
61 vscode.window.showErrorMessage(
62 "Unfortunately we don't ship binaries for your platform yet. " +
63 "You need to manually clone rust-analyzer repository and " +
64 "run `cargo xtask install --server` to build the language server from sources. " +
65 "If you feel that your platform should be supported, please create an issue " +
66 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
67 "will consider it."
68 );
69 return null;
70 }
71
72 switch (langServerSource.type) {
73 case BinarySource.Type.ExplicitPath: {
74 if (isBinaryAvailable(langServerSource.path)) {
75 return langServerSource.path;
76 }
77
78 vscode.window.showErrorMessage(
79 `Unable to run ${langServerSource.path} binary. ` +
80 `To use the pre-built language server, set "rust-analyzer.raLspServerPath" ` +
81 "value to `null` or remove it from the settings to use it by default."
82 );
83 return null;
84 }
85 case BinarySource.Type.GithubRelease: {
86 const prebuiltBinaryPath = path.join(langServerSource.dir, langServerSource.file);
87
88 if (isBinaryAvailable(prebuiltBinaryPath)) {
89 return prebuiltBinaryPath;
90 }
91
92 const userResponse = await vscode.window.showInformationMessage(
93 "Language server binary for rust-analyzer was not found. " +
94 "Do you want to download it now?",
95 "Download now", "Cancel"
96 );
97 if (userResponse !== "Download now") return null;
98
99 try {
100 await downloadLatestLanguageServer(langServerSource);
101 } catch (err) {
102 vscode.window.showErrorMessage(
103 `Failed to download language server from ${langServerSource.repo.name} ` +
104 `GitHub repository: ${err.message}`
105 );
106
107 dns.resolve('example.com').then(
108 addrs => console.log("DNS resolution for example.com was successful", addrs),
109 err => {
110 console.error(
111 "DNS resolution for example.com failed, " +
112 "there might be an issue with Internet availability"
113 );
114 console.error(err);
115 }
116 );
117
118 return null;
119 }
120
121 if (!isBinaryAvailable(prebuiltBinaryPath)) assert(false,
122 `Downloaded language server binary is not functional.` +
123 `Downloaded from: ${JSON.stringify(langServerSource)}`
124 );
125
126
127 vscode.window.showInformationMessage(
128 "Rust analyzer language server was successfully installed 🦀"
129 );
130
131 return prebuiltBinaryPath;
132 }
133 }
134
135 function isBinaryAvailable(binaryPath: string) {
136 const res = spawnSync(binaryPath, ["--version"]);
137
138 // ACHTUNG! `res` type declaration is inherently wrong, see
139 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221
140
141 console.log("Checked binary availablity via --version", res);
142 console.log(binaryPath, "--version output:", res.output?.map(String));
143
144 return res.status === 0;
145 }
146}