Skip to content

Commit 200c275

Browse files
bors[bot]VeetahaVeetaha
authored
Merge #3534
3534: Feature: vscode impl nightlies download and installation r=Veetaha a=Veetaha I need to test things more, but the core shape of the code is quite well-formed. The main problem is that we save the release date only for nightlies and there are no means to get the release date of the stable extension (i.e. for this we would need to consult the github releases via a network request, or we would need to somehow save this info into package.json or any other file accessible from the extension code during the deployment step, but this will be very hard I guess). So there is an invariant that the users can install nightly only from our extension and they can't do it manually, because when installing the nightly `.vsix` we actually save its release date to `globalState` Closes: #3402 TODO: - [x] More manual tests and documentation cc @matklad @lnicola Co-authored-by: Veetaha <gerzoh1@gmail.com> Co-authored-by: Veetaha <veetaha2@gmail.com>
2 parents a99cac6 + 5a0041c commit 200c275

File tree

13 files changed

+454
-147
lines changed

13 files changed

+454
-147
lines changed

docs/user/readme.adoc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,25 @@ Note that we only support the latest version of VS Code.
6565

6666
The extension will be updated automatically as new versions become available. It will ask your permission to download the matching language server version binary if needed.
6767

68+
===== Nightly
69+
70+
We ship nightly releases for VS Code. To help us out with testing the newest code and follow the bleeding edge of our `master`, please use the following config:
71+
72+
[source,json]
73+
----
74+
{ "rust-analyzer.updates.channel": "nightly" }
75+
----
76+
77+
You will be prompted to install the `nightly` extension version. Just click `Download now` and from that moment you will get automatic updates each 24 hours.
78+
79+
If you don't want to be asked for `Download now` every day when the new nightly version is released add the following to your `settings.json`:
80+
[source,json]
81+
----
82+
{ "rust-analyzer.updates.askBeforeDownload": false }
83+
----
84+
85+
NOTE: Nightly extension should **only** be installed via the `Download now` action from VS Code.
86+
6887
==== Building From Source
6988

7089
Alternatively, both the server and the plugin can be installed from source:

editors/code/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editors/code/package.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"private": true,
77
"icon": "icon.png",
88
"//": "The real version is in release.yaml, this one just needs to be bigger",
9-
"version": "0.2.20200211-dev",
9+
"version": "0.2.20200309-nightly",
1010
"publisher": "matklad",
1111
"repository": {
1212
"url": "https://github.com/rust-analyzer/rust-analyzer.git",
@@ -219,6 +219,19 @@
219219
}
220220
}
221221
},
222+
"rust-analyzer.updates.channel": {
223+
"type": "string",
224+
"enum": [
225+
"stable",
226+
"nightly"
227+
],
228+
"default": "stable",
229+
"markdownEnumDescriptions": [
230+
"`\"stable\"` updates are shipped weekly, they don't contain cutting-edge features from VSCode proposed APIs but have less bugs in general",
231+
"`\"nightly\"` updates are shipped daily, they contain cutting-edge features and latest bug fixes. These releases help us get your feedback very quickly and speed up rust-analyzer development **drastically**"
232+
],
233+
"markdownDescription": "Choose `\"nightly\"` updates to get the latest features and bug fixes every day. While `\"stable\"` releases occur weekly and don't contain cutting-edge features from VSCode proposed APIs"
234+
},
222235
"rust-analyzer.updates.askBeforeDownload": {
223236
"type": "boolean",
224237
"default": true,
@@ -235,7 +248,7 @@
235248
"string"
236249
],
237250
"default": null,
238-
"description": "Path to rust-analyzer executable (points to bundled binary by default)"
251+
"description": "Path to rust-analyzer executable (points to bundled binary by default). If this is set, then \"rust-analyzer.updates.channel\" setting is not used"
239252
},
240253
"rust-analyzer.excludeGlobs": {
241254
"type": "array",

editors/code/src/commands/server_version.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { spawnSync } from 'child_process';
55

66
export function serverVersion(ctx: Ctx): Cmd {
77
return async () => {
8-
const binaryPath = await ensureServerBinary(ctx.config.serverSource);
8+
const binaryPath = await ensureServerBinary(ctx.config);
99

1010
if (binaryPath == null) {
1111
throw new Error(
@@ -18,4 +18,3 @@ export function serverVersion(ctx: Ctx): Cmd {
1818
vscode.window.showInformationMessage('rust-analyzer version : ' + version);
1919
};
2020
}
21-

editors/code/src/config.ts

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as os from "os";
22
import * as vscode from 'vscode';
33
import { ArtifactSource } from "./installation/interfaces";
4-
import { log } from "./util";
4+
import { log, vscodeReloadWindow } from "./util";
55

66
const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
77

@@ -23,25 +23,40 @@ export interface CargoFeatures {
2323
allFeatures: boolean;
2424
features: string[];
2525
}
26+
27+
export const enum UpdatesChannel {
28+
Stable = "stable",
29+
Nightly = "nightly"
30+
}
31+
32+
export const NIGHTLY_TAG = "nightly";
2633
export class Config {
27-
private static readonly rootSection = "rust-analyzer";
28-
private static readonly requiresReloadOpts = [
34+
readonly extensionId = "matklad.rust-analyzer";
35+
36+
private readonly rootSection = "rust-analyzer";
37+
private readonly requiresReloadOpts = [
38+
"serverPath",
2939
"cargoFeatures",
3040
"cargo-watch",
3141
"highlighting.semanticTokens",
3242
"inlayHints",
3343
]
34-
.map(opt => `${Config.rootSection}.${opt}`);
44+
.map(opt => `${this.rootSection}.${opt}`);
3545

36-
private static readonly extensionVersion: string = (() => {
37-
const packageJsonVersion = vscode
38-
.extensions
39-
.getExtension("matklad.rust-analyzer")!
40-
.packageJSON
41-
.version as string; // n.n.YYYYMMDD
46+
readonly packageJsonVersion = vscode
47+
.extensions
48+
.getExtension(this.extensionId)!
49+
.packageJSON
50+
.version as string; // n.n.YYYYMMDD[-nightly]
51+
52+
/**
53+
* Either `nightly` or `YYYY-MM-DD` (i.e. `stable` release)
54+
*/
55+
readonly extensionReleaseTag: string = (() => {
56+
if (this.packageJsonVersion.endsWith(NIGHTLY_TAG)) return NIGHTLY_TAG;
4257

4358
const realVersionRegexp = /^\d+\.\d+\.(\d{4})(\d{2})(\d{2})/;
44-
const [, yyyy, mm, dd] = packageJsonVersion.match(realVersionRegexp)!;
59+
const [, yyyy, mm, dd] = this.packageJsonVersion.match(realVersionRegexp)!;
4560

4661
return `${yyyy}-${mm}-${dd}`;
4762
})();
@@ -54,16 +69,19 @@ export class Config {
5469
}
5570

5671
private refreshConfig() {
57-
this.cfg = vscode.workspace.getConfiguration(Config.rootSection);
72+
this.cfg = vscode.workspace.getConfiguration(this.rootSection);
5873
const enableLogging = this.cfg.get("trace.extension") as boolean;
5974
log.setEnabled(enableLogging);
60-
log.debug("Using configuration:", this.cfg);
75+
log.debug(
76+
"Extension version:", this.packageJsonVersion,
77+
"using configuration:", this.cfg
78+
);
6179
}
6280

6381
private async onConfigChange(event: vscode.ConfigurationChangeEvent) {
6482
this.refreshConfig();
6583

66-
const requiresReloadOpt = Config.requiresReloadOpts.find(
84+
const requiresReloadOpt = this.requiresReloadOpts.find(
6785
opt => event.affectsConfiguration(opt)
6886
);
6987

@@ -75,7 +93,7 @@ export class Config {
7593
);
7694

7795
if (userResponse === "Reload now") {
78-
vscode.commands.executeCommand("workbench.action.reloadWindow");
96+
await vscodeReloadWindow();
7997
}
8098
}
8199

@@ -121,8 +139,14 @@ export class Config {
121139
}
122140
}
123141

142+
get installedExtensionUpdateChannel(): UpdatesChannel {
143+
return this.extensionReleaseTag === NIGHTLY_TAG
144+
? UpdatesChannel.Nightly
145+
: UpdatesChannel.Stable;
146+
}
147+
124148
get serverSource(): null | ArtifactSource {
125-
const serverPath = RA_LSP_DEBUG ?? this.cfg.get<null | string>("serverPath");
149+
const serverPath = RA_LSP_DEBUG ?? this.serverPath;
126150

127151
if (serverPath) {
128152
return {
@@ -135,23 +159,42 @@ export class Config {
135159

136160
if (!prebuiltBinaryName) return null;
137161

162+
return this.createGithubReleaseSource(
163+
prebuiltBinaryName,
164+
this.extensionReleaseTag
165+
);
166+
}
167+
168+
private createGithubReleaseSource(file: string, tag: string): ArtifactSource.GithubRelease {
138169
return {
139170
type: ArtifactSource.Type.GithubRelease,
171+
file,
172+
tag,
140173
dir: this.ctx.globalStoragePath,
141-
file: prebuiltBinaryName,
142-
storage: this.ctx.globalState,
143-
tag: Config.extensionVersion,
144-
askBeforeDownload: this.cfg.get("updates.askBeforeDownload") as boolean,
145174
repo: {
146175
name: "rust-analyzer",
147176
owner: "rust-analyzer",
148177
}
149178
};
150179
}
151180

181+
get nightlyVsixSource(): ArtifactSource.GithubRelease {
182+
return this.createGithubReleaseSource("rust-analyzer.vsix", NIGHTLY_TAG);
183+
}
184+
185+
readonly installedNightlyExtensionReleaseDate = new DateStorage(
186+
"installed-nightly-extension-release-date",
187+
this.ctx.globalState
188+
);
189+
readonly serverReleaseDate = new DateStorage("server-release-date", this.ctx.globalState);
190+
readonly serverReleaseTag = new Storage<null | string>("server-release-tag", this.ctx.globalState, null);
191+
152192
// We don't do runtime config validation here for simplicity. More on stackoverflow:
153193
// https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension
154194

195+
private get serverPath() { return this.cfg.get("serverPath") as null | string; }
196+
get updatesChannel() { return this.cfg.get("updates.channel") as UpdatesChannel; }
197+
get askBeforeDownload() { return this.cfg.get("updates.askBeforeDownload") as boolean; }
155198
get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; }
156199
get highlightingOn() { return this.cfg.get("highlightingOn") as boolean; }
157200
get rainbowHighlightingOn() { return this.cfg.get("rainbowHighlightingOn") as boolean; }
@@ -189,3 +232,37 @@ export class Config {
189232
// for internal use
190233
get withSysroot() { return this.cfg.get("withSysroot", true) as boolean; }
191234
}
235+
236+
export class Storage<T> {
237+
constructor(
238+
private readonly key: string,
239+
private readonly storage: vscode.Memento,
240+
private readonly defaultVal: T
241+
) { }
242+
243+
get(): T {
244+
const val = this.storage.get(this.key, this.defaultVal);
245+
log.debug(this.key, "==", val);
246+
return val;
247+
}
248+
async set(val: T) {
249+
log.debug(this.key, "=", val);
250+
await this.storage.update(this.key, val);
251+
}
252+
}
253+
export class DateStorage {
254+
inner: Storage<null | string>;
255+
256+
constructor(key: string, storage: vscode.Memento) {
257+
this.inner = new Storage(key, storage, null);
258+
}
259+
260+
get(): null | Date {
261+
const dateStr = this.inner.get();
262+
return dateStr ? new Date(dateStr) : null;
263+
}
264+
265+
async set(date: null | Date) {
266+
await this.inner.set(date ? date.toString() : null);
267+
}
268+
}

editors/code/src/installation/download_artifact.ts

Lines changed: 0 additions & 50 deletions
This file was deleted.

editors/code/src/installation/download_file.ts renamed to editors/code/src/installation/downloads.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import fetch from "node-fetch";
2+
import * as vscode from "vscode";
3+
import * as path from "path";
24
import * as fs from "fs";
35
import * as stream from "stream";
46
import * as util from "util";
57
import { log, assert } from "../util";
8+
import { ArtifactReleaseInfo } from "./interfaces";
69

710
const pipeline = util.promisify(stream.pipeline);
811

@@ -49,3 +52,46 @@ export async function downloadFile(
4952
// Issue at nodejs repo: https://github.com/nodejs/node/issues/31776
5053
});
5154
}
55+
56+
/**
57+
* Downloads artifact from given `downloadUrl`.
58+
* Creates `installationDir` if it is not yet created and puts the artifact under
59+
* `artifactFileName`.
60+
* Displays info about the download progress in an info message printing the name
61+
* of the artifact as `displayName`.
62+
*/
63+
export async function downloadArtifactWithProgressUi(
64+
{ downloadUrl, releaseName }: ArtifactReleaseInfo,
65+
artifactFileName: string,
66+
installationDir: string,
67+
displayName: string,
68+
) {
69+
await fs.promises.mkdir(installationDir).catch(err => assert(
70+
err?.code === "EEXIST",
71+
`Couldn't create directory "${installationDir}" to download ` +
72+
`${artifactFileName} artifact: ${err?.message}`
73+
));
74+
75+
const installationPath = path.join(installationDir, artifactFileName);
76+
77+
await vscode.window.withProgress(
78+
{
79+
location: vscode.ProgressLocation.Notification,
80+
cancellable: false, // FIXME: add support for canceling download?
81+
title: `Downloading rust-analyzer ${displayName} (${releaseName})`
82+
},
83+
async (progress, _cancellationToken) => {
84+
let lastPrecentage = 0;
85+
const filePermissions = 0o755; // (rwx, r_x, r_x)
86+
await downloadFile(downloadUrl, installationPath, filePermissions, (readBytes, totalBytes) => {
87+
const newPercentage = (readBytes / totalBytes) * 100;
88+
progress.report({
89+
message: newPercentage.toFixed(0) + "%",
90+
increment: newPercentage - lastPrecentage
91+
});
92+
93+
lastPrecentage = newPercentage;
94+
});
95+
}
96+
);
97+
}

0 commit comments

Comments
 (0)