Skip to content

Commit

Permalink
remove the initial setup with automatic Zig version management
Browse files Browse the repository at this point in the history
This commit replaces the initial setup with the following mechanism:
1. Check if the "Install Zig" has been previously executed in the active workspace. If so, install that version.
2. If the workspace contains a `.zigversion`, install the given Zig version.
3. If the workspace contains a `build.zig.zon` with a `minimum_zig_version`, install the next available Zig version.
4. Otherwise fallback to the latest tagged release of Zig.

Some parts of this are not fully implemented.

fixes #111
  • Loading branch information
Techatrix committed Nov 16, 2024
1 parent a3ae05f commit 297638a
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 107 deletions.
5 changes: 0 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,6 @@
"type": "object",
"title": "Zig",
"properties": {
"zig.initialSetupDone": {
"type": "boolean",
"default": false,
"description": "Has the initial setup been done yet?"
},
"zig.buildOnSave": {
"type": "boolean",
"default": false,
Expand Down
193 changes: 124 additions & 69 deletions src/zigSetup.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
import vscode from "vscode";

import path from "path";

import semver from "semver";
import vscode from "vscode";

import { ZigVersion, getHostZigName, getVersion, getVersionIndex } from "./zigUtil";
import { ZigVersion, getHostZigName, getVersionIndex } from "./zigUtil";
import { VersionManager } from "./versionManager";
import { ZigProvider } from "./zigProvider";
import { restartClient } from "./zls";

let versionManager: VersionManager;
export let zigProvider: ZigProvider;

export async function installZig(context: vscode.ExtensionContext, version: semver.SemVer) {
const zigPath = await versionManager.install(version);

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.`,
/** Removes the `zig.path` config option. */
async function installZig(context: vscode.ExtensionContext) {
const wantedZig = await getWantedZigVersion(
context,
Object.values(WantedZigVersionSource) as WantedZigVersionSource[],
);
if (!wantedZig) {
await vscode.workspace.getConfiguration("zig").update("path", undefined, true);
zigProvider.set(null);
return;
}

void restartClient(context);
try {
const exePath = await versionManager.install(wantedZig.version);
await vscode.workspace.getConfiguration("zig").update("path", undefined, true);
zigProvider.set({ exe: exePath, version: wantedZig.version });
} catch (err) {
zigProvider.set(null);
if (err instanceof Error) {
void vscode.window.showErrorMessage(
`Failed to install Zig ${wantedZig.version.toString()}: ${err.message}`,
);
} else {
void vscode.window.showErrorMessage(`Failed to install Zig ${wantedZig.version.toString()}!`);
}
return;
}
}

async function getVersions(): Promise<ZigVersion[]> {
Expand Down Expand Up @@ -74,7 +90,12 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) {
if (selection === undefined) return;
for (const option of available) {
if (option.name === selection.label) {
await installZig(context, option.version);
await context.workspaceState.update("zig-version", option.version.raw);
await installZig(context);

void vscode.window.showInformationMessage(
`Zig ${option.version.toString()} has been installed successfully. Relaunch your integrated terminal to make it available.`,
);
return;
}
}
Expand All @@ -87,6 +108,89 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) {
}
}

/** The order of these enums defines the default order in which these sources are executed. */
enum WantedZigVersionSource {
workspaceState = "workspace-state",
/** `.zigversion` */
workspaceZigVersionFile = ".zigversion",
/** The `minimum_zig_version` in `build.zig.zon` */
workspaceBuildZigZon = "build.zig.zon",
latestTagged = "latest-tagged",
}

/** Try to resolve the (workspace-specific) Zig version. */
async function getWantedZigVersion(
context: vscode.ExtensionContext,
/** List of "sources" that should are applied in the given order to resolve the wanted Zig version */
sources: WantedZigVersionSource[],
): Promise<{
version: semver.SemVer;
source: WantedZigVersionSource;
} | null> {
let workspace: vscode.WorkspaceFolder | null = null;
// Supporting multiple workspaces is significantly more complex so we just look for the first workspace.
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
workspace = vscode.workspace.workspaceFolders[0];
}

for (const source of sources) {
let result: semver.SemVer | null = null;

try {
switch (source) {
case WantedZigVersionSource.workspaceState:
// `context.workspaceState` appears to behave like `context.globalState` when outside of a workspace
// There is currently no way to remove the specified zig version.
const wantedZigVersion = context.workspaceState.get<string>("zig-version");
result = wantedZigVersion ? new semver.SemVer(wantedZigVersion) : null;
break;
case WantedZigVersionSource.workspaceZigVersionFile:
if (workspace) {
const zigVersionString = await vscode.workspace.fs.readFile(
vscode.Uri.joinPath(workspace.uri, ".zigversion"),
);
result = semver.parse(zigVersionString.toString().trim());
}
break;
case WantedZigVersionSource.workspaceBuildZigZon:
if (workspace) {
const manifest = await vscode.workspace.fs.readFile(
vscode.Uri.joinPath(workspace.uri, "build.zig.zon"),
);
// Not perfect, but good enough
const matches = /\n\s*\.minimum_zig_version\s=\s\"(.*)\"/.exec(manifest.toString());
if (matches) {
result = semver.parse(matches[1]);
}
}
break;
case WantedZigVersionSource.latestTagged:
const cacheKey = "zig-latest-tagged";
try {
const zigVersion = await getVersions();
const latestTagged = zigVersion.find((item) => item.version.prerelease.length === 0);
result = latestTagged?.version ?? null;
await context.globalState.update(cacheKey, latestTagged?.version.raw);
} catch {
const latestTagged = context.globalState.get<string | null>(cacheKey, null);
if (latestTagged) {
result = new semver.SemVer(latestTagged);
}
}
break;
}
} catch {}

if (!result) continue;

return {
version: result,
source: source,
};
}
return null;
}

function updateZigEnvironmentVariableCollection(context: vscode.ExtensionContext, zigExePath: string | null) {
if (zigExePath) {
const envValue = path.delimiter + path.dirname(zigExePath);
Expand All @@ -101,21 +205,14 @@ function updateZigEnvironmentVariableCollection(context: vscode.ExtensionContext

export async function setupZig(context: vscode.ExtensionContext) {
{
// convert an empty string for `zig.path` and `zig.zls.path` to `zig` and `zls` respectively.
// This check can be removed once enough time has passed so that most users switched to the new value

const zigConfig = vscode.workspace.getConfiguration("zig");
const initialSetupDone = zigConfig.get<boolean>("initialSetupDone", false);
const zigPath = zigConfig.get<string>("path");
if (zigPath === "" && initialSetupDone) {
await zigConfig.update("path", "zig", true);
}

const zlsConfig = vscode.workspace.getConfiguration("zig.zls");
// remove a `zig.path` that points to the global storage.
const zlsConfig = vscode.workspace.getConfiguration("zig");
if (zlsConfig.get<boolean | null>("enabled", null) === null) {
const zlsPath = zlsConfig.get<string>("path");
if (zlsPath === "" && initialSetupDone) {
await zlsConfig.update("path", "zls", true);
const zlsPath = zlsConfig.get<string>("path", "");
if (zlsPath.startsWith(context.globalStorageUri.fsPath)) {
await zlsConfig.update("path", undefined, true);
}
}
}
Expand All @@ -138,49 +235,7 @@ export async function setupZig(context: vscode.ExtensionContext) {
}),
);

const configuration = vscode.workspace.getConfiguration("zig");
if (!configuration.get<boolean>("initialSetupDone")) {
await configuration.update("initialSetupDone", await initialSetup(context), true);
if (!vscode.workspace.getConfiguration("zig").get<string>("path")) {
await installZig(context);
}
}

async function initialSetup(context: vscode.ExtensionContext): Promise<boolean> {
const zigConfig = vscode.workspace.getConfiguration("zig");
if (!!zigConfig.get<string>("path")) return true;

const zigResponse = await vscode.window.showInformationMessage(
"Zig path hasn't been set, do you want to specify the path or install Zig?",
{ modal: true },
"Install",
"Specify path",
"Use Zig in PATH",
);
switch (zigResponse) {
case "Install":
await selectVersionAndInstall(context);
const zigPath = vscode.workspace.getConfiguration("zig").get<string>("path");
if (!zigPath) return false;
break;
case "Specify path":
const uris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
title: "Select Zig executable",
});
if (!uris) return false;

const version = getVersion(uris[0].path, "version");
if (!version) return false;

await zigConfig.update("path", uris[0].path, true);
break;
case "Use Zig in PATH":
await zigConfig.update("path", "zig", true);
break;
case undefined:
return false;
}

return true;
}
83 changes: 50 additions & 33 deletions src/zls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import axios from "axios";
import camelCase from "camelcase";
import semver from "semver";

import { getHostZigName, getVersion, handleConfigOption } from "./zigUtil";
import { getHostZigName, getVersion, handleConfigOption, resolveExePathAndVersion } from "./zigUtil";
import { VersionManager } from "./versionManager";
import { zigProvider } from "./zigSetup";

Expand All @@ -29,12 +29,6 @@ let outputChannel: vscode.OutputChannel;
export let client: LanguageClient | null = null;

export async function restartClient(context: vscode.ExtensionContext): Promise<void> {
const configuration = vscode.workspace.getConfiguration("zig.zls");
if (!configuration.get<string>("path") && !configuration.get<boolean>("enabled", false)) {
await stopClient();
return;
}

const result = await getZLSPath(context);
if (!result) return;

Expand Down Expand Up @@ -92,39 +86,58 @@ async function getZLSPath(context: vscode.ExtensionContext): Promise<{ exe: stri
let zlsExePath = configuration.get<string>("path");
let zlsVersion: semver.SemVer | null = null;

if (!zlsExePath) {
if (!configuration.get<boolean>("enabled", false)) return null;
if (!!zlsExePath) {
// This will fail on older ZLS version that do not support `zls --version`.
// It should be more likely that the given executable is invalid than someone using ZLS 0.9.0 or older.
const result = resolveExePathAndVersion(zlsExePath, "zls", "zig.zls.path", "--version");
if ("message" in result) {
void vscode.window.showErrorMessage(result.message);
return null;
}
return result;
}

if (!configuration.get<boolean>("enabled", false)) return null;

if (!zigProvider.zigVersion) return null;
if (!zigProvider.zigVersion) return null;

const result = await fetchVersion(context, zigProvider.zigVersion, true);
if (!result) return null;
const result = await fetchVersion(context, zigProvider.zigVersion, true);
if (!result) return null;

try {
zlsExePath = await versionManager.install(result.version);
zlsVersion = result.version;
} catch {
try {
zlsExePath = await versionManager.install(result.version);
zlsVersion = result.version;
} catch (err) {
if (err instanceof Error) {
void vscode.window.showErrorMessage(`Failed to install ZLS ${result.version.toString()}: ${err.message}`);
} else {
void vscode.window.showErrorMessage(`Failed to install ZLS ${result.version.toString()}!`);
return null;
}
}

const checkedZLSVersion = getVersion(zlsExePath, "--version");
if (!checkedZLSVersion) {
void vscode.window.showErrorMessage(`Unable to check ZLS version. '${zlsExePath} --version' failed!`);
return null;
}
if (zlsVersion && checkedZLSVersion.compare(zlsVersion) !== 0) {
// The Matrix is broken!
void vscode.window.showErrorMessage(
`Encountered unexpected ZLS version. Expected '${zlsVersion.toString()}' from '${zlsExePath} --version' but got '${checkedZLSVersion.toString()}'!`,
);
return null;

/** `--version` has been added in https://github.com/zigtools/zls/pull/583 */
const zlsVersionArgAdded = new semver.SemVer("0.10.0-dev.150+cb5eeb0b4");

if (semver.gte(zlsVersion, zlsVersionArgAdded)) {
// Verify the installation by quering the version
const checkedZLSVersion = getVersion(zlsExePath, "--version");
if (!checkedZLSVersion) {
void vscode.window.showErrorMessage(`Unable to check ZLS version. '${zlsExePath} --version' failed!`);
return null;
}

if (checkedZLSVersion.compare(zlsVersion) !== 0) {
// The Matrix is broken!
void vscode.window.showWarningMessage(
`Encountered unexpected ZLS version. Expected '${zlsVersion.toString()}' from '${zlsExePath} --version' but got '${checkedZLSVersion.toString()}'!`,
);
}
}

return {
exe: zlsExePath,
version: checkedZLSVersion,
version: zlsVersion,
};
}

Expand Down Expand Up @@ -367,6 +380,14 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("zig.zls.openOutput", () => {
outputChannel.show();
}),
);

if (await isEnabled()) {
await restartClient(context);
}

// These checks are added later to avoid ZLS be started twice because `isEnabled` sets `zig.zls.enabled`.
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(async (change) => {
// The `zig.path` config option is handled by `zigProvider.onChange`.
if (
Expand All @@ -381,10 +402,6 @@ export async function activate(context: vscode.ExtensionContext) {
await restartClient(context);
}),
);

if (await isEnabled()) {
await restartClient(context);
}
}

export async function deactivate(): Promise<void> {
Expand Down

0 comments on commit 297638a

Please sign in to comment.