diff options
author | Veetaha <[email protected]> | 2020-02-08 02:22:44 +0000 |
---|---|---|
committer | Veetaha <[email protected]> | 2020-02-08 02:34:11 +0000 |
commit | 5d88c1db38200896d2e4af7836fec95097adf509 (patch) | |
tree | e865f9b5446b1630acf554f353856334a8ef6007 /editors/code/src/installation | |
parent | 3e0e4e90aeeff25db674f8db562c611bd8016482 (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.ts | 26 | ||||
-rw-r--r-- | editors/code/src/installation/fetch_latest_artifact_metadata.ts | 47 | ||||
-rw-r--r-- | editors/code/src/installation/interfaces.ts | 26 | ||||
-rw-r--r-- | editors/code/src/installation/language_server.ts | 119 |
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 @@ | |||
1 | import fetch from "node-fetch"; | ||
2 | import { throttle } from "throttle-debounce"; | ||
3 | import * as fs from "fs"; | ||
4 | |||
5 | export 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 @@ | |||
1 | import fetch from "node-fetch"; | ||
2 | import { GithubRepo, ArtifactMetadata } from "./interfaces"; | ||
3 | |||
4 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; | ||
5 | |||
6 | export interface FetchLatestArtifactMetadataOpts { | ||
7 | repo: GithubRepo; | ||
8 | artifactFileName: string; | ||
9 | } | ||
10 | |||
11 | export 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 @@ | |||
1 | export interface GithubRepo { | ||
2 | name: string; | ||
3 | owner: string; | ||
4 | } | ||
5 | |||
6 | export interface ArtifactMetadata { | ||
7 | releaseName: string; | ||
8 | downloadUrl: string; | ||
9 | } | ||
10 | |||
11 | |||
12 | export enum BinarySourceType { ExplicitPath, GithubBinary } | ||
13 | |||
14 | export type BinarySource = EplicitPathSource | GithubBinarySource; | ||
15 | |||
16 | export interface EplicitPathSource { | ||
17 | type: BinarySourceType.ExplicitPath; | ||
18 | path: string; | ||
19 | } | ||
20 | |||
21 | export 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 @@ | |||
1 | import { unwrapNotNil } from "ts-not-nil"; | ||
2 | import { spawnSync } from "child_process"; | ||
3 | import * as vscode from "vscode"; | ||
4 | import * as path from "path"; | ||
5 | import { strict as assert } from "assert"; | ||
6 | import { promises as fs } from "fs"; | ||
7 | |||
8 | import { BinarySource, BinarySourceType, GithubBinarySource } from "./interfaces"; | ||
9 | import { fetchLatestArtifactMetadata } from "./fetch_latest_artifact_metadata"; | ||
10 | import { downloadFile } from "./download_file"; | ||
11 | |||
12 | export 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 | } | ||
53 | export 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 | } | ||