Skip to content

Commit a65d4df

Browse files
committed
Extract metadata into dedicated file
1 parent 6382e2d commit a65d4df

File tree

2 files changed

+208
-203
lines changed

2 files changed

+208
-203
lines changed

src/hlsBinaries.ts

Lines changed: 10 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import * as fs from 'fs';
2-
import * as https from 'https';
32
import * as path from 'path';
4-
import { match } from 'ts-pattern';
5-
import { promisify } from 'util';
63
import { ConfigurationTarget, ExtensionContext, window, workspace, WorkspaceFolder } from 'vscode';
74
import { Logger } from 'vscode-languageclient';
85
import { 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';
1814
import { ToolConfig, Tool, initDefaultGHCup, GHCup, GHCupConfig } from './ghcup';
15+
import { getHlsMetadata } from './metadata';
1916
export { IEnvVars };
2017

2118
type ManageHLS = 'GHCup' | 'PATH';
2219
let manageHLS = workspace.getConfiguration('haskell').get('manageHLS') as ManageHLS;
2320

2421
export 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

Comments
 (0)