aboutsummaryrefslogtreecommitdiff
path: root/editors
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2020-02-16 11:54:38 +0000
committerGitHub <[email protected]>2020-02-16 11:54:38 +0000
commita15c8739b9a6da223e1f3a6ff6aa868913c0dbf4 (patch)
tree5694d395b08c416bf60d7206e0380a3859cfe1e2 /editors
parent617b5b3b31cf0b461829810640e28a9090a5b957 (diff)
parent325eba58a286c147f19dada5f205aa9e2ec6f391 (diff)
Merge #3162
3162: Feature: vscode always downloads only the matching ra_lsp_server version r=matklad a=Veetaha I tried to separate logically connected changes into separate commits, so enjoy! Now TypeScript extension saves installed binary version in global state and always checks that the installed binary version equals the version of the TypeScript extension itself (to prevent version drifts). Also, changed `fetchLatestArtifactReleaseInfo()` to `fetchArtifactReleaseInfo()` that takes an optional release tag (when not specified fetches the latest release). The version without a release tag will be useful in the future when adding auto-checking for updates. I decided not to do `Download latest language server` command (I have stated the rationale for this in #3073) and let the extension itself decide which version of the binary it wants. This way the users will be able to get the latest `ra_lsp_server` binary after the approaching 2020-02-17 release, without having to manually delete the outdated one from `~/.config/Code/User/globalStorage/matklad.rust-analyzer`! Closes #3073 Co-authored-by: Veetaha <[email protected]>
Diffstat (limited to 'editors')
-rw-r--r--editors/code/package.json2
-rw-r--r--editors/code/src/client.ts2
-rw-r--r--editors/code/src/config.ts17
-rw-r--r--editors/code/src/ctx.ts4
-rw-r--r--editors/code/src/installation/download_artifact.ts58
-rw-r--r--editors/code/src/installation/fetch_artifact_release_info.ts (renamed from editors/code/src/installation/fetch_latest_artifact_release_info.ts)16
-rw-r--r--editors/code/src/installation/interfaces.ts13
-rw-r--r--editors/code/src/installation/server.ts168
8 files changed, 176 insertions, 104 deletions
diff --git a/editors/code/package.json b/editors/code/package.json
index a607c2148..96b8e9eb0 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -6,7 +6,7 @@
6 "private": true, 6 "private": true,
7 "icon": "icon.png", 7 "icon": "icon.png",
8 "//": "The real version is in release.yaml, this one just needs to be bigger", 8 "//": "The real version is in release.yaml, this one just needs to be bigger",
9 "version": "0.2.0-dev", 9 "version": "0.2.20200211-dev",
10 "publisher": "matklad", 10 "publisher": "matklad",
11 "repository": { 11 "repository": {
12 "url": "https://github.com/rust-analyzer/rust-analyzer.git", 12 "url": "https://github.com/rust-analyzer/rust-analyzer.git",
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index 12c97be2f..efef820ab 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -11,7 +11,7 @@ export async function createClient(config: Config): Promise<null | lc.LanguageCl
11 // It might be a good idea to test if the uri points to a file. 11 // It might be a good idea to test if the uri points to a file.
12 const workspaceFolderPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.'; 12 const workspaceFolderPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.';
13 13
14 const serverPath = await ensureServerBinary(config.serverBinarySource); 14 const serverPath = await ensureServerBinary(config.serverSource);
15 if (!serverPath) return null; 15 if (!serverPath) return null;
16 16
17 const run: lc.Executable = { 17 const run: lc.Executable = {
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index 8c033052b..70cb0a612 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -24,6 +24,19 @@ export class Config {
24 ] 24 ]
25 .map(opt => `${Config.rootSection}.${opt}`); 25 .map(opt => `${Config.rootSection}.${opt}`);
26 26
27 private static readonly extensionVersion: string = (() => {
28 const packageJsonVersion = vscode
29 .extensions
30 .getExtension("matklad.rust-analyzer")!
31 .packageJSON
32 .version as string; // n.n.YYYYMMDD
33
34 const realVersionRegexp = /^\d+\.\d+\.(\d{4})(\d{2})(\d{2})/;
35 const [, yyyy, mm, dd] = packageJsonVersion.match(realVersionRegexp)!;
36
37 return `${yyyy}-${mm}-${dd}`;
38 })();
39
27 private cfg!: vscode.WorkspaceConfiguration; 40 private cfg!: vscode.WorkspaceConfiguration;
28 41
29 constructor(private readonly ctx: vscode.ExtensionContext) { 42 constructor(private readonly ctx: vscode.ExtensionContext) {
@@ -98,7 +111,7 @@ export class Config {
98 } 111 }
99 } 112 }
100 113
101 get serverBinarySource(): null | BinarySource { 114 get serverSource(): null | BinarySource {
102 const serverPath = RA_LSP_DEBUG ?? this.cfg.get<null | string>("raLspServerPath"); 115 const serverPath = RA_LSP_DEBUG ?? this.cfg.get<null | string>("raLspServerPath");
103 116
104 if (serverPath) { 117 if (serverPath) {
@@ -116,6 +129,8 @@ export class Config {
116 type: BinarySource.Type.GithubRelease, 129 type: BinarySource.Type.GithubRelease,
117 dir: this.ctx.globalStoragePath, 130 dir: this.ctx.globalStoragePath,
118 file: prebuiltBinaryName, 131 file: prebuiltBinaryName,
132 storage: this.ctx.globalState,
133 version: Config.extensionVersion,
119 repo: { 134 repo: {
120 name: "rust-analyzer", 135 name: "rust-analyzer",
121 owner: "rust-analyzer", 136 owner: "rust-analyzer",
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index 70042a479..9fcf2ec38 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -60,6 +60,10 @@ export class Ctx {
60 this.pushCleanup(d); 60 this.pushCleanup(d);
61 } 61 }
62 62
63 get globalState(): vscode.Memento {
64 return this.extCtx.globalState;
65 }
66
63 get subscriptions(): Disposable[] { 67 get subscriptions(): Disposable[] {
64 return this.extCtx.subscriptions; 68 return this.extCtx.subscriptions;
65 } 69 }
diff --git a/editors/code/src/installation/download_artifact.ts b/editors/code/src/installation/download_artifact.ts
new file mode 100644
index 000000000..de655f8f4
--- /dev/null
+++ b/editors/code/src/installation/download_artifact.ts
@@ -0,0 +1,58 @@
1import * as vscode from "vscode";
2import * as path from "path";
3import { promises as fs } from "fs";
4import { strict as assert } from "assert";
5
6import { ArtifactReleaseInfo } from "./interfaces";
7import { downloadFile } from "./download_file";
8import { throttle } from "throttle-debounce";
9
10/**
11 * Downloads artifact from given `downloadUrl`.
12 * Creates `installationDir` if it is not yet created and put the artifact under
13 * `artifactFileName`.
14 * Displays info about the download progress in an info message printing the name
15 * of the artifact as `displayName`.
16 */
17export async function downloadArtifact(
18 {downloadUrl, releaseName}: ArtifactReleaseInfo,
19 artifactFileName: string,
20 installationDir: string,
21 displayName: string,
22) {
23 await fs.mkdir(installationDir).catch(err => assert.strictEqual(
24 err?.code,
25 "EEXIST",
26 `Couldn't create directory "${installationDir}" to download `+
27 `${artifactFileName} artifact: ${err.message}`
28 ));
29
30 const installationPath = path.join(installationDir, artifactFileName);
31
32 console.time(`Downloading ${artifactFileName}`);
33 await vscode.window.withProgress(
34 {
35 location: vscode.ProgressLocation.Notification,
36 cancellable: false, // FIXME: add support for canceling download?
37 title: `Downloading ${displayName} (${releaseName})`
38 },
39 async (progress, _cancellationToken) => {
40 let lastPrecentage = 0;
41 const filePermissions = 0o755; // (rwx, r_x, r_x)
42 await downloadFile(downloadUrl, installationPath, filePermissions, throttle(
43 200,
44 /* noTrailing: */ true,
45 (readBytes, totalBytes) => {
46 const newPercentage = (readBytes / totalBytes) * 100;
47 progress.report({
48 message: newPercentage.toFixed(0) + "%",
49 increment: newPercentage - lastPrecentage
50 });
51
52 lastPrecentage = newPercentage;
53 })
54 );
55 }
56 );
57 console.timeEnd(`Downloading ${artifactFileName}`);
58}
diff --git a/editors/code/src/installation/fetch_latest_artifact_release_info.ts b/editors/code/src/installation/fetch_artifact_release_info.ts
index 29ee029a7..7d497057a 100644
--- a/editors/code/src/installation/fetch_latest_artifact_release_info.ts
+++ b/editors/code/src/installation/fetch_artifact_release_info.ts
@@ -3,24 +3,30 @@ import { GithubRepo, ArtifactReleaseInfo } from "./interfaces";
3 3
4const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; 4const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
5 5
6
6/** 7/**
7 * Fetches the latest release from GitHub `repo` and returns metadata about 8 * Fetches the release with `releaseTag` (or just latest release when not specified)
8 * `artifactFileName` shipped with this release or `null` if no such artifact was published. 9 * from GitHub `repo` and returns metadata about `artifactFileName` shipped with
10 * this release or `null` if no such artifact was published.
9 */ 11 */
10export async function fetchLatestArtifactReleaseInfo( 12export async function fetchArtifactReleaseInfo(
11 repo: GithubRepo, artifactFileName: string 13 repo: GithubRepo, artifactFileName: string, releaseTag?: string
12): Promise<null | ArtifactReleaseInfo> { 14): Promise<null | ArtifactReleaseInfo> {
13 15
14 const repoOwner = encodeURIComponent(repo.owner); 16 const repoOwner = encodeURIComponent(repo.owner);
15 const repoName = encodeURIComponent(repo.name); 17 const repoName = encodeURIComponent(repo.name);
16 18
17 const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`; 19 const apiEndpointPath = releaseTag
20 ? `/repos/${repoOwner}/${repoName}/releases/tags/${releaseTag}`
21 : `/repos/${repoOwner}/${repoName}/releases/latest`;
22
18 const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; 23 const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
19 24
20 // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) 25 // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`)
21 26
22 console.log("Issuing request for released artifacts metadata to", requestUrl); 27 console.log("Issuing request for released artifacts metadata to", requestUrl);
23 28
29 // FIXME: handle non-ok response
24 const response: GithubRelease = await fetch(requestUrl, { 30 const response: GithubRelease = await fetch(requestUrl, {
25 headers: { Accept: "application/vnd.github.v3+json" } 31 headers: { Accept: "application/vnd.github.v3+json" }
26 }) 32 })
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts
index 93ea577d4..e40839e4b 100644
--- a/editors/code/src/installation/interfaces.ts
+++ b/editors/code/src/installation/interfaces.ts
@@ -1,3 +1,5 @@
1import * as vscode from "vscode";
2
1export interface GithubRepo { 3export interface GithubRepo {
2 name: string; 4 name: string;
3 owner: string; 5 owner: string;
@@ -50,6 +52,17 @@ export namespace BinarySource {
50 * and in local `.dir`. 52 * and in local `.dir`.
51 */ 53 */
52 file: string; 54 file: string;
55
56 /**
57 * Tag of github release that denotes a version required by this extension.
58 */
59 version: string;
60
61 /**
62 * Object that provides `get()/update()` operations to store metadata
63 * about the actual binary, e.g. its actual version.
64 */
65 storage: vscode.Memento;
53 } 66 }
54 67
55} 68}
diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts
index 406e2299c..80cb719e3 100644
--- a/editors/code/src/installation/server.ts
+++ b/editors/code/src/installation/server.ts
@@ -1,63 +1,15 @@
1import * as vscode from "vscode"; 1import * as vscode from "vscode";
2import * as path from "path"; 2import * as path from "path";
3import { strict as assert } from "assert"; 3import { strict as assert } from "assert";
4import { promises as fs } from "fs";
5import { promises as dns } from "dns"; 4import { promises as dns } from "dns";
6import { spawnSync } from "child_process"; 5import { spawnSync } from "child_process";
7import { throttle } from "throttle-debounce";
8 6
9import { BinarySource } from "./interfaces"; 7import { BinarySource } from "./interfaces";
10import { fetchLatestArtifactReleaseInfo } from "./fetch_latest_artifact_release_info"; 8import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
11import { downloadFile } from "./download_file"; 9import { downloadArtifact } from "./download_artifact";
12
13export async function downloadLatestServer(
14 {file: artifactFileName, dir: installationDir, repo}: BinarySource.GithubRelease
15) {
16 const { releaseName, downloadUrl } = (await fetchLatestArtifactReleaseInfo(
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 ensureServerBinary(
57 serverSource: null | BinarySource
58): Promise<null | string> {
59 10
60 if (!serverSource) { 11export async function ensureServerBinary(source: null | BinarySource): Promise<null | string> {
12 if (!source) {
61 vscode.window.showErrorMessage( 13 vscode.window.showErrorMessage(
62 "Unfortunately we don't ship binaries for your platform yet. " + 14 "Unfortunately we don't ship binaries for your platform yet. " +
63 "You need to manually clone rust-analyzer repository and " + 15 "You need to manually clone rust-analyzer repository and " +
@@ -69,80 +21,104 @@ export async function ensureServerBinary(
69 return null; 21 return null;
70 } 22 }
71 23
72 switch (serverSource.type) { 24 switch (source.type) {
73 case BinarySource.Type.ExplicitPath: { 25 case BinarySource.Type.ExplicitPath: {
74 if (isBinaryAvailable(serverSource.path)) { 26 if (isBinaryAvailable(source.path)) {
75 return serverSource.path; 27 return source.path;
76 } 28 }
77 29
78 vscode.window.showErrorMessage( 30 vscode.window.showErrorMessage(
79 `Unable to run ${serverSource.path} binary. ` + 31 `Unable to run ${source.path} binary. ` +
80 `To use the pre-built language server, set "rust-analyzer.raLspServerPath" ` + 32 `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." 33 "value to `null` or remove it from the settings to use it by default."
82 ); 34 );
83 return null; 35 return null;
84 } 36 }
85 case BinarySource.Type.GithubRelease: { 37 case BinarySource.Type.GithubRelease: {
86 const prebuiltBinaryPath = path.join(serverSource.dir, serverSource.file); 38 const prebuiltBinaryPath = path.join(source.dir, source.file);
39
40 const installedVersion: null | string = getServerVersion(source.storage);
41 const requiredVersion: string = source.version;
87 42
88 if (isBinaryAvailable(prebuiltBinaryPath)) { 43 console.log("Installed version:", installedVersion, "required:", requiredVersion);
44
45 if (isBinaryAvailable(prebuiltBinaryPath) && installedVersion == requiredVersion) {
46 // FIXME: check for new releases and notify the user to update if possible
89 return prebuiltBinaryPath; 47 return prebuiltBinaryPath;
90 } 48 }
91 49
92 const userResponse = await vscode.window.showInformationMessage( 50 const userResponse = await vscode.window.showInformationMessage(
93 "Language server binary for rust-analyzer was not found. " + 51 `Language server version ${source.version} for rust-analyzer is not installed. ` +
94 "Do you want to download it now?", 52 "Do you want to download it now?",
95 "Download now", "Cancel" 53 "Download now", "Cancel"
96 ); 54 );
97 if (userResponse !== "Download now") return null; 55 if (userResponse !== "Download now") return null;
98 56
99 try { 57 if (!await downloadServer(source)) return null;
100 await downloadLatestServer(serverSource);
101 } catch (err) {
102 vscode.window.showErrorMessage(
103 `Failed to download language server from ${serverSource.repo.name} ` +
104 `GitHub repository: ${err.message}`
105 );
106 58
107 console.error(err); 59 return prebuiltBinaryPath;
60 }
61 }
62}
108 63
109 dns.resolve('example.com').then( 64async function downloadServer(source: BinarySource.GithubRelease): Promise<boolean> {
110 addrs => console.log("DNS resolution for example.com was successful", addrs), 65 try {
111 err => { 66 const releaseInfo = (await fetchArtifactReleaseInfo(source.repo, source.file, source.version))!;
112 console.error( 67
113 "DNS resolution for example.com failed, " + 68 await downloadArtifact(releaseInfo, source.file, source.dir, "language server");
114 "there might be an issue with Internet availability" 69 await setServerVersion(source.storage, releaseInfo.releaseName);
115 ); 70 } catch (err) {
116 console.error(err); 71 vscode.window.showErrorMessage(
117 } 72 `Failed to download language server from ${source.repo.name} ` +
118 ); 73 `GitHub repository: ${err.message}`
74 );
75
76 console.error(err);
119 77
120 return null; 78 dns.resolve('example.com').then(
79 addrs => console.log("DNS resolution for example.com was successful", addrs),
80 err => {
81 console.error(
82 "DNS resolution for example.com failed, " +
83 "there might be an issue with Internet availability"
84 );
85 console.error(err);
121 } 86 }
87 );
88 return false;
89 }
122 90
123 if (!isBinaryAvailable(prebuiltBinaryPath)) assert(false, 91 if (!isBinaryAvailable(path.join(source.dir, source.file))) assert(false,
124 `Downloaded language server binary is not functional.` + 92 `Downloaded language server binary is not functional.` +
125 `Downloaded from: ${JSON.stringify(serverSource)}` 93 `Downloaded from: ${JSON.stringify(source, null, 4)}`
126 ); 94 );
127 95
96 vscode.window.showInformationMessage(
97 "Rust analyzer language server was successfully installed 🦀"
98 );
128 99
129 vscode.window.showInformationMessage( 100 return true;
130 "Rust analyzer language server was successfully installed 🦀" 101}
131 );
132 102
133 return prebuiltBinaryPath; 103function isBinaryAvailable(binaryPath: string): boolean {
134 } 104 const res = spawnSync(binaryPath, ["--version"]);
135 }
136 105
137 function isBinaryAvailable(binaryPath: string) { 106 // ACHTUNG! `res` type declaration is inherently wrong, see
138 const res = spawnSync(binaryPath, ["--version"]); 107 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221
139 108
140 // ACHTUNG! `res` type declaration is inherently wrong, see 109 console.log("Checked binary availablity via --version", res);
141 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221 110 console.log(binaryPath, "--version output:", res.output?.map(String));
142 111
143 console.log("Checked binary availablity via --version", res); 112 return res.status === 0;
144 console.log(binaryPath, "--version output:", res.output?.map(String)); 113}
145 114
146 return res.status === 0; 115function getServerVersion(storage: vscode.Memento): null | string {
147 } 116 const version = storage.get<null | string>("server-version", null);
117 console.log("Get server-version:", version);
118 return version;
119}
120
121async function setServerVersion(storage: vscode.Memento, version: string): Promise<void> {
122 console.log("Set server-version:", version);
123 await storage.update("server-version", version.toString());
148} 124}