Skip to content

Commit 778df39

Browse files
committed
Extract metadata into deedicated file
1 parent 391ce94 commit 778df39

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 } 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`);
@@ -117,11 +115,11 @@ export async function findHaskellLanguageServer(
117115
fs.mkdirSync(storagePath);
118116
}
119117

120-
// first plugin initialization
118+
// first extension initialization
121119
manageHLS = await promptUserForManagingHls(context, manageHLS);
122120

123121
if (manageHLS === 'PATH') {
124-
const exe = findHlsInPath(context, logger);
122+
const exe = findHlsInPath(logger);
125123
return {
126124
location: exe,
127125
tag: 'path',
@@ -243,7 +241,7 @@ export async function findHaskellLanguageServer(
243241
// now figure out the actual project GHC version and the latest supported HLS version
244242
// we need for it (e.g. this might in fact be a downgrade for old GHCs)
245243
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);
247245
if (projectHls === undefined) {
248246
projectHls = res[0];
249247
}
@@ -352,10 +350,10 @@ async function promptUserForManagingHls(context: ExtensionContext, manageHlsSett
352350
}
353351
}
354352

355-
async function getLatestProjectHLS(
353+
async function getLatestProjectHls(
356354
ghcup: GHCup,
357-
context: ExtensionContext,
358355
logger: Logger,
356+
storagePath: string,
359357
workingDir: string,
360358
toolchainBindir: string,
361359
): Promise<[string, string]> {
@@ -371,7 +369,7 @@ async function getLatestProjectHLS(
371369
: await callAsync(`ghc${exeExt}`, ['--numeric-version'], logger, undefined, undefined, false);
372370

373371
// 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[]>();
375373
// then we get supported GHC versions from currently installed HLS versions
376374
const ghcupMap = (await findAvailableHlsBinariesFromGHCup(ghcup)) || new Map<string, string[]>();
377375
// since installed HLS versions may support a different set of GHC versions than the bindists
@@ -394,7 +392,7 @@ async function getLatestProjectHLS(
394392
/**
395393
* Obtain the project ghc version from the HLS - Wrapper (which must be in PATH now).
396394
* 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)
398396
* @param workingDir Directory to run the process, usually the root of the workspace.
399397
* @param logger Logger for feedback.
400398
* @returns The GHC version, or fail with an `Error`.
@@ -507,197 +505,6 @@ async function toolInstalled(ghcup: GHCup, tool: Tool, version: string): Promise
507505
return new InstalledTool(tool, version, b);
508506
}
509507

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-
701508
/**
702509
* Tracks the name, version and installation state of tools we need.
703510
*/

0 commit comments

Comments
 (0)