aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/installation
diff options
context:
space:
mode:
authorVeetaha <[email protected]>2020-02-08 02:22:44 +0000
committerVeetaha <[email protected]>2020-02-08 02:34:11 +0000
commit5d88c1db38200896d2e4af7836fec95097adf509 (patch)
treee865f9b5446b1630acf554f353856334a8ef6007 /editors/code/src/installation
parent3e0e4e90aeeff25db674f8db562c611bd8016482 (diff)
vscode: amended config to use binary from globalStoragePath, added ui for downloading
Diffstat (limited to 'editors/code/src/installation')
-rw-r--r--editors/code/src/installation/download_file.ts26
-rw-r--r--editors/code/src/installation/fetch_latest_artifact_metadata.ts47
-rw-r--r--editors/code/src/installation/interfaces.ts26
-rw-r--r--editors/code/src/installation/language_server.ts119
4 files changed, 218 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..7b537e114
--- /dev/null
+++ b/editors/code/src/installation/download_file.ts
@@ -0,0 +1,26 @@
1import fetch from "node-fetch";
2import { throttle } from "throttle-debounce";
3import * as fs from "fs";
4
5export async function downloadFile(
6 url: string,
7 destFilePath: fs.PathLike,
8 onProgress: (readBytes: number, totalBytes: number) => void
9): Promise<void> {
10 onProgress = throttle(1000, /* noTrailing: */ true, onProgress);
11
12 const response = await fetch(url);
13
14 const totalBytes = Number(response.headers.get('content-length'));
15 let readBytes = 0;
16
17 return new Promise<void>((resolve, reject) => response.body
18 .on("data", (chunk: Buffer) => {
19 readBytes += chunk.length;
20 onProgress(readBytes, totalBytes);
21 })
22 .on("end", resolve)
23 .on("error", reject)
24 .pipe(fs.createWriteStream(destFilePath))
25 );
26}
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..f07431aac
--- /dev/null
+++ b/editors/code/src/installation/fetch_latest_artifact_metadata.ts
@@ -0,0 +1,47 @@
1import fetch from "node-fetch";
2import { GithubRepo, ArtifactMetadata } from "./interfaces";
3
4const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
5
6export interface FetchLatestArtifactMetadataOpts {
7 repo: GithubRepo;
8 artifactFileName: string;
9}
10
11export async function fetchLatestArtifactMetadata(
12 opts: FetchLatestArtifactMetadataOpts
13): Promise<null | ArtifactMetadata> {
14
15 const repoOwner = encodeURIComponent(opts.repo.owner);
16 const repoName = encodeURIComponent(opts.repo.name);
17
18 const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`;
19 const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
20
21 // We skip runtime type checks for simplicity (here we cast from `any` to `Release`)
22
23 const response: GithubRelease = await fetch(requestUrl, {
24 headers: { Accept: "application/vnd.github.v3+json" }
25 })
26 .then(res => res.json());
27
28 const artifact = response.assets.find(artifact => artifact.name === opts.artifactFileName);
29
30 return !artifact ? null : {
31 releaseName: response.name,
32 downloadUrl: artifact.browser_download_url
33 };
34
35 // Noise denotes tremendous amount of data that we are not using here
36 interface GithubRelease {
37 name: string;
38 assets: Array<{
39 browser_download_url: string;
40
41 [noise: string]: unknown;
42 }>;
43
44 [noise: string]: unknown;
45 }
46
47}
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts
new file mode 100644
index 000000000..f54e24e26
--- /dev/null
+++ b/editors/code/src/installation/interfaces.ts
@@ -0,0 +1,26 @@
1export interface GithubRepo {
2 name: string;
3 owner: string;
4}
5
6export interface ArtifactMetadata {
7 releaseName: string;
8 downloadUrl: string;
9}
10
11
12export enum BinarySourceType { ExplicitPath, GithubBinary }
13
14export type BinarySource = EplicitPathSource | GithubBinarySource;
15
16export interface EplicitPathSource {
17 type: BinarySourceType.ExplicitPath;
18 path: string;
19}
20
21export interface GithubBinarySource {
22 type: BinarySourceType.GithubBinary;
23 repo: GithubRepo;
24 dir: string;
25 file: string;
26}
diff --git a/editors/code/src/installation/language_server.ts b/editors/code/src/installation/language_server.ts
new file mode 100644
index 000000000..2b3ce6621
--- /dev/null
+++ b/editors/code/src/installation/language_server.ts
@@ -0,0 +1,119 @@
1import { unwrapNotNil } from "ts-not-nil";
2import { spawnSync } from "child_process";
3import * as vscode from "vscode";
4import * as path from "path";
5import { strict as assert } from "assert";
6import { promises as fs } from "fs";
7
8import { BinarySource, BinarySourceType, GithubBinarySource } from "./interfaces";
9import { fetchLatestArtifactMetadata } from "./fetch_latest_artifact_metadata";
10import { downloadFile } from "./download_file";
11
12export async function downloadLatestLanguageServer(
13 {file: artifactFileName, dir: installationDir, repo}: GithubBinarySource
14) {
15 const binaryMetadata = await fetchLatestArtifactMetadata({ artifactFileName, repo });
16
17 const {
18 releaseName,
19 downloadUrl
20 } = unwrapNotNil(binaryMetadata, `Latest GitHub release lacks "${artifactFileName}" file`);
21
22 await fs.mkdir(installationDir).catch(err => assert.strictEqual(
23 err && err.code,
24 "EEXIST",
25 `Couldn't create directory "${installationDir}" to download `+
26 `language server binary: ${err.message}`
27 ));
28
29 const installationPath = path.join(installationDir, artifactFileName);
30
31 await vscode.window.withProgress(
32 {
33 location: vscode.ProgressLocation.Notification,
34 cancellable: false, // FIXME: add support for canceling download?
35 title: `Downloading language server ${releaseName}`
36 },
37 async (progress, _) => {
38 let lastPrecentage = 0;
39 await downloadFile(downloadUrl, installationPath, (readBytes, totalBytes) => {
40 const newPercentage = (readBytes / totalBytes) * 100;
41 progress.report({
42 message: newPercentage.toFixed(0) + "%",
43 increment: newPercentage - lastPrecentage
44 });
45
46 lastPrecentage = newPercentage;
47 });
48 }
49 );
50
51 await fs.chmod(installationPath, 111); // Set xxx permissions
52}
53export async function ensureLanguageServerBinary(
54 langServerSource: null | BinarySource
55): Promise<null | string> {
56
57 if (!langServerSource) {
58 vscode.window.showErrorMessage(
59 "Unfortunately we don't ship binaries for your platform yet. " +
60 "You need to manually clone rust-analyzer repository and " +
61 "run `cargo xtask install --server` to build the language server from sources. " +
62 "If you feel that your platform should be supported, please create an issue " +
63 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
64 "will consider it."
65 );
66 return null;
67 }
68
69 switch (langServerSource.type) {
70 case BinarySourceType.ExplicitPath: {
71 if (isBinaryAvailable(langServerSource.path)) {
72 return langServerSource.path;
73 }
74 vscode.window.showErrorMessage(
75 `Unable to execute ${'`'}${langServerSource.path} --version${'`'}. ` +
76 "To use the bundled language server, set `rust-analyzer.raLspServerPath` " +
77 "value to `null` or remove it from the settings to use it by default."
78 );
79 return null;
80 }
81 case BinarySourceType.GithubBinary: {
82 const bundledBinaryPath = path.join(langServerSource.dir, langServerSource.file);
83
84 if (!isBinaryAvailable(bundledBinaryPath)) {
85 const userResponse = await vscode.window.showInformationMessage(
86 `Language server binary for rust-analyzer was not found. ` +
87 `Do you want to download it now?`,
88 "Download now", "Cancel"
89 );
90 if (userResponse !== "Download now") return null;
91
92 try {
93 await downloadLatestLanguageServer(langServerSource);
94 } catch (err) {
95 await vscode.window.showErrorMessage(
96 `Failed to download language server from ${langServerSource.repo.name} ` +
97 `GitHub repository: ${err.message}`
98 );
99 return null;
100 }
101
102
103 assert(
104 isBinaryAvailable(bundledBinaryPath),
105 "Downloaded language server binary is not functional"
106 );
107
108 vscode.window.showInformationMessage(
109 "Rust analyzer language server was successfully installed"
110 );
111 }
112 return bundledBinaryPath;
113 }
114 }
115
116 function isBinaryAvailable(binaryPath: string) {
117 return spawnSync(binaryPath, ["--version"]).status === 0;
118 }
119}