Skip to content

Commit f3e1e21

Browse files
committed
Manage the entire toolchain wrt haskell#540
Please enter the commit message for your changes. Lines starting
1 parent 70f40be commit f3e1e21

File tree

1 file changed

+107
-44
lines changed

1 file changed

+107
-44
lines changed

src/hlsBinaries.ts

Lines changed: 107 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export async function findHaskellLanguageServer(
226226

227227
if (!manageHLS) {
228228
// plugin needs initialization
229-
const promptMessage = 'How do you want the extension to manage/discover HLS?';
229+
const promptMessage = 'How do you want the extension to manage/discover HLS and the relevant toolchain?';
230230

231231
const decision =
232232
(await window.showInformationMessage(promptMessage, 'system ghcup (recommended)', 'internal ghcup', 'PATH')) ||
@@ -249,39 +249,64 @@ export async function findHaskellLanguageServer(
249249
// we manage HLS, make sure ghcup is installed/available
250250
await getGHCup(context, logger);
251251

252-
// get a preliminary hls wrapper for finding project GHC version,
253-
// later we may install a different HLS that supports the given GHC
254-
let wrapper = await getLatestWrapperFromGHCup(context, logger).then((e) =>
255-
!e
256-
? callGHCup(context, logger, ['install', 'hls'], 'Installing latest HLS', true).then(() =>
257-
callGHCup(
258-
context,
259-
logger,
260-
['whereis', 'hls'],
261-
undefined,
262-
false,
263-
(err, stdout, _stderr, resolve, reject) => {
264-
err ? reject("Couldn't find latest HLS") : resolve(stdout?.trim());
265-
}
266-
)
267-
)
268-
: e
269-
);
252+
// get a preliminary toolchain for finding the correct project GHC version (we need HLS and cabal/stack and ghc as fallback),
253+
// later we may install a different toolchain that's more project-specific
254+
const installGHC = !executableExists('ghc');
255+
let latestHLS = await getLatestToolFromGHCup(context, logger, 'hls');
256+
let latestCabal = await getLatestToolFromGHCup(context, logger, 'cabal');
257+
let latestStack = await getLatestToolFromGHCup(context, logger, 'stack');
258+
let recGHC = await getLatestAvailableToolFromGHCup(context, logger, 'ghc', 'recommended');
259+
// TODO: this should be obsolete for ghcup-0.1.17.6
260+
// and we can drop the use of `-b`.
261+
let symHLSPath = installGHC
262+
? path.join(storagePath, `hls-${latestHLS}_cabal-${latestCabal}-stack-${latestStack}`)
263+
: path.join(storagePath, `hls-${latestHLS}_ghc-${recGHC}-cabal-${latestCabal}-stack-${latestStack}`);
264+
265+
const latestToolchainBindir = await callGHCup(
266+
context,
267+
logger,
268+
[ 'run'
269+
, '--hls', latestHLS ? latestHLS : 'latest'
270+
, '--cabal', latestCabal ? latestCabal : 'latest'
271+
, '--stack', latestStack ? latestStack : 'latest'
272+
, ...(installGHC ? ['--ghc', 'recommended'] : [])
273+
, '-b', symHLSPath
274+
, '--install'
275+
],
276+
'Installing latest toolchain for bootstrap',
277+
true,
278+
(err, stdout, _stderr, resolve, reject) => {
279+
err ? reject("Couldn't install latest toolchain") : resolve(stdout?.trim());
280+
}
281+
);
270282

271283
// now figure out the project GHC version and the latest supported HLS version
272284
// we need for it (e.g. this might in fact be a downgrade for old GHCs)
273-
const installableHls = await getLatestHLS(context, logger, workingDir, wrapper);
285+
const [installableHls, projectGhc] = await getLatestHLS(context, logger, workingDir, latestToolchainBindir);
286+
287+
latestHLS = await getLatestToolFromGHCup(context, logger, 'hls')
288+
latestCabal = await getLatestToolFromGHCup(context, logger, 'cabal');
289+
latestStack = await getLatestToolFromGHCup(context, logger, 'stack');
274290

275291
// now install said version in an isolated symlink directory
276-
const symHLSPath = path.join(storagePath, 'hls', installableHls);
277-
wrapper = path.join(symHLSPath, `haskell-language-server-wrapper${exeExt}`);
292+
293+
// TODO: this should be obsolete for ghcup-0.1.17.6
294+
// and we can drop the use of `-b`.
295+
symHLSPath = path.join(storagePath, `hls-${installableHls}_ghc-${projectGhc}_cabal-${latestCabal}-stack-${latestStack}`);
296+
297+
const wrapper = path.join(symHLSPath, `haskell-language-server-wrapper${exeExt}`);
278298
// Check if we have a working symlink, so we can avoid another popup
279299
if (!fs.existsSync(wrapper)) {
280300
await callGHCup(
281301
context,
282302
logger,
283-
['run', '--hls', installableHls, '-b', symHLSPath, '-i'],
284-
`Installing HLS ${installableHls}`,
303+
[ 'run'
304+
, '--hls', installableHls
305+
, '--ghc', projectGhc
306+
, '--cabal', `${latestCabal}`
307+
, '--stack', `${latestStack}`
308+
, '-b', symHLSPath, '-i'],
309+
`Installing project specific toolchain: HLS-${installableHls}, GHC-${projectGhc}, cabal-${latestCabal}, stack-${latestStack}`,
285310
true
286311
);
287312
}
@@ -340,13 +365,13 @@ async function getLatestHLS(
340365
context: ExtensionContext,
341366
logger: Logger,
342367
workingDir: string,
343-
wrapper?: string
344-
): Promise<string> {
368+
toolchainBindir: string
369+
): Promise<[string, string]> {
345370
const storagePath: string = await getStoragePath(context);
346371

347372
// get project GHC version, but fallback to system ghc if necessary.
348-
const projectGhc = wrapper
349-
? await getProjectGHCVersion(wrapper, workingDir, logger)
373+
const projectGhc = toolchainBindir
374+
? await getProjectGHCVersion(toolchainBindir, workingDir, logger)
350375
: await callAsync(`ghc${exeExt}`, ['--numeric-version'], storagePath, logger, undefined, false);
351376
const noMatchingHLS = `No HLS version was found for supporting GHC ${projectGhc}.`;
352377

@@ -368,7 +393,7 @@ async function getLatestHLS(
368393
window.showErrorMessage(noMatchingHLS);
369394
throw new Error(noMatchingHLS);
370395
} else {
371-
return latest[0];
396+
return [latest[0], projectGhc];
372397
}
373398
}
374399

@@ -380,21 +405,27 @@ async function getLatestHLS(
380405
* @param logger Logger for feedback.
381406
* @returns The GHC version, or fail with an `Error`.
382407
*/
383-
export async function getProjectGHCVersion(wrapper: string, workingDir: string, logger: Logger): Promise<string> {
408+
export async function getProjectGHCVersion(toolchainBindir: string, workingDir: string, logger: Logger): Promise<string> {
384409
const title = 'Working out the project GHC version. This might take a while...';
385410
logger.info(title);
411+
386412
const args = ['--project-ghc-version'];
387413

414+
const newPath = addPathToProcessPath(toolchainBindir);
415+
const environmentNew: IEnvVars = {
416+
PATH: newPath,
417+
};
418+
388419
return callAsync(
389-
wrapper,
420+
'haskell-language-server-wrapper',
390421
args,
391422
workingDir,
392423
logger,
393424
title,
394425
false,
395-
undefined,
426+
environmentNew,
396427
(err, stdout, stderr, resolve, reject) => {
397-
const command: string = wrapper + ' ' + args.join(' ');
428+
const command: string = 'haskell-language-server-wrapper' + ' ' + args.join(' ');
398429
if (err) {
399430
logger.error(`Error executing '${command}' with error code ${err.code}`);
400431
logger.error(`stderr: ${stderr}`);
@@ -413,7 +444,7 @@ export async function getProjectGHCVersion(wrapper: string, workingDir: string,
413444
}
414445
reject(new MissingToolError('unknown'));
415446
}
416-
reject(Error(`${wrapper} --project-ghc-version exited with exit code ${err.code}:\n${stdout}\n${stderr}`));
447+
reject(Error(`haskell-language-server --project-ghc-version exited with exit code ${err.code}:\n${stdout}\n${stderr}`));
417448
} else {
418449
logger.info(`The GHC version for the project or file: ${stdout?.trim()}`);
419450
resolve(stdout?.trim());
@@ -541,25 +572,57 @@ export function addPathToProcessPath(extraPath: string): string {
541572
return PATH.join(pathSep);
542573
}
543574

544-
async function getLatestWrapperFromGHCup(context: ExtensionContext, logger: Logger): Promise<string | null> {
545-
const hlsVersions = await callGHCup(
575+
// the tool might be installed or not
576+
async function getLatestToolFromGHCup(context: ExtensionContext, logger: Logger, tool: string): Promise<string> {
577+
// these might be custom/stray/compiled, so we try first
578+
const installedVersions = await callGHCup(
546579
context,
547580
logger,
548-
['list', '-t', 'hls', '-c', 'installed', '-r'],
581+
['list', '-t', tool, '-c', 'installed', '-r'],
549582
undefined,
550583
false
551584
);
552-
const installed = hlsVersions.split(/\r?\n/).pop();
553-
if (installed) {
554-
const latestHlsVersion = installed.split(' ')[1];
585+
const latestInstalled = installedVersions.split(/\r?\n/).pop();
586+
if (latestInstalled) {
587+
const latestInstalledVersion = latestInstalled.split(/\s+/)[1];
555588

556-
let bin = await callGHCup(context, logger, ['whereis', 'hls', `${latestHlsVersion}`], undefined, false);
557-
return bin;
589+
let bin = await callGHCup(context, logger, ['whereis', tool, `${latestInstalledVersion}`], undefined, false);
590+
const storagePath: string = await getStoragePath(context);
591+
const ver = await callAsync(`${bin}`, ['--numeric-version'], storagePath, logger, undefined, false)
592+
if (ver) {
593+
return ver;
594+
} else {
595+
throw new Error(`Could not figure out version of ${bin}`);
596+
}
597+
}
598+
599+
return getLatestAvailableToolFromGHCup(context, logger, tool);
600+
}
601+
602+
async function getLatestAvailableToolFromGHCup(context: ExtensionContext, logger: Logger, tool: string, tag?: string, criteria?: string): Promise<string> {
603+
// fall back to installable versions
604+
const availableVersions = await callGHCup(
605+
context,
606+
logger,
607+
['list', '-t', tool, '-c', criteria ? criteria : 'available', '-r'],
608+
undefined,
609+
false
610+
).then(s => s.split(/\r?\n/));
611+
612+
let latestAvailable: string | null = null;
613+
availableVersions.forEach((ver) => {
614+
if (ver.split(/\s+/)[2].split(',').includes(tag ? tag : 'latest')) {
615+
latestAvailable = ver.split(/\s+/)[1];
616+
}
617+
});
618+
if (!latestAvailable) {
619+
throw new Error(`Unable to find ${tag ? tag : 'latest'} tool ${tool}`)
558620
} else {
559-
return null;
621+
return latestAvailable;
560622
}
561623
}
562624

625+
563626
// complements getLatestHLSfromMetadata, by checking possibly locally compiled
564627
// HLS in ghcup
565628
// If 'targetGhc' is omitted, picks the latest 'haskell-language-server-wrapper',
@@ -585,7 +648,7 @@ async function getHLSesFromGHCup(
585648
.catch(() => false);
586649
});
587650

588-
const installed = hlsVersions.split(/\r?\n/).map((e) => e.split(' ')[1]);
651+
const installed = hlsVersions.split(/\r?\n/).map((e) => e.split(/\s+/)[1]);
589652
if (installed?.length) {
590653
const myMap = new Map<string, string[]>();
591654
installed.forEach((hls) => {

0 commit comments

Comments
 (0)