Skip to content

Commit

Permalink
add a VersionManager that installs Zig and ZLS
Browse files Browse the repository at this point in the history
See the TODOs on what needs to be added to the VersionManager

fixes #111
  • Loading branch information
Techatrix committed Sep 24, 2024
1 parent 41f9443 commit fd28431
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 146 deletions.
159 changes: 159 additions & 0 deletions src/versionManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import vscode from "vscode";

import childProcess from "child_process";
import fs from "fs";
import util from "util";
import which from "which";

import axios from "axios";
import semver from "semver";

import { getZigArchName, getZigOSName } from "./zigUtil";

const execFile = util.promisify(childProcess.execFile);
const chmod = util.promisify(fs.chmod);

/**
* A version manager for Zig and ZLS.
*
* Expects a provider that follows the following scheme:
* `${PROVIDER_URL}/${NAME}-${OS}-${ARCH}-${VERSION}.${FILE_EXTENSION}`
*
* Example:
* - `https://ziglang.org/download/0.13.0/zig-windows-x86_64-0.13.0.zip`
* - `https://builds.zigtools.org/zls-linux-x86_64-0.13.0.tar.xz`
*
* TODO automatically remove unnecessary versions
* Maybe limit the number of versions and track how long the version was not used.
*
* TODO verify installation with minisig
*/
export class VersionManager {
context: vscode.ExtensionContext;
kind: "zig" | "zls";

/** The maxmimum number of installation that can be store until they will be removed */
static maxInstallCount = 5;

constructor(context: vscode.ExtensionContext, kind: "zig" | "zls") {
this.context = context;
this.kind = kind;
}

/** Returns the path to the executable */
public async install(version: semver.SemVer): Promise<string> {
let title: string;
let artifactBaseUrl: vscode.Uri;
let extraTarArgs: string[];
switch (this.kind) {
case "zig":
title = "Zig";
if (version.prerelease.length === 0) {
artifactBaseUrl = vscode.Uri.joinPath(
vscode.Uri.parse("https://ziglang.org/download"),
version.raw,
);
} else {
artifactBaseUrl = vscode.Uri.parse("https://ziglang.org/builds");
}
extraTarArgs = ["--strip-components=1"];
break;
case "zls":
title = "ZLS";
artifactBaseUrl = vscode.Uri.parse("https://builds.zigtools.org");
break;
}

const isWindows = process.platform === "win32";
const fileExtension = process.platform === "win32" ? "zip" : "tar.xz";
const exeName = this.kind + (isWindows ? ".exe" : "");
const subDirName = `${getZigOSName()}-${getZigArchName()}-${version.raw}`;
const fileName = `${this.kind}-${subDirName}.${fileExtension}`;

const artifactUrl = vscode.Uri.joinPath(artifactBaseUrl, fileName);

const installDir = vscode.Uri.joinPath(this.context.globalStorageUri, this.kind, subDirName);
const exeUri = vscode.Uri.joinPath(installDir, exeName);
const exePath = exeUri.fsPath;
const tarballUri = vscode.Uri.joinPath(installDir, fileName);

try {
await vscode.workspace.fs.stat(exeUri);
return exePath;
} catch (e) {
if (e instanceof vscode.FileSystemError) {
if (e.code !== "FileNotFound") {
throw e;
}
// go ahead an install
} else {
throw e;
}
}

const tarPath = await which("tar", { nothrow: true });
if (!tarPath) {
throw new Error(`Downloaded ${title} tarball can't be extracted because 'tar' could not be found`);
}

return await vscode.window.withProgress<string>(
{
title: `Installing ${title}`,
location: vscode.ProgressLocation.Notification,
},
async (progress, cancelToken) => {
const abortController = new AbortController();
cancelToken.onCancellationRequested(() => {
abortController.abort();
});

const response = await axios.get<Buffer>(artifactUrl.toString(), {
responseType: "arraybuffer",
signal: abortController.signal,
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,
});
}
},
});

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), {
signal: abortController.signal,
timeout: 60000, // 60 seconds
});
} catch (err) {
try {
await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false });
} catch {}
if (err instanceof Error) {
throw new Error(`Failed to extract ${title} tarball: ${err.message}`);
} else {
throw err;
}
} finally {
try {
await vscode.workspace.fs.delete(tarballUri, { useTrash: false });
} catch {}
}

await chmod(exePath, 0o755);

return exePath;
},
);
}
}
43 changes: 23 additions & 20 deletions src/zigSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import axios from "axios";
import semver from "semver";
import vscode from "vscode";

import { downloadAndExtractArtifact, getHostZigName, getVersion, getZigPath, shouldCheckUpdate } from "./zigUtil";
import { getHostZigName, getVersion, getZigPath, shouldCheckUpdate } from "./zigUtil";
import { VersionManager } from "./versionManager";
import { restartClient } from "./zls";

let versionManager: VersionManager;

const DOWNLOAD_INDEX = "https://ziglang.org/download/index.json";

function getNightlySemVer(url: string): string {
Expand All @@ -22,27 +25,20 @@ interface ZigVersion {
url: string;
sha: string;
notes?: string;
version: semver.SemVer;
}

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);
export async function installZig(context: vscode.ExtensionContext, version: semver.SemVer) {
const zigPath = await versionManager.install(version);

void vscode.window.showInformationMessage(
`Zig has been installed successfully. Relaunch your integrated terminal to make it available.`,
);
const configuration = vscode.workspace.getConfiguration("zig");
await configuration.update("path", zigPath, true);

void restartClient(context);
}
void vscode.window.showInformationMessage(
`Zig has been installed successfully. Relaunch your integrated terminal to make it available.`,
);

void restartClient(context);
}

async function getVersions(): Promise<ZigVersion[]> {
Expand All @@ -51,13 +47,18 @@ async function getVersions(): Promise<ZigVersion[]> {
const result: ZigVersion[] = [];
for (let key in indexJson) {
const value = indexJson[key];
let version: semver.SemVer;
if (key === "master") {
key = "nightly";
version = new semver.SemVer((value as unknown as { version: string }).version);
} else {
version = new semver.SemVer(key);
}
const release = value[hostName];
if (release) {
result.push({
name: key,
version: version,
url: release.tarball,
sha: release.shasum,
notes: (value as { notes?: string }).notes,
Expand Down Expand Up @@ -90,7 +91,7 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) {
if (selection === undefined) return;
for (const option of available) {
if (option.name === selection.label) {
await installZig(context, option);
await installZig(context, option.version);
return;
}
}
Expand All @@ -117,7 +118,7 @@ async function checkUpdate(context: vscode.ExtensionContext) {
);
switch (response) {
case "Install":
await installZig(context, update);
await installZig(context, update.version);
break;
case "Ignore":
case undefined:
Expand Down Expand Up @@ -190,6 +191,8 @@ export async function setupZig(context: vscode.ExtensionContext) {
}
}

versionManager = new VersionManager(context, "zig");

context.environmentVariableCollection.description = "Add Zig to PATH";
updateZigEnvironmentVariableCollection(context);

Expand Down
Loading

0 comments on commit fd28431

Please sign in to comment.