11import * as fs from 'fs' ;
2- import * as https from 'https' ;
32import * as path from 'path' ;
4- import { match } from 'ts-pattern' ;
5- import { promisify } from 'util' ;
63import { ConfigurationTarget , ExtensionContext , window , workspace , WorkspaceFolder } from 'vscode' ;
74import { Logger } from 'vscode-languageclient' ;
85import { HlsError , MissingToolError , NoMatchingHls } from './errors' ;
@@ -11,18 +8,19 @@ import {
118 callAsync ,
129 comparePVP ,
1310 executableExists ,
14- httpsGetSilently ,
1511 IEnvVars ,
1612 resolvePathPlaceHolders ,
1713} from './utils' ;
1814import { ToolConfig , Tool , initDefaultGHCup , GHCup , GHCupConfig } from './ghcup' ;
15+ import { getHlsMetadata } from './metadata' ;
1916export { IEnvVars } ;
2017
2118type ManageHLS = 'GHCup' | 'PATH' ;
2219let manageHLS = workspace . getConfiguration ( 'haskell' ) . get ( 'manageHLS' ) as ManageHLS ;
2320
2421export type Context = {
2522 manageHls : ManageHLS ;
23+ storagePath : string ;
2624 serverExecutable ?: HlsExecutable ;
2725 logger : Logger ;
2826} ;
@@ -47,7 +45,7 @@ function findServerExecutable(logger: Logger, folder?: WorkspaceFolder): string
4745
4846/** Searches the PATH. Fails if nothing is found.
4947 */
50- function findHlsInPath ( _context : ExtensionContext , logger : Logger ) : string {
48+ function findHlsInPath ( logger : Logger ) : string {
5149 // try PATH
5250 const exes : string [ ] = [ 'haskell-language-server-wrapper' , 'haskell-language-server' ] ;
5351 logger . info ( `Searching for server executables ${ exes . join ( ',' ) } in $PATH` ) ;
@@ -118,11 +116,11 @@ export async function findHaskellLanguageServer(
118116 fs . mkdirSync ( storagePath ) ;
119117 }
120118
121- // first plugin initialization
119+ // first extension initialization
122120 manageHLS = await promptUserForManagingHls ( context , manageHLS ) ;
123121
124122 if ( manageHLS === 'PATH' ) {
125- const exe = findHlsInPath ( context , logger ) ;
123+ const exe = findHlsInPath ( logger ) ;
126124 return {
127125 location : exe ,
128126 tag : 'path' ,
@@ -244,7 +242,7 @@ export async function findHaskellLanguageServer(
244242 // now figure out the actual project GHC version and the latest supported HLS version
245243 // we need for it (e.g. this might in fact be a downgrade for old GHCs)
246244 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 ) ;
248246 if ( projectHls === undefined ) {
249247 projectHls = res [ 0 ] ;
250248 }
@@ -353,10 +351,10 @@ async function promptUserForManagingHls(context: ExtensionContext, manageHlsSett
353351 }
354352}
355353
356- async function getLatestProjectHLS (
354+ async function getLatestProjectHls (
357355 ghcup : GHCup ,
358- context : ExtensionContext ,
359356 logger : Logger ,
357+ storagePath : string ,
360358 workingDir : string ,
361359 toolchainBindir : string ,
362360) : Promise < [ string , string ] > {
@@ -372,7 +370,7 @@ async function getLatestProjectHLS(
372370 : await callAsync ( `ghc${ exeExt } ` , [ '--numeric-version' ] , logger , undefined , undefined , false ) ;
373371
374372 // 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 [ ] > ( ) ;
376374 // then we get supported GHC versions from currently installed HLS versions
377375 const ghcupMap = ( await findAvailableHlsBinariesFromGHCup ( ghcup ) ) || new Map < string , string [ ] > ( ) ;
378376 // since installed HLS versions may support a different set of GHC versions than the bindists
@@ -395,7 +393,7 @@ async function getLatestProjectHLS(
395393/**
396394 * Obtain the project ghc version from the HLS - Wrapper (which must be in PATH now).
397395 * 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)
399397 * @param workingDir Directory to run the process, usually the root of the workspace.
400398 * @param logger Logger for feedback.
401399 * @returns The GHC version, or fail with an `Error`.
@@ -508,197 +506,6 @@ async function toolInstalled(ghcup: GHCup, tool: Tool, version: string): Promise
508506 return new InstalledTool ( tool , version , b ) ;
509507}
510508
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-
702509/**
703510 * Tracks the name, version and installation state of tools we need.
704511 */
0 commit comments