diff --git a/vscode/README.md b/vscode/README.md index 792a652..db1bc73 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -15,10 +15,15 @@ in the following order: 2. `thrift-ls` on `PATH`, or 3. a previously downloaded copy in the extension's storage. -If none is found, a prompt offers to download the latest release binary for -your platform (`linux`/`darwin`/`windows`, `amd64`/`arm64`) and verify its +If none is found, a prompt offers to download the release binary for your +platform (`linux`/`darwin`/`windows`, `amd64`/`arm64`) and verify its SHA-256 against `checksums.txt` from the release. +A stable vsix downloads from `releases/latest` (never a prerelease). A dev +vsix from a per-commit prerelease (`0.1.0-dev.`) pins itself to that +same commit's release, so the extension and the server binary always come +from the same code. + Manual installs: ```bash @@ -36,7 +41,9 @@ and put it on `PATH` (or point `thrift-ls.path` at it). All server features are negotiated over LSP; no editor-specific code beyond the client. The `thrift-ls.downloadServer` command re-downloads the binary (use it -to update), and `thrift-ls.openReleases` opens the releases page. +to update): it stops the running server first — required on Windows, where a +running executable cannot be replaced — and restarts the client on the new +binary. `thrift-ls.openReleases` opens the releases page. ## Settings diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 1de841a..22911f1 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -15,11 +15,10 @@ import { LanguageClientOptions, ServerOptions, } from 'vscode-languageclient/node'; -import { assetName, findOnPath, parseChecksums, sha256Hex } from './platform'; +import { assetName, findOnPath, parseChecksums, releaseTag, sha256Hex } from './platform'; const REPO = 'karitham/thrift-ls'; const RELEASES_URL = `https://github.com/${REPO}/releases`; -const DOWNLOAD_URL = `${RELEASES_URL}/latest/download`; // The constructs with per-construct separator and break settings. const CONSTRUCTS = [ @@ -149,7 +148,7 @@ async function resolveBinary(context: ExtensionContext): Promise { return; } - const target = releaseTarget(); + const target = releaseTarget(context); if (!target) { window.showErrorMessage( `thrift-ls publishes no binary for ${process.platform}/${process.arch}.` @@ -197,16 +196,22 @@ async function reinstall(context: ExtensionContext): Promise { return; } + // Windows cannot replace a running executable: the cached binary is the + // live server process, so stop it first to release the lock, then restart + // the client on the new file. + await client?.stop(); + const bin = await downloadBinary(context, target, true); if (!bin) { return; } - const choice = await window.showInformationMessage( - 'thrift-ls downloaded. Reload the window to restart the server with it.', - 'Reload Window' - ); - if (choice === 'Reload Window') { - await commands.executeCommand('workbench.action.reloadWindow'); + + try { + await client?.start(); + } catch (err) { + window.showErrorMessage( + `thrift-ls updated but could not restart the server: ${errMessage(err)}. Reload the window to pick up the new binary.` + ); } } @@ -311,15 +316,22 @@ function versionOf(bin: string): string | undefined { return text || undefined; } -function releaseTarget(): ReleaseTarget | undefined { +function releaseTarget(context: ExtensionContext): ReleaseTarget | undefined { const name = assetName(process.platform, process.arch); if (!name) { return undefined; } + + // A dev vsix pins itself to its own commit's prerelease, so it pairs + // with the binaries built from the same code; stable builds use + // releases/latest (which never resolves to a prerelease). + const tag = releaseTag(context.extension.packageJSON.version); + const base = tag ? `${RELEASES_URL}/download/${tag}` : `${RELEASES_URL}/latest/download`; + return { name, - url: `${DOWNLOAD_URL}/${name}`, - checksumUrl: `${DOWNLOAD_URL}/checksums.txt`, + url: `${base}/${name}`, + checksumUrl: `${base}/checksums.txt`, }; } diff --git a/vscode/src/platform.test.ts b/vscode/src/platform.test.ts index c9b4568..2008341 100644 --- a/vscode/src/platform.test.ts +++ b/vscode/src/platform.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert'; import { test } from 'node:test'; -import { assetName, findOnPath, parseChecksums, sha256Hex } from './platform'; +import { assetName, findOnPath, parseChecksums, releaseTag, sha256Hex } from './platform'; test('assetName maps every released platform/arch', () => { assert.equal(assetName('linux', 'x64'), 'thrift-ls-linux-amd64'); @@ -77,3 +77,15 @@ test('sha256Hex matches known digests', () => { '4c43bf971bd110e5ff6122e6f10ab8730559f8459f7a13757df0960c0b9ccc56' ); }); + +test('releaseTag pins dev builds to their commit release', () => { + assert.equal(releaseTag('0.1.0-dev.110abea'), 'dev-110abea'); + assert.equal(releaseTag('0.1.0-dev.9984fd1'), 'dev-9984fd1'); +}); + +test('releaseTag is undefined for stable or unknown versions', () => { + assert.equal(releaseTag('0.1.0'), undefined); + assert.equal(releaseTag('0.2.0-beta.1'), undefined); + assert.equal(releaseTag(undefined), undefined); + assert.equal(releaseTag(''), undefined); +}); diff --git a/vscode/src/platform.ts b/vscode/src/platform.ts index 96ee12a..c5aff64 100644 --- a/vscode/src/platform.ts +++ b/vscode/src/platform.ts @@ -80,3 +80,22 @@ export function parseChecksums(text: string): ReadonlyMap { export function sha256Hex(bytes: Buffer): string { return createHash('sha256').update(bytes).digest('hex'); } + +// The vsix version of a per-commit dev build, set by the release workflow: +// "0.1.0-dev.". +const DEV_VERSION_RE = /^[\d.]+-dev\.([0-9a-f]{7})$/; + +/** + * releaseTag returns the release tag an extension version downloads from: + * "dev-" for dev builds (the matching per-commit prerelease), or + * undefined for stable builds, which use releases/latest. GitHub's latest + * never resolves to a prerelease, so without this a dev vsix would pair + * itself with the stable server binary. + */ +export function releaseTag(extensionVersion: string | undefined): string | undefined { + const m = DEV_VERSION_RE.exec(extensionVersion ?? ''); + if (!m) { + return undefined; + } + return `dev-${m[1]}`; +}