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