@@ -4,8 +4,13 @@ import * as fs from 'fs';
4
4
import * as path from 'path' ;
5
5
import { ExtensionContext , ProgressLocation , Uri , window , workspace } from 'vscode' ;
6
6
import { Logger } from 'vscode-languageclient' ;
7
- import { downloadFile , executableExists , resolvePathPlaceHolders } from './utils' ;
7
+ import { httpsGetSilently , downloadFile , executableExists , resolvePathPlaceHolders } from './utils' ;
8
8
import { match } from 'ts-pattern' ;
9
+ import * as url from 'url' ;
10
+ import * as https from 'https' ;
11
+ import { promisify } from 'util' ;
12
+
13
+ export type ReleaseMetadata = Map < string , Map < string , Map < string , string [ ] > > > ;
9
14
10
15
// Used for environment variables later on
11
16
export interface IEnvVars {
@@ -134,7 +139,7 @@ async function callAsync(
134
139
* Downloads the latest haskell-language-server binaries via ghcup.
135
140
* Returns null if it can't find any match.
136
141
*/
137
- export async function downloadHaskellLanguageServer ( context : ExtensionContext , logger : Logger ) : Promise < string > {
142
+ export async function downloadHaskellLanguageServer ( context : ExtensionContext , logger : Logger , workingDir : string ) : Promise < string > {
138
143
logger . info ( 'Downloading haskell-language-server' ) ;
139
144
140
145
const storagePath : string = await getStoragePath ( context ) ;
@@ -179,62 +184,94 @@ export async function downloadHaskellLanguageServer(context: ExtensionContext, l
179
184
}
180
185
await callAsync (
181
186
ghcup ,
182
- [ '--no-verbose' , 'install' , 'hls' , '--set' , ' latest'] ,
187
+ [ '--no-verbose' , 'install' , 'hls' , 'latest' ] ,
183
188
storagePath ,
184
189
logger ,
185
190
`Installing latest HLS` ,
186
191
true ,
187
192
{ GHCUP_INSTALL_BASE_PREFIX : storagePath }
188
193
) ;
194
+ await callAsync (
195
+ ghcup ,
196
+ [ '--no-verbose' , 'set' , 'hls' , 'latest' ] ,
197
+ storagePath ,
198
+ logger ,
199
+ undefined ,
200
+ false ,
201
+ { GHCUP_INSTALL_BASE_PREFIX : storagePath }
202
+ ) ;
189
203
return downloadedWrapper ;
190
204
} else {
191
- const args = [ '--numeric-version' ] ;
192
- const version = await callAsync ( wrapper , args , storagePath , logger ) ;
193
205
194
- const args2 = [ '--no-verbose' , 'list' , '-t' , 'hls' , '-c' , 'available' , '-r' ] ;
195
- const hls_versions = await callAsync ( ghcup , args2 , storagePath , logger , undefined , false , { GHCUP_INSTALL_BASE_PREFIX : storagePath } ) ;
206
+ // version of active hls wrapper
207
+ const set_version = await callAsync ( wrapper , [ '--numeric-version' ] , storagePath , logger ) ;
208
+
209
+ // get latest hls version
210
+ const hls_versions = await callAsync ( ghcup , [ '--no-verbose' , 'list' , '-t' , 'hls' , '-c' , 'available' , '-r' ] , storagePath , logger , undefined , false , { GHCUP_INSTALL_BASE_PREFIX : storagePath } ) ;
196
211
const latest_hls_version = hls_versions . split ( / \r ? \n / ) . pop ( ) ! . split ( ' ' ) [ 1 ] ;
197
212
198
- const cmp = comparePVP ( version , latest_hls_version ) ;
199
- if ( cmp < 0 ) {
213
+ // get project GHC version
214
+ const project_ghc = await getProjectGHCVersion ( wrapper , workingDir , logger ) ;
215
+
216
+ // get installable HLS that supports the project GHC version (this might not be the most recent)
217
+ const latest_metadata_hls = ( project_ghc != null ) ? await getLatestHLSforGHC ( context , storagePath , project_ghc , logger ) : null ;
218
+ const installable_hls = ( latest_metadata_hls != null ) ? latest_metadata_hls : latest_hls_version ;
219
+
220
+ const downgrade : boolean = comparePVP ( latest_hls_version , installable_hls ) > 0 ;
221
+
222
+ const projectGhcWrapper = path . join (
223
+ storagePath ,
224
+ '.ghcup' ,
225
+ 'bin' ,
226
+ `haskell-language-server-wrapper-${ project_ghc } ${ exeExt } `
227
+ ) ;
228
+ const need_install = ! executableExists ( path . join ( projectGhcWrapper ) ) ;
229
+
230
+ if ( comparePVP ( set_version , installable_hls ) != 0 ) {
200
231
// only update if the user wants to
201
232
if ( updateBehaviour === 'never-check' ) {
202
233
logger . warn (
203
234
"As 'haskell.updateBehaviour' config option is set to 'never-check' " +
204
235
'we try to use the possibly obsolete cached release data'
205
236
) ;
206
237
return wrapper ;
207
- } else if ( updateBehaviour === 'prompt' ) {
208
- const promptMessage =
209
- 'A new version of the haskell-language-server is available, would you like to upgrade now?' ;
238
+ } else if ( updateBehaviour === 'prompt' && need_install ) {
239
+ let promptMessage : string ;
240
+ if ( downgrade ) {
241
+ promptMessage = `A different (lower) version of the haskell-language-server is required to support ${ project_ghc } , would you like to upgrade now?` ;
242
+
243
+ } else {
244
+ promptMessage = 'A new version of the haskell-language-server is available, would you like to upgrade now?' ;
245
+ }
210
246
211
247
const decision = await window . showInformationMessage ( promptMessage , 'Download' , 'Nevermind' ) ;
212
248
if ( decision !== 'Download' ) {
213
249
return wrapper ;
214
250
}
251
+ } else {
252
+ if ( downgrade && need_install ) {
253
+ const decision = await window . showInformationMessage ( `Cannot install the latest HLS version ${ latest_hls_version } , because it does not support GHC ${ project_ghc } . Installing HLS ${ installable_hls } instead?` , 'Continue' , "Abort" ) ;
254
+ if ( decision !== 'Continue' ) {
255
+ return wrapper ;
256
+ }
257
+ }
215
258
}
216
259
217
- // there's a new version
218
- // delete old HLS
219
- await callAsync (
220
- ghcup ,
221
- [ '--no-verbose' , 'rm' , 'hls' , version ] ,
222
- storagePath ,
223
- logger ,
224
- `Removing old HLS ${ version } ` ,
225
- false ,
226
- { GHCUP_INSTALL_BASE_PREFIX : storagePath }
227
- ) ;
228
- // install new hls
260
+ // we use this command to both install a HLS, but also create a nice
261
+ // isolated symlinked dir with only the given HLS in place, so
262
+ // this works for installing and setting
263
+ const symHLSPath = path . join ( storagePath , 'hls' , installable_hls ) ;
229
264
await callAsync (
230
265
ghcup ,
231
- [ '--no-verbose' , 'install' , 'hls' , '--set' , latest_hls_version ] ,
266
+ [ '--no-verbose' , 'run' , '--hls' , installable_hls
267
+ , '-b' , symHLSPath , '-i' ] ,
232
268
storagePath ,
233
269
logger ,
234
- `Upgrading HLS to ${ latest_hls_version } ` ,
235
- true ,
270
+ need_install ? `Installing HLS ${ installable_hls } ` : undefined ,
271
+ need_install ,
236
272
{ GHCUP_INSTALL_BASE_PREFIX : storagePath }
237
273
) ;
274
+ return path . join ( symHLSPath , `haskell-language-server-wrapper${ exeExt } ` ) ;
238
275
}
239
276
return wrapper ;
240
277
}
@@ -393,3 +430,115 @@ export function addPathToProcessPath(path: string): string {
393
430
return PATH . join ( pathSep ) ;
394
431
}
395
432
433
+ async function getLatestHLSforGHC (
434
+ context : ExtensionContext ,
435
+ storagePath : string ,
436
+ targetGhc : string ,
437
+ logger : Logger
438
+ ) : Promise < string | null > {
439
+ const metadata = await getReleaseMetadata ( context , storagePath , logger ) ;
440
+ if ( metadata === null ) {
441
+ window . showErrorMessage ( `Could not get release metadata` ) ;
442
+ return null ;
443
+ }
444
+ const plat = match ( process . platform )
445
+ . with ( 'darwin' , ( _ ) => 'Darwin' )
446
+ . with ( 'linux' , ( _ ) => 'Linux_UnknownLinux' )
447
+ . with ( 'win32' , ( _ ) => 'Windows' )
448
+ . with ( 'freebsd' , ( _ ) => 'FreeBSD' )
449
+ . otherwise ( ( _ ) => null ) ;
450
+ if ( plat === null ) {
451
+ window . showErrorMessage ( `Unknown platform ${ process . platform } ` ) ;
452
+ return null ;
453
+ }
454
+ const arch = match ( process . arch )
455
+ . with ( 'arm' , ( _ ) => 'A_ARM' )
456
+ . with ( 'arm64' , ( _ ) => 'A_ARM64' )
457
+ . with ( 'x32' , ( _ ) => 'A_32' )
458
+ . with ( 'x64' , ( _ ) => 'A_64' )
459
+ . otherwise ( ( _ ) => null ) ;
460
+ if ( arch === null ) {
461
+ window . showErrorMessage ( `Unknown architecture ${ process . arch } ` ) ;
462
+ return null ;
463
+ }
464
+
465
+ let cur_hls : string | null = null ;
466
+
467
+ const map : ReleaseMetadata = new Map ( Object . entries ( metadata ) ) ;
468
+ map . forEach ( ( value , key ) => {
469
+ const value_ = new Map ( Object . entries ( value ) ) ;
470
+ const archValues = new Map ( Object . entries ( value_ . get ( arch ) ) ) ;
471
+ const versions : string [ ] = archValues . get ( plat ) as string [ ] ;
472
+ if ( versions != undefined && versions . some ( ( el , _ix , _arr ) => el === targetGhc ) ) {
473
+ if ( cur_hls == null ) {
474
+ cur_hls = key ;
475
+ } else if ( comparePVP ( key , cur_hls ) > 0 ) {
476
+ cur_hls = key ;
477
+ }
478
+ }
479
+ } ) ;
480
+
481
+ logger . info ( `cur_hls: ${ cur_hls } ` ) ;
482
+ return cur_hls ;
483
+ }
484
+
485
+ async function getReleaseMetadata (
486
+ context : ExtensionContext ,
487
+ storagePath : string ,
488
+ logger : Logger
489
+ ) : Promise < ReleaseMetadata | null > {
490
+ const releasesUrl = workspace . getConfiguration ( 'haskell' ) . releasesURL
491
+ ? url . parse ( workspace . getConfiguration ( 'haskell' ) . releasesURL )
492
+ : undefined ;
493
+ const opts : https . RequestOptions = releasesUrl
494
+ ? {
495
+ host : releasesUrl . host ,
496
+ path : releasesUrl . path ,
497
+ }
498
+ : {
499
+ host : 'gist.githubusercontent.com' ,
500
+ path : '/hasufell/dd84df5f81a3a7e6e6fad8f122dba429/raw/73efc1078555d971076d3ccf31154f10ed683a82/hls-metadata.json' ,
501
+ } ;
502
+
503
+ const offlineCache = path . join ( storagePath , 'ghcupReleases.cache.json' ) ;
504
+
505
+ async function readCachedReleaseData ( ) : Promise < ReleaseMetadata | null > {
506
+ try {
507
+ logger . info ( `Reading cached release data at ${ offlineCache } ` ) ;
508
+ const cachedInfo = await promisify ( fs . readFile ) ( offlineCache , { encoding : 'utf-8' } ) ;
509
+ // export type ReleaseMetadata = Map<string, Map<string, Map<string, string[]>>>;
510
+ const value : ReleaseMetadata = JSON . parse ( cachedInfo ) ;
511
+ return value ;
512
+ } catch ( err : any ) {
513
+ // If file doesn't exist, return null, otherwise consider it a failure
514
+ if ( err . code === 'ENOENT' ) {
515
+ logger . warn ( `No cached release data found at ${ offlineCache } ` ) ;
516
+ return null ;
517
+ }
518
+ throw err ;
519
+ }
520
+ }
521
+
522
+ try {
523
+ const releaseInfo = await httpsGetSilently ( opts ) ;
524
+ const releaseInfoParsed = JSON . parse ( releaseInfo ) ;
525
+
526
+ // Cache the latest successfully fetched release information
527
+ await promisify ( fs . writeFile ) ( offlineCache , JSON . stringify ( releaseInfoParsed ) , { encoding : 'utf-8' } ) ;
528
+ return releaseInfoParsed ;
529
+ } catch ( githubError : any ) {
530
+ // Attempt to read from the latest cached file
531
+ try {
532
+ const cachedInfoParsed = await readCachedReleaseData ( ) ;
533
+
534
+ window . showWarningMessage (
535
+ "Couldn't get the latest haskell-language-server releases from GitHub, used local cache instead: " +
536
+ githubError . message
537
+ ) ;
538
+ return cachedInfoParsed ;
539
+ } catch ( fileError ) {
540
+ throw new Error ( "Couldn't get the latest haskell-language-server releases from GitHub: " +
541
+ githubError . message ) ;
542
+ }
543
+ }
544
+ }
0 commit comments