From 9cab21a589a209ef389f17c431149e98b6dc121f Mon Sep 17 00:00:00 2001 From: Techatrix Date: Sat, 31 Aug 2024 14:43:01 +0200 Subject: [PATCH] update ZLS install tool ZLS builds are now offered by https://github.com/zigtools/release-worker --- package-lock.json | 15 --- package.json | 1 - src/zigSetup.ts | 105 +++++---------------- src/zigUtil.ts | 94 ++++++++++++++++++- src/zls.ts | 232 ++++++++++++++++++++-------------------------- 5 files changed, 219 insertions(+), 228 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7445978..567b9e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "axios": "^1.7.4", "camelcase": "^7.0.1", "lodash-es": "^4.17.21", - "mkdirp": "^2.1.3", "semver": "^7.5.2", "vscode-languageclient": "8.0.2-next.5", "which": "^3.0.0" @@ -2213,20 +2212,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mkdirp": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", - "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", diff --git a/package.json b/package.json index d63a467..deb2540 100644 --- a/package.json +++ b/package.json @@ -385,7 +385,6 @@ "axios": "^1.7.4", "camelcase": "^7.0.1", "lodash-es": "^4.17.21", - "mkdirp": "^2.1.3", "semver": "^7.5.2", "vscode-languageclient": "8.0.2-next.5", "which": "^3.0.0" diff --git a/src/zigSetup.ts b/src/zigSetup.ts index a8e37b2..f255c4f 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -1,16 +1,11 @@ -import childProcess from "child_process"; -import crypto from "crypto"; -import fs from "fs"; import path from "path"; import axios from "axios"; -import mkdirp from "mkdirp"; import semver from "semver"; import vscode from "vscode"; -import which from "which"; -import { getHostZigName, getVersion, getZigPath, isWindows, shouldCheckUpdate } from "./zigUtil"; -import { install as installZLS } from "./zls"; +import { downloadAndExtractArtifact, getHostZigName, getVersion, getZigPath, shouldCheckUpdate } from "./zigUtil"; +import { installZLS } from "./zls"; const DOWNLOAD_INDEX = "https://ziglang.org/download/index.json"; @@ -29,6 +24,25 @@ interface ZigVersion { notes?: string; } +export async function installZig(context: vscode.ExtensionContext, version: ZigVersion) { + const zigPath = await downloadAndExtractArtifact( + "Zig", + "zig", + vscode.Uri.joinPath(context.globalStorageUri, "zig_install"), + version.url, + version.sha, + ["--strip-components=1"], + ); + if (zigPath !== null) { + const configuration = vscode.workspace.getConfiguration("zig"); + await configuration.update("path", zigPath, true); + + void vscode.window.showInformationMessage( + `Zig has been installed successfully. Relaunch your integrated terminal to make it available.`, + ); + } +} + async function getVersions(): Promise { const hostName = getHostZigName(); const indexJson = (await axios.get(DOWNLOAD_INDEX, {})).data; @@ -56,79 +70,6 @@ async function getVersions(): Promise { return result; } -async function install(context: vscode.ExtensionContext, version: ZigVersion) { - await vscode.window.withProgress( - { - title: "Installing Zig", - location: vscode.ProgressLocation.Notification, - }, - async (progress) => { - progress.report({ message: "downloading Zig tarball..." }); - const response = await axios.get(version.url, { - responseType: "arraybuffer", - onDownloadProgress: (progressEvent) => { - if (progressEvent.total) { - const increment = (progressEvent.bytes / progressEvent.total) * 100; - progress.report({ - message: progressEvent.progress - ? `downloading tarball ${(progressEvent.progress * 100).toFixed()}%` - : "downloading tarball...", - increment: increment, - }); - } - }, - }); - const tarHash = crypto.createHash("sha256").update(response.data).digest("hex"); - if (tarHash !== version.sha) { - throw Error(`hash of downloaded tarball ${tarHash} does not match expected hash ${version.sha}`); - } - - const installDir = vscode.Uri.joinPath(context.globalStorageUri, "zig_install"); - if (fs.existsSync(installDir.fsPath)) { - fs.rmSync(installDir.fsPath, { recursive: true, force: true }); - } - mkdirp.sync(installDir.fsPath); - - const tarPath = which.sync("tar", { nothrow: true }); - if (!tarPath) { - void vscode.window.showErrorMessage( - "Downloaded Zig tarball can't be extracted because 'tar' could not be found", - ); - return; - } - - progress.report({ message: "Extracting..." }); - try { - childProcess.execFileSync(tarPath, ["-xJf", "-", "-C", installDir.fsPath, "--strip-components=1"], { - encoding: "buffer", - input: response.data, - maxBuffer: 100 * 1024 * 1024, // 100MB - timeout: 60000, // 60 seconds - }); - } catch (err) { - if (err instanceof Error) { - void vscode.window.showErrorMessage(`Failed to extract Zig tarball: ${err.message}`); - } else { - throw err; - } - return; - } - - progress.report({ message: "Installing..." }); - const exeName = `zig${isWindows ? ".exe" : ""}`; - const zigPath = vscode.Uri.joinPath(installDir, exeName).fsPath; - fs.chmodSync(zigPath, 0o755); - - const configuration = vscode.workspace.getConfiguration("zig"); - await configuration.update("path", zigPath, true); - - void vscode.window.showInformationMessage( - `Zig has been installed successfully. Relaunch your integrated terminal to make it available.`, - ); - }, - ); -} - async function selectVersionAndInstall(context: vscode.ExtensionContext) { try { const available = await getVersions(); @@ -147,7 +88,7 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) { if (selection === undefined) return; for (const option of available) { if (option.name === selection.label) { - await install(context, option); + await installZig(context, option); return; } } @@ -174,7 +115,7 @@ async function checkUpdate(context: vscode.ExtensionContext) { ); switch (response) { case "Install": - await install(context, update); + await installZig(context, update); break; case "Ignore": case undefined: diff --git a/src/zigUtil.ts b/src/zigUtil.ts index 2c1e85d..ad5916c 100644 --- a/src/zigUtil.ts +++ b/src/zigUtil.ts @@ -1,14 +1,19 @@ import vscode from "vscode"; import childProcess from "child_process"; +import crypto from "crypto"; import fs from "fs"; import os from "os"; import path from "path"; +import { promisify } from "util"; +import assert from "assert"; +import axios from "axios"; import semver from "semver"; import which from "which"; -export const isWindows = process.platform === "win32"; +const execFile = promisify(childProcess.execFile); +const chmod = promisify(fs.chmod); // Replace any references to predefined variables in config string. // https://code.visualstudio.com/docs/editor/variables-reference#_predefined-variables @@ -125,3 +130,90 @@ export function getVersion(filePath: string, arg: string): semver.SemVer | null return null; } } + +export async function downloadAndExtractArtifact( + /** e.g. `Zig` or `ZLS` */ + title: string, + /** e.g. `zig` or `zls` */ + executableName: string, + /** e.g. inside `context.globalStorageUri` */ + installDir: vscode.Uri, + artifactUrl: string, + /** The expected sha256 hash (in hex) of the artifact/tarball. */ + sha256: string, + /** Extract arguments that should be passed to `tar`. e.g. `--strip-components=1` */ + extraTarArgs: string[], +): Promise { + assert.strictEqual(sha256.length, 64); + + return await vscode.window.withProgress( + { + title: `Installing ${title}`, + location: vscode.ProgressLocation.Notification, + }, + async (progress) => { + progress.report({ message: `downloading ${title} tarball...` }); + const response = await axios.get(artifactUrl, { + responseType: "arraybuffer", + onDownloadProgress: (progressEvent) => { + if (progressEvent.total) { + const increment = (progressEvent.bytes / progressEvent.total) * 100; + progress.report({ + message: progressEvent.progress + ? `downloading tarball ${(progressEvent.progress * 100).toFixed()}%` + : "downloading tarball...", + increment: increment, + }); + } + }, + }); + const tarHash = crypto.createHash("sha256").update(response.data).digest("hex"); + if (tarHash !== sha256) { + throw Error(`hash of downloaded tarball ${tarHash} does not match expected hash ${sha256}`); + } + + const tarPath = await which("tar", { nothrow: true }); + if (!tarPath) { + void vscode.window.showErrorMessage( + `Downloaded ${title} tarball can't be extracted because 'tar' could not be found`, + ); + return null; + } + + const tarballUri = vscode.Uri.joinPath(installDir, path.basename(artifactUrl)); + + try { + await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); + } catch {} + await vscode.workspace.fs.createDirectory(installDir); + await vscode.workspace.fs.writeFile(tarballUri, response.data); + + progress.report({ message: "Extracting..." }); + try { + await execFile(tarPath, ["-xf", tarballUri.fsPath, "-C", installDir.fsPath].concat(extraTarArgs), { + timeout: 60000, // 60 seconds + }); + } catch (err) { + if (err instanceof Error) { + void vscode.window.showErrorMessage(`Failed to extract ${title} tarball: ${err.message}`); + } else { + throw err; + } + return null; + } finally { + try { + await vscode.workspace.fs.delete(tarballUri, { useTrash: false }); + } catch {} + } + + progress.report({ message: "Installing..." }); + + const isWindows = process.platform === "win32"; + const exeName = `${executableName}${isWindows ? ".exe" : ""}`; + const exePath = vscode.Uri.joinPath(installDir, exeName).fsPath; + await chmod(exePath, 0o755); + + return exePath; + }, + ); +} diff --git a/src/zls.ts b/src/zls.ts index 54b06b7..214f136 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -1,7 +1,5 @@ import vscode from "vscode"; -import fs from "fs"; - import { CancellationToken, ConfigurationParams, @@ -15,16 +13,15 @@ import { } from "vscode-languageclient/node"; import axios from "axios"; import camelCase from "camelcase"; -import mkdirp from "mkdirp"; import semver from "semver"; import { + downloadAndExtractArtifact, getExePath, getHostZigName, getVersion, getZigPath, handleConfigOption, - isWindows, shouldCheckUpdate, } from "./zigUtil"; @@ -88,7 +85,7 @@ export async function stopClient() { client = null; } -// returns the file system path to the zls executable +/** returns the file system path to the zls executable */ export function getZLSPath(): string { const configuration = vscode.workspace.getConfiguration("zig.zls"); const zlsPath = configuration.get("path"); @@ -182,31 +179,80 @@ async function configurationMiddleware( return result as unknown[]; } -const downloadsRoot = "https://zigtools-releases.nyc3.digitaloceanspaces.com/zls"; - -interface Version { +/** + * Similar to https://ziglang.org/download/index.json + */ +interface SelectVersionResponse { + /** The ZLS version */ + version: string; + /** `YYYY-MM-DD` */ date: string; - builtWithZigVersion: string; - zlsVersion: string; - zlsMinimumBuildVersion: string; - commit: string; - targets: string[]; + [artifact: string]: ArtifactEntry | string | undefined; +} + +export interface SelectVersionFailureResponse { + /** + * The `code` **may** be one of `SelectVersionFailureCode`. Be aware that new + * codes can be added over time. + */ + code: number; + /** A simplified explanation of why no ZLS build could be selected */ + message: string; } -interface VersionIndex { - latest: string; - latestTagged: string; - releases: Record; - versions: Record; +interface ArtifactEntry { + /** A download URL */ + tarball: string; + /** A SHA256 hash of the tarball */ + shasum: string; + /** Size of the tarball in bytes */ + size: string; } -async function getVersionIndex(): Promise { - const index = (await axios.get(`${downloadsRoot}/index.json`)).data; - if (!index.versions[index.latest]) { - void vscode.window.showErrorMessage("Invalid ZLS version index; please contact a ZLS maintainer."); - throw new Error("Invalid ZLS version"); +async function fetchVersion( + zigVersion: semver.SemVer, +): Promise<{ version: semver.SemVer; artifact: ArtifactEntry } | null> { + let response: SelectVersionResponse | SelectVersionFailureResponse; + try { + response = ( + await axios.get( + "https://releases.zigtools.org/v1/zls/select-version", + { + params: { + // eslint-disable-next-line @typescript-eslint/naming-convention + zig_version: zigVersion.raw, + compatibility: "only-runtime", + }, + }, + ) + ).data; + } catch (err) { + if (err instanceof Error) { + void vscode.window.showErrorMessage(`Failed to query ZLS version: ${err.message}`); + } else { + throw err; + } + return null; + } + + if ("message" in response) { + void vscode.window.showErrorMessage(`Unable to fetch ZLS: ${response.message as string}`); + return null; } - return index; + + const hostName = getHostZigName(); + + if (!(hostName in response)) { + void vscode.window.showErrorMessage( + `A prebuilt ZLS ${response.version} binary is not available for your system. You can build it yourself with https://github.com/zigtools/zls#from-source`, + ); + return null; + } + + return { + version: new semver.SemVer(response.version), + artifact: response[hostName] as ArtifactEntry, + }; } // checks whether there is newer version on master @@ -216,21 +262,21 @@ async function checkUpdate(context: vscode.ExtensionContext) { const zlsBinPath = vscode.Uri.joinPath(context.globalStorageUri, "zls_install", "zls").fsPath; if (!zlsPath?.startsWith(zlsBinPath)) return; - // get current version - const version = getVersion(zlsPath, "--version"); - if (!version) return; + const zigVersion = getVersion(getZigPath(), "version"); + if (!zigVersion) return; + + const currentVersion = getVersion(zlsPath, "--version"); + if (!currentVersion) return; - const index = await getVersionIndex(); - const latestVersionString = version.build.length === 0 ? index.latestTagged : index.latest; - // having a build number implies nightly version - const latestVersion = new semver.SemVer(latestVersionString); + const result = await fetchVersion(zigVersion); + if (!result) return; - if (semver.gte(version, latestVersion)) return; + if (semver.gte(currentVersion, result.version)) return; const response = await vscode.window.showInformationMessage("New version of ZLS available", "Install", "Ignore"); switch (response) { case "Install": - await installVersion(context, latestVersion); + await installZLSVersion(context, result.artifact); break; case "Ignore": case undefined: @@ -238,122 +284,50 @@ async function checkUpdate(context: vscode.ExtensionContext) { } } -export async function install(context: vscode.ExtensionContext, ask: boolean) { - const path = getZigPath(); - - const zlsConfiguration = vscode.workspace.getConfiguration("zig.zls", null); - const zigVersion = getVersion(path, "version"); +export async function installZLS(context: vscode.ExtensionContext, ask: boolean) { + const zigVersion = getVersion(getZigPath(), "version"); if (!zigVersion) { + const zlsConfiguration = vscode.workspace.getConfiguration("zig.zls", null); await zlsConfiguration.update("path", undefined, true); - return; - } - // Zig 0.9.0 was the first version to have a tagged zls release - if (semver.lt(zigVersion, "0.9.0")) { - if (zlsConfiguration.get("path")) { - void vscode.window.showErrorMessage(`ZLS is not available for Zig version ${zigVersion.version}`); - } - await zlsConfiguration.update("path", undefined, true); - return; + return undefined; } + const result = await fetchVersion(zigVersion); + if (!result) return; + if (ask) { - const result = await vscode.window.showInformationMessage( - `Do you want to install ZLS (the Zig Language Server) for Zig version ${zigVersion.version}`, + const selected = await vscode.window.showInformationMessage( + `Do you want to install ZLS (the Zig Language Server) for Zig version ${result.version.toString()}`, "Install", "Ignore", ); - switch (result) { + switch (selected) { case "Install": break; case "Ignore": + const zlsConfiguration = vscode.workspace.getConfiguration("zig.zls", null); await zlsConfiguration.update("path", undefined, true); return; case undefined: return; } } - let zlsVersion: semver.SemVer; - if (zigVersion.build.length !== 0) { - // Nightly, install latest ZLS - zlsVersion = new semver.SemVer((await getVersionIndex()).latest); - } else { - // ZLS does not make releases for patches - zlsVersion = zigVersion; - zlsVersion.patch = 0; - } - try { - await installVersion(context, zlsVersion); - } catch (err) { - if (err instanceof Error) { - void vscode.window.showErrorMessage( - `Unable to install ZLS ${zlsVersion.version} for Zig version ${zigVersion.version}: ${err.message}`, - ); - } else { - throw err; - } - } + await installZLSVersion(context, result.artifact); } -async function installVersion(context: vscode.ExtensionContext, version: semver.SemVer) { - const hostName = getHostZigName(); - - await vscode.window.withProgress( - { - title: "Installing ZLS", - location: vscode.ProgressLocation.Notification, - }, - async (progress) => { - progress.report({ message: "downloading executable..." }); - let exe: Buffer; - try { - const response = await axios.get( - `${downloadsRoot}/${version.raw}/${hostName}/zls${isWindows ? ".exe" : ""}`, - { - responseType: "arraybuffer", - onDownloadProgress: (progressEvent) => { - if (progressEvent.total) { - const increment = (progressEvent.bytes / progressEvent.total) * 100; - progress.report({ - message: progressEvent.progress - ? `downloading executable ${(progressEvent.progress * 100).toFixed()}%` - : "downloading executable...", - increment: increment, - }); - } - }, - }, - ); - exe = response.data; - } catch (err) { - // Missing prebuilt binary is reported as AccessDenied - if (axios.isAxiosError(err) && err.response?.status === 403) { - void vscode.window.showErrorMessage( - `A prebuilt ZLS ${version.version} binary is not available for your system. You can build it yourself with https://github.com/zigtools/zls#from-source`, - ); - return; - } - throw err; - } - - await stopClient(); - - const installDir = vscode.Uri.joinPath(context.globalStorageUri, "zls_install"); - if (fs.existsSync(installDir.fsPath)) { - fs.rmSync(installDir.fsPath, { recursive: true, force: true }); - } - mkdirp.sync(installDir.fsPath); - - const binName = `zls${isWindows ? ".exe" : ""}`; - const zlsBinPath = vscode.Uri.joinPath(installDir, binName).fsPath; - - fs.writeFileSync(zlsBinPath, exe, "binary"); - fs.chmodSync(zlsBinPath, 0o755); - - const config = vscode.workspace.getConfiguration("zig.zls"); - await config.update("path", zlsBinPath, true); - }, +async function installZLSVersion(context: vscode.ExtensionContext, artifact: ArtifactEntry) { + const zlsPath = await downloadAndExtractArtifact( + "ZLS", + "zls", + vscode.Uri.joinPath(context.globalStorageUri, "zls_install"), + artifact.tarball, + artifact.shasum, + [], ); + + const zlsConfiguration = vscode.workspace.getConfiguration("zig.zls", null); + await zlsConfiguration.update("path", zlsPath ?? undefined, true); } function checkInstalled(): boolean { @@ -383,7 +357,7 @@ export async function activate(context: vscode.ExtensionContext) { } await stopClient(); - await install(context, true); + await installZLS(context, false); }), vscode.commands.registerCommand("zig.zls.stop", async () => { if (!checkInstalled()) return;