Skip to content
This repository was archived by the owner on Nov 25, 2025. It is now read-only.

Commit 0dbb0fe

Browse files
committed
update exe lookup to resolve file extensions and reject relative paths
fixes #164 The error messages have also been updated to improve readability.
1 parent 70c2cc5 commit 0dbb0fe

4 files changed

Lines changed: 67 additions & 62 deletions

File tree

src/zigProvider.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,9 @@ export class ZigProvider {
5656
public resolveZigPathConfigOption(zigPath?: string): ExeWithVersion | null | undefined {
5757
zigPath ??= vscode.workspace.getConfiguration("zig").get<string>("path", "");
5858
if (!zigPath) return null;
59-
const exePath = zigPath !== "zig" ? zigPath : null; // the string "zig" means lookup in PATH
60-
const result = resolveExePathAndVersion(exePath, "zig", "zig.path", "version");
59+
const result = resolveExePathAndVersion(zigPath, "version");
6160
if ("message" in result) {
62-
void vscode.window.showErrorMessage(result.message);
61+
void vscode.window.showErrorMessage(`Unexpected 'zig.path': ${result.message}`);
6362
return undefined;
6463
}
6564
return result;

src/zigSetup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ async function installZig(context: vscode.ExtensionContext, temporaryVersion?: s
3939

4040
if (!version) {
4141
// Lookup zig in $PATH
42-
const result = resolveExePathAndVersion(null, "zig", null, "version");
42+
const result = resolveExePathAndVersion("zig", "version");
4343
if ("exe" in result) {
4444
await vscode.workspace.getConfiguration("zig").update("path", undefined, true);
4545
zigProvider.set(result);
@@ -234,7 +234,7 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) {
234234
});
235235
}
236236

237-
const zigInPath = resolveExePathAndVersion(null, "zig", null, "version");
237+
const zigInPath = resolveExePathAndVersion("zig", "version");
238238
if (!("message" in zigInPath)) {
239239
items.push({
240240
label: "Use Zig in PATH",

src/zigUtil.ts

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import { debounce } from "lodash-es";
1010
import semver from "semver";
1111
import which from "which";
1212

13-
// Replace any references to predefined variables in config string.
14-
// https://code.visualstudio.com/docs/editor/variables-reference#_predefined-variables
13+
/**
14+
* Replace any references to predefined variables in config string.
15+
* https://code.visualstudio.com/docs/editor/variables-reference#_predefined-variables
16+
*/
1517
export function handleConfigOption(input: string): string {
1618
if (input.includes("${userHome}")) {
1719
input = input.replaceAll("${userHome}", os.homedir());
@@ -51,60 +53,62 @@ export function handleConfigOption(input: string): string {
5153

5254
/** Resolves the absolute executable path and version of a program like Zig or ZLS. */
5355
export function resolveExePathAndVersion(
54-
/** `null` means lookup in PATH */
55-
exePath: string | null,
56-
/** e.g. `zig` or `zig` */
57-
exeName: string,
58-
/** e.g. `zig.path` or `zig.zls.path`. Can be null if `exePath === null` */
59-
optionName: string | null,
56+
/**
57+
* - resolves '~' to the user home directory.
58+
* - resolves VS Code predefined variables.
59+
* - resolves possible executable file extensions on windows like '.exe' or '.cmd'.
60+
*/
61+
cmd: string,
6062
/**
6163
* The command-line argument that is used to query the version of the executable.
6264
* Zig uses `version`. ZLS uses `--version`.
6365
*/
6466
versionArg: string,
6567
): { exe: string; version: semver.SemVer } | { message: string } {
66-
/* `optionName === null` implies `exePath === null` */
67-
assert(optionName !== null || exePath === null);
68+
assert(cmd.length);
6869

69-
let resolvedExePath;
70-
if (!exePath) {
71-
resolvedExePath = which.sync(exeName, { nothrow: true });
72-
} else {
73-
// allow passing predefined variables
74-
resolvedExePath = handleConfigOption(exePath);
75-
76-
if (resolvedExePath.startsWith("~")) {
77-
resolvedExePath = path.join(os.homedir(), resolvedExePath.substring(1));
78-
} else if (!path.isAbsolute(resolvedExePath)) {
79-
resolvedExePath = which.sync(resolvedExePath, { nothrow: true });
80-
}
81-
}
70+
// allow passing predefined variables
71+
cmd = handleConfigOption(cmd);
8272

83-
if (!resolvedExePath) {
84-
return {
85-
message: (optionName ? `\`${optionName}\` ` : "") + `Could not find '${exePath ?? exeName}' in PATH`,
86-
};
73+
if (cmd.startsWith("~")) {
74+
cmd = path.join(os.homedir(), cmd.substring(1));
8775
}
8876

89-
if (!fs.existsSync(resolvedExePath)) {
90-
return {
91-
message: (optionName ? `\`${optionName}\` ` : "") + `${resolvedExePath} does not exist`,
92-
};
77+
const isWindows = os.platform() === "win32";
78+
const isAbsolute = path.isAbsolute(cmd);
79+
const hasPathSeparator = !!/\//.exec(cmd) || (isWindows && !!/\\/.exec(cmd));
80+
if (!isAbsolute && hasPathSeparator) {
81+
// A value like `./zig` would be looked up relative to the cwd of the VS Code process which makes little sense.
82+
return { message: `'${cmd}' is not valid` };
9383
}
9484

95-
try {
96-
fs.accessSync(resolvedExePath, fs.constants.R_OK | fs.constants.X_OK);
97-
} catch {
98-
return {
99-
message: optionName
100-
? `\`${optionName}\` ${resolvedExePath} is not an executable`
101-
: `${resolvedExePath} is not an executable`,
102-
};
85+
let exePath = which.sync(cmd, { nothrow: true });
86+
if (!exePath) {
87+
if (!isAbsolute) {
88+
return { message: `Could not find '${cmd}' in PATH` };
89+
}
90+
91+
if (!fs.existsSync(cmd)) {
92+
return {
93+
message: `'${cmd}' does not exist`,
94+
};
95+
}
96+
97+
try {
98+
fs.accessSync(cmd, fs.constants.R_OK | fs.constants.X_OK);
99+
} catch {
100+
return {
101+
message: `'${cmd}' is not an executable`,
102+
};
103+
}
104+
105+
// this code path should be impossible
106+
exePath = cmd;
103107
}
104108

105-
const version = getVersion(resolvedExePath, versionArg);
106-
if (!version) return { message: `Failed to run '${resolvedExePath} ${versionArg}'!` };
107-
return { exe: resolvedExePath, version: version };
109+
const version = getVersion(exePath, versionArg);
110+
if (!version) return { message: `Failed to run '${exePath} ${versionArg}'!` };
111+
return { exe: exePath, version: version };
108112
}
109113

110114
export function asyncDebounce<T extends (...args: unknown[]) => Promise<Awaited<ReturnType<T>>>>(

src/zls.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -110,22 +110,24 @@ async function getZLSPath(context: vscode.ExtensionContext): Promise<{ exe: stri
110110
if (!!zlsExePath) {
111111
// This will fail on older ZLS version that do not support `zls --version`.
112112
// It should be more likely that the given executable is invalid than someone using ZLS 0.9.0 or older.
113-
const result = resolveExePathAndVersion(zlsExePath, "zls", "zig.zls.path", "--version");
113+
const result = resolveExePathAndVersion(zlsExePath, "--version");
114114
if ("message" in result) {
115-
vscode.window.showErrorMessage(result.message, "install ZLS", "open settings").then(async (response) => {
116-
switch (response) {
117-
case "install ZLS":
118-
const zlsConfig = vscode.workspace.getConfiguration("zig.zls");
119-
await workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "on", true);
120-
await workspaceConfigUpdateNoThrow(zlsConfig, "path", undefined);
121-
break;
122-
case "open settings":
123-
await vscode.commands.executeCommand("workbench.action.openSettings", "zig.zls.path");
124-
break;
125-
case undefined:
126-
break;
127-
}
128-
});
115+
vscode.window
116+
.showErrorMessage(`Unexpected 'zig.zls.path': ${result.message}`, "install ZLS", "open settings")
117+
.then(async (response) => {
118+
switch (response) {
119+
case "install ZLS":
120+
const zlsConfig = vscode.workspace.getConfiguration("zig.zls");
121+
await workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "on", true);
122+
await workspaceConfigUpdateNoThrow(zlsConfig, "path", undefined);
123+
break;
124+
case "open settings":
125+
await vscode.commands.executeCommand("workbench.action.openSettings", "zig.zls.path");
126+
break;
127+
case undefined:
128+
break;
129+
}
130+
});
129131
return null;
130132
}
131133
return result;

0 commit comments

Comments
 (0)