Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider user installed HLSes (e.g. via ghcup compile) #543

Merged
merged 8 commits into from
Mar 17, 2022

Conversation

hasufell
Copy link
Member

Fixes #542

We don't do random fallbacks anymore. If serverExecutablePath
is set, it must exist. We only check PATH if no other setting
is enabled.
Comment on lines +157 to +166
if (e instanceof MissingToolError) {
const link = e.installLink();
if (link) {
if (await window.showErrorMessage(e.message, `Install ${e.tool}`)) {
env.openExternal(link);
}
} else {
await window.showErrorMessage(e.message);
}
} else if (e instanceof Error) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here at the top-level of extension init we catch all promise errors and also restore the MissingToolError with link popup.

*/
function findHLSinPATH(context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder): string | null {
// try 'serverExecutablePath' setting
function findServerExecutable(context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder): string {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we separate findHLSinPATH and findServerExecutable...

Comment on lines 221 to 249
// permissively check if we have HLS installed
// this is just to avoid a popup
let wrapper = await callGHCup(context, logger,
['whereis', 'hls'],
undefined,
false,
(err, stdout, _stderr, resolve, _reject) => { err ? resolve('') : resolve(stdout?.trim()); }
);
if (wrapper === '') {
// install recommended HLS... even if this doesn't support the project GHC, because
// we need a HLS to find the correct project GHC in the first place
await callGHCup(context, logger,
// we manage HLS, make sure ghcup is installed/available
await getGHCup(context, logger);

// get a preliminary hls wrapper for finding project GHC version,
// later we may install a different HLS that supports the given GHC
let wrapper = await getLatestHLSfromGHCup(context, storagePath, logger).then(e =>
(e === null)
? callGHCup(context, logger,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's some considerable cleanup... now we pick a proper hls-wrapper executable (e.g. a compiled one) for the first pass (the one that just discovers project GHC), not just the recommended... then the next pass will possibly downgrade the wrapper to match the project GHC.

@@ -346,10 +384,15 @@ export async function getProjectGHCVersion(
logger.error(`stdout: ${stdout}`);
}
// Error message emitted by HLS-wrapper
const regex = /Cradle requires (.+) but couldn't find it/;
const regex = /Cradle requires (.+) but couldn't find it|The program \'(.+)\' version .* is required but the version of.*could.*not be determined|Cannot find the program \'(.+)\'\. User-specified/;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the regex that will cause MissingToolError and matches on various outputs from the wrapper.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, this is a work-around until haskell/haskell-language-server#2591 lands?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess

context: ExtensionContext,
storagePath: string,
logger: Logger,
targetGhc?: string
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

targetGhc is undefined for the first pass, where we just want the hls wrapper and don't care about hls exe.

throw new Error(noMatchingHLS);
} else {
const latestMetadataHls = await getLatestHLSfromMetadata(context, storagePath, projectGhc, logger);
const latestGhcupHls = await getLatestHLSfromGHCup(context, storagePath, logger, projectGhc).then(e => e === null ? null : e[0]);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where we get a possibly self-compiled already installed HLS.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worthy not: we only consider the latest self-compiled...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is another edge case: if you compile HLS 1.6.1.0 manually instead of via bindist, the supported GHC versions differ from the metadata.

I'm not sure that's worth to support explicitly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worthy not: we only consider the latest self-compiled...

What does that mean? If I have compiled version 1.6.1.0, and then I compile version 1.5.0.0, does that mean version 1.5.0.0 is used (since it is the most recent compiled HLS version), or that no matter what, we only use 1.6.1.0 and if there is no version 1.6.1.0, we would not use the self compiled version 1.5.0.0 either? Does this take into account what the latest supported HLS version relative to a GHC version is? E.g. when 8.10.4 supports only up to 1.4.0.0, and I compile the version 1.5.0.0 for it (because that's possible in theory) would the extension pick that up correctly?

Copy link
Member Author

@hasufell hasufell Mar 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If 1.6.1.0 is the latest properly released one and you have 1.7.0.0 and 1.8.0.0 compiled, the extension will only bother looking at 1.8.0.0.

There are a number of complicated interactions here, e.g. it's possible that you compiled 1.6.1.0 as well and it doesn't support the project GHC, because it's only built for one GHC (unlike the bindist).

So we have a Map of HLS ver -> GHC versions.

A way out here would be this:

  1. Map A: gather all ghcup-installed HLSes and their corresponding supported GHC version, regardless of the HLS metadata file
  2. Map B: gather HLS metadata
  3. merge both this way:
    • if a HLS version exists both in A and B, only consider GHC versions from A (because ghcup install hls <ver> would not overwrite the already existing one... and the existing one might have less supported GHC versions)
    • if a HLS version exists only in A, pick A
    • if a HLS version exists only in B, pick B

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm starting to think this functionality belongs into ghcup. It is non-trivial.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is non-trivial. Adding it to ghcup directly sounds like a nice idea!

Copy link
Collaborator

@fendor fendor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, haven't had a chance to test everything out, though

@@ -346,10 +384,15 @@ export async function getProjectGHCVersion(
logger.error(`stdout: ${stdout}`);
}
// Error message emitted by HLS-wrapper
const regex = /Cradle requires (.+) but couldn't find it/;
const regex = /Cradle requires (.+) but couldn't find it|The program \'(.+)\' version .* is required but the version of.*could.*not be determined|Cannot find the program \'(.+)\'\. User-specified/;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, this is a work-around until haskell/haskell-language-server#2591 lands?

throw new Error(noMatchingHLS);
} else {
const latestMetadataHls = await getLatestHLSfromMetadata(context, storagePath, projectGhc, logger);
const latestGhcupHls = await getLatestHLSfromGHCup(context, storagePath, logger, projectGhc).then(e => e === null ? null : e[0]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worthy not: we only consider the latest self-compiled...

What does that mean? If I have compiled version 1.6.1.0, and then I compile version 1.5.0.0, does that mean version 1.5.0.0 is used (since it is the most recent compiled HLS version), or that no matter what, we only use 1.6.1.0 and if there is no version 1.6.1.0, we would not use the self compiled version 1.5.0.0 either? Does this take into account what the latest supported HLS version relative to a GHC version is? E.g. when 8.10.4 supports only up to 1.4.0.0, and I compile the version 1.5.0.0 for it (because that's possible in theory) would the extension pick that up correctly?

@hasufell hasufell marked this pull request as draft March 14, 2022 21:13
@hasufell hasufell marked this pull request as ready for review March 14, 2022 23:17
Comment on lines 333 to 358
// first we get supported GHC versions from available HLS bindists (whether installed or not)
const metadataMap = await getHLSesfromMetadata(context, storagePath, logger);
// then we get supported GHC versions from currently installed HLS versions
const ghcupMap = await getHLSesFromGHCup(context, storagePath, logger);
// since installed HLS versions may support a different set of GHC versions than the bindists
// (e.g. because the user ran 'ghcup compile hls'), we need to merge both maps, preferring
// values from already installed HLSes
const merged = (metadataMap === null)
? ghcupMap
: ((ghcupMap === null)
? null
: (new Map<string, string[]>([...metadataMap, ...ghcupMap]))); // right-biased

if (!merged) {
window.showErrorMessage(noMatchingHLS);
throw new Error(noMatchingHLS);
} else {
return latestMetadataHls;
// now sort and get the latest suitable version
const latest = [...merged].filter(([k, v]) => v.some(x => x === projectGhc)).sort(([k1, v1], [k2, v2]) => comparePVP(k1, k2)).pop();
if (latest) {
return latest[0];

} else {
window.showErrorMessage(noMatchingHLS);
throw new Error(noMatchingHLS);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the juice... we merge the maps.

And correctly merge all those maps.
Copy link
Collaborator

@fendor fendor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the changes are good as-is, but we need to make sure the documentation clearly explains all the possible user interactions.

E.g. the options system-ghcup, internal-ghcup and PATH are quite unclear to absolute beginners.

@fendor fendor merged commit 950e02f into haskell:master Mar 17, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Consider user installed (e.g. via ghcup compile) HLSes
3 participants