1
1
import * as fs from 'fs' ;
2
- import * as https from 'https' ;
3
2
import * as path from 'path' ;
4
- import { match } from 'ts-pattern' ;
5
- import { promisify } from 'util' ;
6
3
import { ConfigurationTarget , ExtensionContext , window , workspace , WorkspaceFolder } from 'vscode' ;
7
4
import { Logger } from 'vscode-languageclient' ;
8
5
import { HlsError , MissingToolError , NoMatchingHls } from './errors' ;
@@ -11,18 +8,19 @@ import {
11
8
callAsync ,
12
9
comparePVP ,
13
10
executableExists ,
14
- httpsGetSilently ,
15
11
IEnvVars ,
16
12
resolvePathPlaceHolders ,
17
13
} from './utils' ;
18
14
import { ToolConfig , Tool , initDefaultGHCup , GHCup } from './ghcup' ;
15
+ import { getHlsMetadata } from './metadata' ;
19
16
export { IEnvVars } ;
20
17
21
18
type ManageHLS = 'GHCup' | 'PATH' ;
22
19
let manageHLS = workspace . getConfiguration ( 'haskell' ) . get ( 'manageHLS' ) as ManageHLS ;
23
20
24
21
export type Context = {
25
22
manageHls : ManageHLS ;
23
+ storagePath : string ;
26
24
serverExecutable ?: HlsExecutable ;
27
25
logger : Logger ;
28
26
} ;
@@ -47,7 +45,7 @@ function findServerExecutable(logger: Logger, folder?: WorkspaceFolder): string
47
45
48
46
/** Searches the PATH. Fails if nothing is found.
49
47
*/
50
- function findHlsInPath ( _context : ExtensionContext , logger : Logger ) : string {
48
+ function findHlsInPath ( logger : Logger ) : string {
51
49
// try PATH
52
50
const exes : string [ ] = [ 'haskell-language-server-wrapper' , 'haskell-language-server' ] ;
53
51
logger . info ( `Searching for server executables ${ exes . join ( ',' ) } in $PATH` ) ;
@@ -117,11 +115,11 @@ export async function findHaskellLanguageServer(
117
115
fs . mkdirSync ( storagePath ) ;
118
116
}
119
117
120
- // first plugin initialization
118
+ // first extension initialization
121
119
manageHLS = await promptUserForManagingHls ( context , manageHLS ) ;
122
120
123
121
if ( manageHLS === 'PATH' ) {
124
- const exe = findHlsInPath ( context , logger ) ;
122
+ const exe = findHlsInPath ( logger ) ;
125
123
return {
126
124
location : exe ,
127
125
tag : 'path' ,
@@ -243,7 +241,7 @@ export async function findHaskellLanguageServer(
243
241
// now figure out the actual project GHC version and the latest supported HLS version
244
242
// we need for it (e.g. this might in fact be a downgrade for old GHCs)
245
243
if ( projectHls === undefined || projectGhc === undefined ) {
246
- const res = await getLatestProjectHLS ( ghcup , context , logger , workingDir , latestToolchainBindir ) ;
244
+ const res = await getLatestProjectHls ( ghcup , logger , storagePath , workingDir , latestToolchainBindir ) ;
247
245
if ( projectHls === undefined ) {
248
246
projectHls = res [ 0 ] ;
249
247
}
@@ -352,10 +350,10 @@ async function promptUserForManagingHls(context: ExtensionContext, manageHlsSett
352
350
}
353
351
}
354
352
355
- async function getLatestProjectHLS (
353
+ async function getLatestProjectHls (
356
354
ghcup : GHCup ,
357
- context : ExtensionContext ,
358
355
logger : Logger ,
356
+ storagePath : string ,
359
357
workingDir : string ,
360
358
toolchainBindir : string ,
361
359
) : Promise < [ string , string ] > {
@@ -371,7 +369,7 @@ async function getLatestProjectHLS(
371
369
: await callAsync ( `ghc${ exeExt } ` , [ '--numeric-version' ] , logger , undefined , undefined , false ) ;
372
370
373
371
// first we get supported GHC versions from available HLS bindists (whether installed or not)
374
- const metadataMap = ( await getHlsMetadata ( context , logger ) ) || new Map < string , string [ ] > ( ) ;
372
+ const metadataMap = ( await getHlsMetadata ( storagePath , logger ) ) || new Map < string , string [ ] > ( ) ;
375
373
// then we get supported GHC versions from currently installed HLS versions
376
374
const ghcupMap = ( await findAvailableHlsBinariesFromGHCup ( ghcup ) ) || new Map < string , string [ ] > ( ) ;
377
375
// since installed HLS versions may support a different set of GHC versions than the bindists
@@ -394,7 +392,7 @@ async function getLatestProjectHLS(
394
392
/**
395
393
* Obtain the project ghc version from the HLS - Wrapper (which must be in PATH now).
396
394
* Also, serves as a sanity check.
397
- * @param toolchainBindir Path to the toolchainn bin directory (added to PATH)
395
+ * @param toolchainBindir Path to the toolchain bin directory (added to PATH)
398
396
* @param workingDir Directory to run the process, usually the root of the workspace.
399
397
* @param logger Logger for feedback.
400
398
* @returns The GHC version, or fail with an `Error`.
@@ -507,197 +505,6 @@ async function toolInstalled(ghcup: GHCup, tool: Tool, version: string): Promise
507
505
return new InstalledTool ( tool , version , b ) ;
508
506
}
509
507
510
- /**
511
- * Metadata of release information.
512
- *
513
- * Example of the expected format:
514
- *
515
- * ```
516
- * {
517
- * "1.6.1.0": {
518
- * "A_64": {
519
- * "Darwin": [
520
- * "8.10.6",
521
- * ],
522
- * "Linux_Alpine": [
523
- * "8.10.7",
524
- * "8.8.4",
525
- * ],
526
- * },
527
- * "A_ARM": {
528
- * "Linux_UnknownLinux": [
529
- * "8.10.7"
530
- * ]
531
- * },
532
- * "A_ARM64": {
533
- * "Darwin": [
534
- * "8.10.7"
535
- * ],
536
- * "Linux_UnknownLinux": [
537
- * "8.10.7"
538
- * ]
539
- * }
540
- * }
541
- * }
542
- * ```
543
- *
544
- * consult [ghcup metadata repo](https://github.com/haskell/ghcup-metadata/) for details.
545
- */
546
- export type ReleaseMetadata = Map < string , Map < string , Map < string , string [ ] > > > ;
547
-
548
- /**
549
- * Compute Map of supported HLS versions for this platform.
550
- * Fetches HLS metadata information.
551
- *
552
- * @param context Context of the extension, required for metadata.
553
- * @param logger Logger for feedback
554
- * @returns Map of supported HLS versions or null if metadata could not be fetched.
555
- */
556
- async function getHlsMetadata ( context : ExtensionContext , logger : Logger ) : Promise < Map < string , string [ ] > | null > {
557
- const storagePath : string = getStoragePath ( context ) ;
558
- const metadata = await getReleaseMetadata ( storagePath , logger ) . catch ( ( ) => null ) ;
559
- if ( ! metadata ) {
560
- window . showErrorMessage ( 'Could not get release metadata' ) ;
561
- return null ;
562
- }
563
- const plat : Platform | null = match ( process . platform )
564
- . with ( 'darwin' , ( ) => 'Darwin' as Platform )
565
- . with ( 'linux' , ( ) => 'Linux_UnknownLinux' as Platform )
566
- . with ( 'win32' , ( ) => 'Windows' as Platform )
567
- . with ( 'freebsd' , ( ) => 'FreeBSD' as Platform )
568
- . otherwise ( ( ) => null ) ;
569
- if ( plat === null ) {
570
- throw new Error ( `Unknown platform ${ process . platform } ` ) ;
571
- }
572
- const arch : Arch | null = match ( process . arch )
573
- . with ( 'arm' , ( ) => 'A_ARM' as Arch )
574
- . with ( 'arm64' , ( ) => 'A_ARM64' as Arch )
575
- . with ( 'ia32' , ( ) => 'A_32' as Arch )
576
- . with ( 'x64' , ( ) => 'A_64' as Arch )
577
- . otherwise ( ( ) => null ) ;
578
- if ( arch === null ) {
579
- throw new Error ( `Unknown architecture ${ process . arch } ` ) ;
580
- }
581
-
582
- return findSupportedHlsPerGhc ( plat , arch , metadata , logger ) ;
583
- }
584
-
585
- export type Platform = 'Darwin' | 'Linux_UnknownLinux' | 'Windows' | 'FreeBSD' ;
586
-
587
- export type Arch = 'A_ARM' | 'A_ARM64' | 'A_32' | 'A_64' ;
588
-
589
- /**
590
- * Find all supported GHC versions per HLS version supported on the given
591
- * platform and architecture.
592
- * @param platform Platform of the host.
593
- * @param arch Arch of the host.
594
- * @param metadata HLS Metadata information.
595
- * @param logger Logger.
596
- * @returns Map from HLS version to GHC versions that are supported.
597
- */
598
- export function findSupportedHlsPerGhc (
599
- platform : Platform ,
600
- arch : Arch ,
601
- metadata : ReleaseMetadata ,
602
- logger : Logger ,
603
- ) : Map < string , string [ ] > {
604
- logger . info ( `Platform constants: ${ platform } , ${ arch } ` ) ;
605
- const newMap = new Map < string , string [ ] > ( ) ;
606
- metadata . forEach ( ( supportedArch , hlsVersion ) => {
607
- const supportedOs = supportedArch . get ( arch ) ;
608
- if ( supportedOs ) {
609
- const ghcSupportedOnOs = supportedOs . get ( platform ) ;
610
- if ( ghcSupportedOnOs ) {
611
- logger . log ( `HLS ${ hlsVersion } compatible with GHC Versions: ${ ghcSupportedOnOs . join ( ',' ) } ` ) ;
612
- // copy supported ghc versions to avoid unintended modifications
613
- newMap . set ( hlsVersion , [ ...ghcSupportedOnOs ] ) ;
614
- }
615
- }
616
- } ) ;
617
-
618
- return newMap ;
619
- }
620
-
621
- /**
622
- * Download GHCUP metadata.
623
- *
624
- * @param storagePath Path to put in binary files and caches.
625
- * @param logger Logger for feedback.
626
- * @returns Metadata of releases, or null if the cache can not be found.
627
- */
628
- async function getReleaseMetadata ( storagePath : string , logger : Logger ) : Promise < ReleaseMetadata | null > {
629
- const releasesUrl = workspace . getConfiguration ( 'haskell' ) . releasesURL
630
- ? new URL ( workspace . getConfiguration ( 'haskell' ) . releasesURL as string )
631
- : undefined ;
632
- const opts : https . RequestOptions = releasesUrl
633
- ? {
634
- host : releasesUrl . host ,
635
- path : releasesUrl . pathname ,
636
- }
637
- : {
638
- host : 'raw.githubusercontent.com' ,
639
- path : '/haskell/ghcup-metadata/master/hls-metadata-0.0.1.json' ,
640
- } ;
641
-
642
- const offlineCache = path . join ( storagePath , 'ghcupReleases.cache.json' ) ;
643
-
644
- /**
645
- * Convert a json value to ReleaseMetadata.
646
- * Assumes the json is well-formed and a valid Release-Metadata.
647
- * @param someObj Release Metadata without any typing information but well-formed.
648
- * @returns Typed ReleaseMetadata.
649
- */
650
- const objectToMetadata = ( someObj : any ) : ReleaseMetadata => {
651
- const obj = someObj as [ string : [ string : [ string : string [ ] ] ] ] ;
652
- const hlsMetaEntries = Object . entries ( obj ) . map ( ( [ hlsVersion , archMap ] ) => {
653
- const archMetaEntries = Object . entries ( archMap ) . map ( ( [ arch , supportedGhcVersionsPerOs ] ) => {
654
- return [ arch , new Map ( Object . entries ( supportedGhcVersionsPerOs ) ) ] as [ string , Map < string , string [ ] > ] ;
655
- } ) ;
656
- return [ hlsVersion , new Map ( archMetaEntries ) ] as [ string , Map < string , Map < string , string [ ] > > ] ;
657
- } ) ;
658
- return new Map ( hlsMetaEntries ) ;
659
- } ;
660
-
661
- async function readCachedReleaseData ( ) : Promise < ReleaseMetadata | null > {
662
- try {
663
- logger . info ( `Reading cached release data at ${ offlineCache } ` ) ;
664
- const cachedInfo = await promisify ( fs . readFile ) ( offlineCache , { encoding : 'utf-8' } ) ;
665
- // export type ReleaseMetadata = Map<string, Map<string, Map<string, string[]>>>;
666
- const value : any = JSON . parse ( cachedInfo ) ;
667
- return objectToMetadata ( value ) ;
668
- } catch ( err : any ) {
669
- // If file doesn't exist, return null, otherwise consider it a failure
670
- if ( err . code === 'ENOENT' ) {
671
- logger . warn ( `No cached release data found at ${ offlineCache } ` ) ;
672
- return null ;
673
- }
674
- throw err ;
675
- }
676
- }
677
-
678
- try {
679
- const releaseInfo = await httpsGetSilently ( opts ) ;
680
- const releaseInfoParsed = JSON . parse ( releaseInfo ) ;
681
-
682
- // Cache the latest successfully fetched release information
683
- await promisify ( fs . writeFile ) ( offlineCache , JSON . stringify ( releaseInfoParsed ) , { encoding : 'utf-8' } ) ;
684
- return objectToMetadata ( releaseInfoParsed ) ;
685
- } catch ( githubError : any ) {
686
- // Attempt to read from the latest cached file
687
- try {
688
- const cachedInfoParsed = await readCachedReleaseData ( ) ;
689
-
690
- window . showWarningMessage (
691
- "Couldn't get the latest haskell-language-server releases from GitHub, used local cache instead: " +
692
- githubError . message ,
693
- ) ;
694
- return cachedInfoParsed ;
695
- } catch ( _fileError ) {
696
- throw new Error ( "Couldn't get the latest haskell-language-server releases from GitHub: " + githubError . message ) ;
697
- }
698
- }
699
- }
700
-
701
508
/**
702
509
* Tracks the name, version and installation state of tools we need.
703
510
*/
0 commit comments