feat: Allow unsetting env vars in server.extraEnv config

This commit is contained in:
Lukas Wirth 2025-04-20 12:54:55 +02:00
parent 66e3b5819e
commit 05b374acd4
5 changed files with 38 additions and 20 deletions

View file

@ -543,7 +543,8 @@
"additionalProperties": {
"type": [
"string",
"number"
"number",
"null"
]
},
"default": null,

View file

@ -187,8 +187,16 @@ async function hasToolchainFileWithRaDeclared(uri: vscode.Uri): Promise<boolean>
export async function isValidExecutable(path: string, extraEnv: Env): Promise<boolean> {
log.debug("Checking availability of a binary at", path);
const newEnv = { ...process.env };
for (const [k, v] of Object.entries(extraEnv)) {
if (v) {
newEnv[k] = v;
} else if (k in newEnv) {
delete newEnv[k];
}
}
const res = await spawnAsync(path, ["--version"], {
env: { ...process.env, ...extraEnv },
env: newEnv,
});
if (res.error) {

View file

@ -213,12 +213,13 @@ export class Config {
get serverExtraEnv(): Env {
const extraEnv =
this.get<{ [key: string]: string | number } | null>("server.extraEnv") ?? {};
this.get<{ [key: string]: { toString(): string } | null } | null>("server.extraEnv") ??
{};
return substituteVariablesInEnv(
Object.fromEntries(
Object.entries(extraEnv).map(([k, v]) => [
k,
typeof v !== "string" ? v.toString() : v,
typeof v === "string" ? v : v?.toString(),
]),
),
);
@ -398,6 +399,7 @@ export function prepareVSCodeConfig<T>(resp: T): T {
// FIXME: Merge this with `substituteVSCodeVariables` above
export function substituteVariablesInEnv(env: Env): Env {
const depRe = new RegExp(/\${(?<depName>.+?)}/g);
const missingDeps = new Set<string>();
// vscode uses `env:ENV_NAME` for env vars resolution, and it's easier
// to follow the same convention for our dependency tracking
@ -405,15 +407,16 @@ export function substituteVariablesInEnv(env: Env): Env {
const envWithDeps = Object.fromEntries(
Object.entries(env).map(([key, value]) => {
const deps = new Set<string>();
const depRe = new RegExp(/\${(?<depName>.+?)}/g);
let match = undefined;
while ((match = depRe.exec(value))) {
const depName = unwrapUndefinable(match.groups?.["depName"]);
deps.add(depName);
// `depName` at this point can have a form of `expression` or
// `prefix:expression`
if (!definedEnvKeys.has(depName)) {
missingDeps.add(depName);
if (value) {
let match = undefined;
while ((match = depRe.exec(value))) {
const depName = unwrapUndefinable(match.groups?.["depName"]);
deps.add(depName);
// `depName` at this point can have a form of `expression` or
// `prefix:expression`
if (!definedEnvKeys.has(depName)) {
missingDeps.add(depName);
}
}
}
return [`env:${key}`, { deps: [...deps], value }];
@ -454,11 +457,10 @@ export function substituteVariablesInEnv(env: Env): Env {
do {
leftToResolveSize = toResolve.size;
for (const key of toResolve) {
const item = unwrapUndefinable(envWithDeps[key]);
if (item.deps.every((dep) => resolved.has(dep))) {
item.value = item.value.replace(/\${(?<depName>.+?)}/g, (_wholeMatch, depName) => {
const item = unwrapUndefinable(envWithDeps[depName]);
return item.value;
const item = envWithDeps[key];
if (item && item.deps.every((dep) => resolved.has(dep))) {
item.value = item.value?.replace(/\${(?<depName>.+?)}/g, (_wholeMatch, depName) => {
return envWithDeps[depName]?.value ?? "";
});
resolved.add(key);
toResolve.delete(key);

View file

@ -213,7 +213,14 @@ export class Ctx implements RustAnalyzerExtensionApi {
this.refreshServerStatus();
},
);
const newEnv = Object.assign({}, process.env, this.config.serverExtraEnv);
const newEnv = { ...process.env };
for (const [k, v] of Object.entries(this.config.serverExtraEnv)) {
if (v) {
newEnv[k] = v;
} else if (k in newEnv) {
delete newEnv[k];
}
}
const run: lc.Executable = {
command: this._serverPath,
options: { env: newEnv },

View file

@ -14,7 +14,7 @@ export function assert(condition: boolean, explanation: string): asserts conditi
}
export type Env = {
[name: string]: string;
[name: string]: string | undefined;
};
class Log {