Skip to content

Commit 96102ba

Browse files
committed
Fix PATH handling and don't overwrite process.env
1 parent fe8d0e7 commit 96102ba

File tree

3 files changed

+72
-46
lines changed

3 files changed

+72
-46
lines changed

src/extension.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import {
2323
import { CommandNames } from './commands/constants';
2424
import { ImportIdentifier } from './commands/importIdentifier';
2525
import { DocsBrowser } from './docsBrowser';
26-
import { MissingToolError, addPathToProcessPath, findHaskellLanguageServer, IEnvVars } from './hlsBinaries';
27-
import { expandHomeDir, ExtensionLogger } from './utils';
26+
import { MissingToolError, findHaskellLanguageServer, IEnvVars } from './hlsBinaries';
27+
import { expandHomeDir, ExtensionLogger, addPathToProcessPath } from './utils';
2828

2929
// The current map of documents & folders to language servers.
3030
// It may be null to indicate that we are in the process of launching a server,
@@ -196,17 +196,17 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
196196
}
197197
logger.info(cwdMsg);
198198

199-
let serverEnvironment: IEnvVars = workspace.getConfiguration('haskell', uri).serverEnvironment;
199+
let serverEnvironment: IEnvVars = await workspace.getConfiguration('haskell', uri).serverEnvironment;
200200
if (addInternalServerPath !== undefined) {
201-
const newPath = addPathToProcessPath(addInternalServerPath);
201+
const newPath = await addPathToProcessPath(addInternalServerPath, logger);
202202
serverEnvironment = {
203-
PATH: newPath,
204203
...serverEnvironment,
204+
...{PATH: newPath},
205205
};
206206
}
207207
const exeOptions: ExecutableOptions = {
208208
cwd: folder ? undefined : path.dirname(uri.fsPath),
209-
env: Object.assign(process.env, serverEnvironment),
209+
env: {...process.env, ...serverEnvironment},
210210
};
211211

212212
// We don't want empty strings in our args

src/hlsBinaries.ts

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,11 @@ import {
1717
WorkspaceFolder,
1818
} from 'vscode';
1919
import { Logger } from 'vscode-languageclient';
20-
import { executableExists, httpsGetSilently, resolvePathPlaceHolders } from './utils';
20+
import { executableExists, httpsGetSilently, resolvePathPlaceHolders, IEnvVars, addPathToProcessPath, resolveServerEnvironmentPATH } from './utils';
21+
export { IEnvVars }
2122

2223
export type ReleaseMetadata = Map<string, Map<string, Map<string, string[]>>>;
2324

24-
// Used for environment variables later on
25-
export interface IEnvVars {
26-
[key: string]: string;
27-
}
28-
2925
type ManageHLS = 'GHCup' | 'PATH';
3026
let manageHLS = workspace.getConfiguration('haskell').get('manageHLS') as ManageHLS | null;
3127

@@ -103,9 +99,10 @@ async function callAsync(
10399
reject: (reason?: any) => void
104100
) => void
105101
): Promise<string> {
106-
let newEnv: IEnvVars = workspace.getConfiguration('haskell').get('serverEnvironment') || {};
107-
newEnv = Object.assign(process.env, newEnv);
108-
newEnv = Object.assign(newEnv, (envAdd || {}));
102+
let newEnv: IEnvVars = await resolveServerEnvironmentPATH(workspace.getConfiguration('haskell').get('serverEnvironment') || {});
103+
newEnv = {...process.env as IEnvVars, ...newEnv};
104+
newEnv = {...newEnv, ...(envAdd || {})};
105+
logger.info(`newEnv: ${newEnv.PATH!.split(':')}`);
109106
return window.withProgress(
110107
{
111108
location: ProgressLocation.Notification,
@@ -162,12 +159,12 @@ async function callAsync(
162159

163160
/** Gets serverExecutablePath and fails if it's not set.
164161
*/
165-
function findServerExecutable(context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder): string {
162+
async function findServerExecutable(context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder): Promise<string> {
166163
let exePath = workspace.getConfiguration('haskell').get('serverExecutablePath') as string;
167164
logger.info(`Trying to find the server executable in: ${exePath}`);
168165
exePath = resolvePathPlaceHolders(exePath, folder);
169166
logger.log(`Location after path variables substitution: ${exePath}`);
170-
if (executableExists(exePath)) {
167+
if (await executableExists(exePath)) {
171168
return exePath;
172169
} else {
173170
const msg = `Could not find a HLS binary at ${exePath}! Consider installing HLS via ghcup or change "haskell.manageHLS" in your settings.`;
@@ -178,13 +175,13 @@ function findServerExecutable(context: ExtensionContext, logger: Logger, folder?
178175

179176
/** Searches the PATH. Fails if nothing is found.
180177
*/
181-
function findHLSinPATH(context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder): string {
178+
async function findHLSinPATH(context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder): Promise<string> {
182179
// try PATH
183180
const exes: string[] = ['haskell-language-server-wrapper', 'haskell-language-server'];
184181
logger.info(`Searching for server executables ${exes.join(',')} in $PATH`);
185182
logger.info(`$PATH environment variable: ${process.env.PATH}`);
186183
for (const exe of exes) {
187-
if (executableExists(exe)) {
184+
if (await executableExists(exe)) {
188185
logger.info(`Found server executable in $PATH: ${exe}`);
189186
return exe;
190187
}
@@ -248,7 +245,7 @@ export async function findHaskellLanguageServer(
248245
return findHLSinPATH(context, logger, folder);
249246
} else {
250247
// we manage HLS, make sure ghcup is installed/available
251-
await getGHCup(context, logger);
248+
await upgradeGHCup(context, logger);
252249

253250
// get a preliminary toolchain for finding the correct project GHC version (we need HLS and cabal/stack and ghc as fallback),
254251
// later we may install a different toolchain that's more project-specific
@@ -319,8 +316,9 @@ async function callGHCup(
319316
const metadataUrl = workspace.getConfiguration('haskell').metadataURL;
320317

321318
if (manageHLS === 'GHCup') {
319+
const ghcup = await findGHCup(context, logger);
322320
return await callAsync(
323-
'ghcup',
321+
ghcup,
324322
['--no-verbose'].concat(metadataUrl ? ['-s', metadataUrl] : []).concat(args),
325323
logger,
326324
undefined,
@@ -382,7 +380,7 @@ export async function getProjectGHCVersion(toolchainBindir: string, workingDir:
382380

383381
const args = ['--project-ghc-version'];
384382

385-
const newPath = addPathToProcessPath(toolchainBindir);
383+
const newPath = await addPathToProcessPath(toolchainBindir, logger);
386384
const environmentNew: IEnvVars = {
387385
PATH: newPath,
388386
};
@@ -424,29 +422,28 @@ export async function getProjectGHCVersion(toolchainBindir: string, workingDir:
424422
);
425423
}
426424

427-
/**
428-
* Downloads the latest ghcup binary.
429-
* Returns undefined if it can't find any for the given architecture/platform.
430-
*/
431-
export async function getGHCup(context: ExtensionContext, logger: Logger): Promise<string | undefined> {
432-
logger.info('Checking for ghcup installation');
433-
const localGHCup = ['ghcup'].find(executableExists);
434-
if (!localGHCup) {
435-
throw new MissingToolError('ghcup');
436-
}
437-
425+
export async function upgradeGHCup(context: ExtensionContext, logger: Logger): Promise<void> {
438426
if (manageHLS === 'GHCup') {
439-
logger.info(`found ghcup at ${localGHCup}`);
440427
const upgrade = workspace.getConfiguration('haskell').get('upgradeGHCup') as boolean;
441428
if (upgrade) {
442429
await callGHCup(context, logger, ['upgrade'], 'Upgrading ghcup', true);
443430
}
444-
return localGHCup;
445431
} else {
446432
throw new Error(`Internal error: tried to call ghcup while haskell.manageHLS is set to ${manageHLS}. Aborting!`);
447433
}
448434
}
449435

436+
export async function findGHCup(context: ExtensionContext, logger: Logger): Promise<string> {
437+
logger.info('Checking for ghcup installation');
438+
const localGHCup = ['ghcup'].find(executableExists);
439+
if (!localGHCup) {
440+
throw new MissingToolError('ghcup');
441+
} else {
442+
logger.info(`found ghcup at ${localGHCup}`);
443+
return localGHCup
444+
}
445+
}
446+
450447
/**
451448
* Compare the PVP versions of two strings.
452449
* Details: https://github.com/haskell/pvp/
@@ -496,13 +493,6 @@ export async function getStoragePath(context: ExtensionContext): Promise<string>
496493
return storagePath;
497494
}
498495

499-
export function addPathToProcessPath(extraPath: string): string {
500-
const pathSep = process.platform === 'win32' ? ';' : ':';
501-
const PATH = process.env.PATH!.split(pathSep);
502-
PATH.unshift(extraPath);
503-
return PATH.join(pathSep);
504-
}
505-
506496
// the tool might be installed or not
507497
async function getLatestToolFromGHCup(context: ExtensionContext, logger: Logger, tool: string): Promise<string> {
508498
// these might be custom/stray/compiled, so we try first

src/utils.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ import * as os from 'os';
88
import { extname } from 'path';
99
import * as url from 'url';
1010
import { promisify } from 'util';
11-
import { OutputChannel, ProgressLocation, window, WorkspaceFolder } from 'vscode';
11+
import { workspace, OutputChannel, ProgressLocation, window, WorkspaceFolder } from 'vscode';
1212
import { Logger } from 'vscode-languageclient';
1313
import * as which from 'which';
1414
import * as yazul from 'yauzl';
1515
import { createGunzip } from 'zlib';
1616

17+
// Used for environment variables later on
18+
export interface IEnvVars {
19+
[key: string]: string;
20+
}
21+
1722
enum LogLevel {
1823
Off,
1924
Error,
@@ -279,11 +284,13 @@ function getWithRedirects(opts: https.RequestOptions, f: (res: http.IncomingMess
279284
/*
280285
* Checks if the executable is on the PATH
281286
*/
282-
export function executableExists(exe: string): boolean {
287+
export async function executableExists(exe: string): Promise<boolean> {
283288
const isWindows = process.platform === 'win32';
289+
let newEnv: IEnvVars = await resolveServerEnvironmentPATH(workspace.getConfiguration('haskell').get('serverEnvironment') || {});
290+
newEnv = {...process.env as IEnvVars, ...newEnv};
284291
const cmd: string = isWindows ? 'where' : 'which';
285-
const out = child_process.spawnSync(cmd, [exe]);
286-
return out.status === 0 || (which.sync(exe, { nothrow: true }) ?? '') !== '';
292+
const out = child_process.spawnSync(cmd, [exe], { env: newEnv });
293+
return out.status === 0 || (which.sync(exe, { nothrow: true, path: newEnv.PATH }) ?? '') !== '';
287294
}
288295

289296
export function directoryExists(path: string): boolean {
@@ -304,3 +311,32 @@ export function resolvePathPlaceHolders(path: string, folder?: WorkspaceFolder)
304311
}
305312
return path;
306313
}
314+
315+
export function resolvePATHPlaceHolders(path: string, folder?: WorkspaceFolder) {
316+
return path
317+
.replace('${HOME}', os.homedir)
318+
.replace('${home}', os.homedir)
319+
.replace('$PATH', process.env.PATH!)
320+
.replace('${PATH}', process.env.PATH!);
321+
}
322+
323+
// also honours serverEnvironment.PATH
324+
export async function addPathToProcessPath(extraPath: string, logger: Logger): Promise<string> {
325+
const pathSep = process.platform === 'win32' ? ';' : ':';
326+
const serverEnvironment: IEnvVars = (await workspace.getConfiguration('haskell').get('serverEnvironment')) || {};
327+
const path: string[] = serverEnvironment.PATH
328+
? serverEnvironment.PATH.split(pathSep).map((p) => resolvePATHPlaceHolders(p))
329+
: process.env.PATH!.split(pathSep);
330+
path.unshift(extraPath);
331+
return path.join(pathSep);
332+
}
333+
334+
export async function resolveServerEnvironmentPATH(serverEnv: IEnvVars): Promise<IEnvVars> {
335+
const pathSep = process.platform === 'win32' ? ';' : ':';
336+
const path: string[] = serverEnv.PATH.split(pathSep).map((p) => resolvePATHPlaceHolders(p));
337+
return {
338+
...serverEnv,
339+
...{ PATH: path.join(pathSep)}
340+
}
341+
}
342+

0 commit comments

Comments
 (0)