Skip to content

Commit 2fa7a9e

Browse files
authored
Merge pull request #506 from mduerig/454
support old hls versions compatible with the requested ghc version
2 parents 627b1da + 560b8e4 commit 2fa7a9e

File tree

3 files changed

+63
-26
lines changed

3 files changed

+63
-26
lines changed

.github/workflows/test.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ jobs:
1111
strategy:
1212
matrix:
1313
os: [macos-11, ubuntu-latest, windows-latest]
14+
ghc: [9.0.1]
15+
include:
16+
# To test a ghc version deprecated in newer hls versions
17+
- os: ubuntu-latest
18+
ghc: 8.10.4
1419
runs-on: ${{ matrix.os }}
1520
steps:
1621
- name: Checkout
@@ -22,7 +27,7 @@ jobs:
2227
- name: Ensure there is a supported ghc versions
2328
uses: haskell/actions/setup@v1
2429
with:
25-
ghc-version: 9.0.1
30+
ghc-version: ${{ matrix.ghc }}
2631
- run: npm ci
2732
- run: npm run webpack
2833
- run: xvfb-run -s '-screen 0 640x480x16' -a npm test

src/hlsBinaries.ts

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const releaseValidator: validate.Validator<IRelease> = validate.object({
3737

3838
const githubReleaseApiValidator: validate.Validator<IRelease[]> = validate.array(releaseValidator);
3939

40-
const cachedReleaseValidator: validate.Validator<IRelease | null> = validate.optional(releaseValidator);
40+
const cachedReleaseValidator: validate.Validator<IRelease[] | null> = validate.optional(githubReleaseApiValidator);
4141

4242
// On Windows the executable needs to be stored somewhere with an .exe extension
4343
const exeExt = process.platform === 'win32' ? '.exe' : '';
@@ -85,7 +85,7 @@ class NoBinariesError extends Error {
8585
const supportedReleasesLink =
8686
'[See the list of supported versions here](https://github.com/haskell/vscode-haskell#supported-ghc-versions)';
8787
if (ghcVersion) {
88-
super(`haskell-language-server ${hlsVersion} for GHC ${ghcVersion} is not available on ${os.type()}.
88+
super(`haskell-language-server ${hlsVersion} or earlier for GHC ${ghcVersion} is not available on ${os.type()}.
8989
${supportedReleasesLink}`);
9090
} else {
9191
super(`haskell-language-server ${hlsVersion} is not available on ${os.type()}.
@@ -205,7 +205,11 @@ async function getProjectGhcVersion(
205205
return callWrapper(downloadedWrapper);
206206
}
207207

208-
async function getLatestReleaseMetadata(context: ExtensionContext, storagePath: string): Promise<IRelease | null> {
208+
async function getReleaseMetadata(
209+
context: ExtensionContext,
210+
storagePath: string,
211+
logger: Logger
212+
): Promise<IRelease[] | null> {
209213
const releasesUrl = workspace.getConfiguration('haskell').releasesURL
210214
? url.parse(workspace.getConfiguration('haskell').releasesURL)
211215
: undefined;
@@ -219,9 +223,28 @@ async function getLatestReleaseMetadata(context: ExtensionContext, storagePath:
219223
path: '/repos/haskell/haskell-language-server/releases',
220224
};
221225

222-
const offlineCache = path.join(storagePath, 'latestApprovedRelease.cache.json');
226+
const offlineCache = path.join(storagePath, 'approvedReleases.cache.json');
227+
const offlineCacheOldFormat = path.join(storagePath, 'latestApprovedRelease.cache.json');
223228

224-
async function readCachedReleaseData(): Promise<IRelease | null> {
229+
// Migrate existing old cache file latestApprovedRelease.cache.json to the new cache file
230+
// approvedReleases.cache.json if no such file exists yet.
231+
if (!fs.existsSync(offlineCache)) {
232+
try {
233+
const oldCachedInfo = await promisify(fs.readFile)(offlineCacheOldFormat, { encoding: 'utf-8' });
234+
const oldCachedInfoParsed = validate.parseAndValidate(oldCachedInfo, validate.optional(releaseValidator));
235+
if (oldCachedInfoParsed !== null) {
236+
await promisify(fs.writeFile)(offlineCache, JSON.stringify([oldCachedInfoParsed]), { encoding: 'utf-8' });
237+
}
238+
logger.info(`Successfully migrated ${offlineCacheOldFormat} to ${offlineCache}`);
239+
} catch (err: any) {
240+
// Ignore if old cache file does not exist
241+
if (err.code !== 'ENOENT') {
242+
logger.error(`Failed to migrate ${offlineCacheOldFormat} to ${offlineCache}: ${err}`);
243+
}
244+
}
245+
}
246+
247+
async function readCachedReleaseData(): Promise<IRelease[] | null> {
225248
try {
226249
const cachedInfo = await promisify(fs.readFile)(offlineCache, { encoding: 'utf-8' });
227250
return validate.parseAndValidate(cachedInfo, cachedReleaseValidator);
@@ -242,15 +265,16 @@ async function getLatestReleaseMetadata(context: ExtensionContext, storagePath:
242265

243266
try {
244267
const releaseInfo = await httpsGetSilently(opts);
245-
const latestInfoParsed =
246-
validate.parseAndValidate(releaseInfo, githubReleaseApiValidator).find((x) => !x.prerelease) || null;
268+
const releaseInfoParsed =
269+
validate.parseAndValidate(releaseInfo, githubReleaseApiValidator).filter((x) => !x.prerelease) || null;
247270

248271
if (updateBehaviour === 'prompt') {
249272
const cachedInfoParsed = await readCachedReleaseData();
250273

251274
if (
252-
latestInfoParsed !== null &&
253-
(cachedInfoParsed === null || latestInfoParsed.tag_name !== cachedInfoParsed.tag_name)
275+
releaseInfoParsed !== null && releaseInfoParsed.length > 0 &&
276+
(cachedInfoParsed === null || cachedInfoParsed.length === 0
277+
|| releaseInfoParsed[0].tag_name !== cachedInfoParsed[0].tag_name)
254278
) {
255279
const promptMessage =
256280
cachedInfoParsed === null
@@ -266,8 +290,8 @@ async function getLatestReleaseMetadata(context: ExtensionContext, storagePath:
266290
}
267291

268292
// Cache the latest successfully fetched release information
269-
await promisify(fs.writeFile)(offlineCache, JSON.stringify(latestInfoParsed), { encoding: 'utf-8' });
270-
return latestInfoParsed;
293+
await promisify(fs.writeFile)(offlineCache, JSON.stringify(releaseInfoParsed), { encoding: 'utf-8' });
294+
return releaseInfoParsed;
271295
} catch (githubError: any) {
272296
// Attempt to read from the latest cached file
273297
try {
@@ -316,8 +340,8 @@ export async function downloadHaskellLanguageServer(
316340
}
317341

318342
logger.info('Fetching the latest release from GitHub or from cache');
319-
const release = await getLatestReleaseMetadata(context, storagePath);
320-
if (!release) {
343+
const releases = await getReleaseMetadata(context, storagePath, logger);
344+
if (!releases) {
321345
let message = "Couldn't find any pre-built haskell-language-server binaries";
322346
const updateBehaviour = workspace.getConfiguration('haskell').get('updateBehavior') as UpdateBehaviour;
323347
if (updateBehaviour === 'never-check') {
@@ -326,12 +350,12 @@ export async function downloadHaskellLanguageServer(
326350
window.showErrorMessage(message);
327351
return null;
328352
}
329-
logger.info(`The latest release is ${release.tag_name}`);
353+
logger.info(`The latest release is ${releases[0].tag_name}`);
330354
logger.info('Figure out the ghc version to use or advertise an installation link for missing components');
331355
const dir: string = folder?.uri?.fsPath ?? path.dirname(resource.fsPath);
332356
let ghcVersion: string;
333357
try {
334-
ghcVersion = await getProjectGhcVersion(context, logger, dir, release, storagePath);
358+
ghcVersion = await getProjectGhcVersion(context, logger, dir, releases[0], storagePath);
335359
} catch (error) {
336360
if (error instanceof MissingToolError) {
337361
const link = error.installLink();
@@ -354,29 +378,37 @@ export async function downloadHaskellLanguageServer(
354378
// When searching for binaries, use startsWith because the compression may differ
355379
// between .zip and .gz
356380
const assetName = `haskell-language-server-${githubOS}-${ghcVersion}${exeExt}`;
357-
logger.info(`Search for binary ${assetName} in release assests`);
381+
logger.info(`Search for binary ${assetName} in release assets`);
382+
const release = releases?.find(r => r.assets.find((x) => x.name.startsWith(assetName)));
358383
const asset = release?.assets.find((x) => x.name.startsWith(assetName));
359384
if (!asset) {
360385
logger.error(
361-
`No binary ${assetName} found in the release assets: ${release?.assets.map((value) => value.name).join(',')}`
386+
`No binary ${assetName} found in the release assets`
362387
);
363-
window.showInformationMessage(new NoBinariesError(release.tag_name, ghcVersion).message);
388+
window.showInformationMessage(new NoBinariesError(releases[0].tag_name, ghcVersion).message);
364389
return null;
365390
}
366391

367-
const serverName = `haskell-language-server-${release.tag_name}-${process.platform}-${ghcVersion}${exeExt}`;
392+
const serverName = `haskell-language-server-${release?.tag_name}-${process.platform}-${ghcVersion}${exeExt}`;
368393
const binaryDest = path.join(storagePath, serverName);
369394

370-
const title = `Downloading haskell-language-server ${release.tag_name} for GHC ${ghcVersion}`;
395+
const title = `Downloading haskell-language-server ${release?.tag_name} for GHC ${ghcVersion}`;
371396
logger.info(title);
372-
await downloadFile(title, asset.browser_download_url, binaryDest);
397+
const downloaded = await downloadFile(title, asset.browser_download_url, binaryDest);
373398
if (ghcVersion.startsWith('9.')) {
374399
const warning =
375400
'Currently, HLS supports GHC 9 only partially. ' +
376401
'See [issue #297](https://github.com/haskell/haskell-language-server/issues/297) for more detail.';
377402
logger.warn(warning);
378403
window.showWarningMessage(warning);
379404
}
405+
if (release?.tag_name !== releases[0].tag_name) {
406+
const warning = `haskell-language-server ${releases[0].tag_name} for GHC ${ghcVersion} is not available on ${os.type()}. Falling back to haskell-language-server ${release?.tag_name}`;
407+
logger.warn(warning);
408+
if (downloaded) {
409+
window.showInformationMessage(warning);
410+
}
411+
}
380412
return binaryDest;
381413
}
382414

src/utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ const userAgentHeader = { 'User-Agent': 'vscode-haskell' };
9595
* equality is by reference, not value in Map. And we are using a tuple of
9696
* [src, dest] as the key.
9797
*/
98-
const inFlightDownloads = new Map<string, Map<string, Thenable<void>>>();
98+
const inFlightDownloads = new Map<string, Map<string, Thenable<boolean>>>();
9999

100100
export async function httpsGetSilently(options: https.RequestOptions): Promise<string> {
101101
const opts: https.RequestOptions = {
@@ -141,7 +141,7 @@ async function ignoreFileNotExists(err: NodeJS.ErrnoException): Promise<void> {
141141
throw err;
142142
}
143143

144-
export async function downloadFile(titleMsg: string, src: string, dest: string): Promise<void> {
144+
export async function downloadFile(titleMsg: string, src: string, dest: string): Promise<boolean> {
145145
// Check to see if we're already in the process of downloading the same thing
146146
const inFlightDownload = inFlightDownloads.get(src)?.get(dest);
147147
if (inFlightDownload) {
@@ -150,7 +150,7 @@ export async function downloadFile(titleMsg: string, src: string, dest: string):
150150

151151
// If it already is downloaded just use that
152152
if (fs.existsSync(dest)) {
153-
return;
153+
return false;
154154
}
155155

156156
// Download it to a .tmp location first, then rename it!
@@ -241,7 +241,7 @@ export async function downloadFile(titleMsg: string, src: string, dest: string):
241241
inFlightDownloads.get(src)?.delete(dest);
242242
}
243243
}
244-
);
244+
).then(_ => true);
245245

246246
try {
247247
if (inFlightDownloads.has(src)) {

0 commit comments

Comments
 (0)