Skip to content

Commit e111eca

Browse files
committed
Move ghcup specific code into its own file
1 parent de6b6b5 commit e111eca

File tree

6 files changed

+340
-330
lines changed

6 files changed

+340
-330
lines changed

src/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { OutputChannel, Uri, window, WorkspaceConfiguration, WorkspaceFolder } from 'vscode';
2-
import { expandHomeDir, ExtensionLogger, IEnvVars } from './utils';
2+
import { expandHomeDir, IEnvVars } from './utils';
33
import * as path from 'path';
44
import { Logger } from 'vscode-languageclient';
5+
import { ExtensionLogger } from './logger';
56

67
export type LogLevel = 'off' | 'messages' | 'verbose';
78
export type ClientLogLevel = 'off' | 'error' | 'info' | 'debug';

src/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
import { RestartServerCommandName, StartServerCommandName, StopServerCommandName } from './commands/constants';
1111
import * as DocsBrowser from './docsBrowser';
1212
import { HlsError, MissingToolError, NoMatchingHls } from './errors';
13-
import { callAsync, findHaskellLanguageServer, HlsExecutable, IEnvVars } from './hlsBinaries';
14-
import { addPathToProcessPath, comparePVP } from './utils';
13+
import { findHaskellLanguageServer, HlsExecutable, IEnvVars } from './hlsBinaries';
14+
import { addPathToProcessPath, comparePVP, callAsync } from './utils';
1515
import { Config, initConfig, initLoggerFromConfig, logConfig } from './config';
1616

1717
// The current map of documents & folders to language servers.

src/ghcup.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import * as path from 'path';
2+
import * as os from 'os';
3+
import * as process from 'process';
4+
import { workspace, WorkspaceFolder } from 'vscode';
5+
import { Logger } from 'vscode-languageclient';
6+
import { MissingToolError } from './errors';
7+
import { resolvePathPlaceHolders, executableExists, callAsync, ProcessCallback } from './utils';
8+
import { match } from 'ts-pattern';
9+
10+
export type Tool = 'hls' | 'ghc' | 'cabal' | 'stack';
11+
12+
export type ToolConfig = Map<Tool, string>;
13+
14+
export async function callGHCup(
15+
logger: Logger,
16+
args: string[],
17+
title?: string,
18+
cancellable?: boolean,
19+
callback?: ProcessCallback,
20+
): Promise<string> {
21+
const metadataUrl = workspace.getConfiguration('haskell').metadataURL;
22+
const ghcup = findGHCup(logger);
23+
return await callAsync(
24+
ghcup,
25+
['--no-verbose'].concat(metadataUrl ? ['-s', metadataUrl] : []).concat(args),
26+
logger,
27+
undefined,
28+
title,
29+
cancellable,
30+
{
31+
// omit colourful output because the logs are uglier
32+
NO_COLOR: '1',
33+
},
34+
callback,
35+
);
36+
}
37+
38+
export async function upgradeGHCup(logger: Logger): Promise<void> {
39+
const upgrade = workspace.getConfiguration('haskell').get('upgradeGHCup') as boolean;
40+
if (upgrade) {
41+
await callGHCup(logger, ['upgrade'], 'Upgrading ghcup', true);
42+
}
43+
}
44+
45+
export function findGHCup(logger: Logger, folder?: WorkspaceFolder): string {
46+
logger.info('Checking for ghcup installation');
47+
let exePath = workspace.getConfiguration('haskell').get('ghcupExecutablePath') as string;
48+
if (exePath) {
49+
logger.info(`Trying to find the ghcup executable in: ${exePath}`);
50+
exePath = resolvePathPlaceHolders(exePath, folder);
51+
logger.log(`Location after path variables substitution: ${exePath}`);
52+
if (executableExists(exePath)) {
53+
return exePath;
54+
} else {
55+
throw new Error(`Could not find a ghcup binary at ${exePath}!`);
56+
}
57+
} else {
58+
const localGHCup = ['ghcup'].find(executableExists);
59+
if (!localGHCup) {
60+
logger.info(`probing for GHCup binary`);
61+
const ghcupExe: string | null = match(process.platform)
62+
.with('win32', () => {
63+
const ghcupPrefix = process.env.GHCUP_INSTALL_BASE_PREFIX;
64+
if (ghcupPrefix) {
65+
return path.join(ghcupPrefix, 'ghcup', 'bin', 'ghcup.exe');
66+
} else {
67+
return path.join('C:\\', 'ghcup', 'bin', 'ghcup.exe');
68+
}
69+
})
70+
.otherwise(() => {
71+
const useXDG = process.env.GHCUP_USE_XDG_DIRS;
72+
if (useXDG) {
73+
const xdgBin = process.env.XDG_BIN_HOME;
74+
if (xdgBin) {
75+
return path.join(xdgBin, 'ghcup');
76+
} else {
77+
return path.join(os.homedir(), '.local', 'bin', 'ghcup');
78+
}
79+
} else {
80+
const ghcupPrefix = process.env.GHCUP_INSTALL_BASE_PREFIX;
81+
if (ghcupPrefix) {
82+
return path.join(ghcupPrefix, '.ghcup', 'bin', 'ghcup');
83+
} else {
84+
return path.join(os.homedir(), '.ghcup', 'bin', 'ghcup');
85+
}
86+
}
87+
});
88+
if (ghcupExe !== null && executableExists(ghcupExe)) {
89+
return ghcupExe;
90+
} else {
91+
logger.warn(`ghcup at ${ghcupExe} does not exist`);
92+
throw new MissingToolError('ghcup');
93+
}
94+
} else {
95+
logger.info(`found ghcup at ${localGHCup}`);
96+
return localGHCup;
97+
}
98+
}
99+
}
100+
101+
// the tool might be installed or not
102+
export async function getLatestToolFromGHCup(logger: Logger, tool: Tool): Promise<string> {
103+
// these might be custom/stray/compiled, so we try first
104+
const installedVersions = await callGHCup(logger, ['list', '-t', tool, '-c', 'installed', '-r'], undefined, false);
105+
const latestInstalled = installedVersions.split(/\r?\n/).pop();
106+
if (latestInstalled) {
107+
return latestInstalled.split(/\s+/)[1];
108+
}
109+
110+
return getLatestAvailableToolFromGHCup(logger, tool);
111+
}
112+
113+
export async function getLatestAvailableToolFromGHCup(
114+
logger: Logger,
115+
tool: Tool,
116+
tag?: string,
117+
criteria?: string,
118+
): Promise<string> {
119+
// fall back to installable versions
120+
const availableVersions = await callGHCup(
121+
logger,
122+
['list', '-t', tool, '-c', criteria ? criteria : 'available', '-r'],
123+
undefined,
124+
false,
125+
).then((s) => s.split(/\r?\n/));
126+
127+
let latestAvailable: string | null = null;
128+
availableVersions.forEach((ver) => {
129+
if (
130+
ver
131+
.split(/\s+/)[2]
132+
.split(',')
133+
.includes(tag ? tag : 'latest')
134+
) {
135+
latestAvailable = ver.split(/\s+/)[1];
136+
}
137+
});
138+
if (!latestAvailable) {
139+
throw new Error(`Unable to find ${tag ? tag : 'latest'} tool ${tool}`);
140+
} else {
141+
return latestAvailable;
142+
}
143+
}

0 commit comments

Comments
 (0)