@@ -37,7 +37,7 @@ const releaseValidator: validate.Validator<IRelease> = validate.object({
3737
3838const 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
4343const 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
0 commit comments