234 lines
7.1 KiB
TypeScript
234 lines
7.1 KiB
TypeScript
import * as core from '@actions/core';
|
|
import {exec} from '@actions/exec';
|
|
import {which} from '@actions/io';
|
|
import {create as glob} from '@actions/glob';
|
|
import * as tc from '@actions/tool-cache';
|
|
import {promises as fs} from 'fs';
|
|
import {join} from 'path';
|
|
import type {OS, Tool} from './opts';
|
|
|
|
function failed(tool: Tool, version: string): void {
|
|
throw new Error(`All install methods for ${tool} ${version} failed`);
|
|
}
|
|
|
|
async function success(
|
|
tool: Tool,
|
|
version: string,
|
|
path: string
|
|
): Promise<true> {
|
|
core.addPath(path);
|
|
core.setOutput(`${tool}-path`, path);
|
|
core.setOutput(`${tool}-exe`, await which(tool));
|
|
core.info(
|
|
`Found ${tool} ${version} in cache at path ${path}. Setup successful.`
|
|
);
|
|
return true;
|
|
}
|
|
|
|
function warn(tool: Tool, version: string): void {
|
|
const policy = {
|
|
cabal: `the two latest major releases of ${tool} are commonly supported.`,
|
|
ghc: `the three latest major releases of ${tool} are commonly supported.`,
|
|
stack: `the latest release of ${tool} is commonly supported.`
|
|
}[tool];
|
|
|
|
core.warning(
|
|
`${tool} ${version} was not found in the cache. It will be downloaded.\n` +
|
|
`If this is unexpected, please check if version ${version} is pre-installed.\n` +
|
|
`The list of pre-installed versions is available here: https://help.github.com/en/actions/reference/software-installed-on-github-hosted-runners\n` +
|
|
`The above list follows a common haskell convention that ${policy}\n` +
|
|
'If the list is outdated, please file an issue here: https://github.com/actions/virtual-environments\n' +
|
|
'by using the appropriate tool request template: https://github.com/actions/virtual-environments/issues/new/choose'
|
|
);
|
|
}
|
|
|
|
async function isInstalled(
|
|
tool: Tool,
|
|
version: string,
|
|
os: OS
|
|
): Promise<boolean> {
|
|
const toolPath = tc.find(tool, version);
|
|
if (toolPath) return success(tool, version, toolPath);
|
|
|
|
const ghcupPath = `${process.env.HOME}/.ghcup${
|
|
tool === 'ghc' ? `/ghc/${version}` : ''
|
|
}/bin`;
|
|
const v = tool === 'cabal' ? version.slice(0, 3) : version;
|
|
const aptPath = `/opt/${tool}/${v}/bin`;
|
|
|
|
const chocoPath = getChocoPath(tool, version);
|
|
|
|
const locations = {
|
|
stack: [], // Always installed into the tool cache
|
|
cabal: {
|
|
win32: [chocoPath],
|
|
linux: [aptPath],
|
|
darwin: []
|
|
}[os],
|
|
ghc: {
|
|
win32: [chocoPath],
|
|
linux: [aptPath, ghcupPath],
|
|
darwin: [ghcupPath]
|
|
}[os]
|
|
};
|
|
|
|
for (const p of locations[tool]) {
|
|
const installedPath = await fs
|
|
.access(p)
|
|
.then(() => p)
|
|
.catch(() => undefined);
|
|
|
|
if (installedPath) {
|
|
// Make sure that the correct ghc is used, even if ghcup has set a
|
|
// default prior to this action being ran.
|
|
if (tool === 'ghc' && installedPath === ghcupPath)
|
|
await exec(await ghcupBin(os), ['set', version]);
|
|
|
|
return success(tool, version, installedPath);
|
|
}
|
|
}
|
|
|
|
if (tool === 'cabal' && os !== 'win32') {
|
|
const installedPath = await fs
|
|
.access(`${ghcupPath}/cabal`)
|
|
.then(() => ghcupPath)
|
|
.catch(() => undefined);
|
|
|
|
if (installedPath) return success(tool, version, installedPath);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export async function installTool(
|
|
tool: Tool,
|
|
version: string,
|
|
os: OS
|
|
): Promise<void> {
|
|
if (await isInstalled(tool, version, os)) return;
|
|
warn(tool, version);
|
|
|
|
if (tool === 'stack') {
|
|
await stack(version, os);
|
|
if (await isInstalled(tool, version, os)) return;
|
|
return failed(tool, version);
|
|
}
|
|
|
|
switch (os) {
|
|
case 'linux':
|
|
await apt(tool, version);
|
|
if (await isInstalled(tool, version, os)) return;
|
|
await ghcup(tool, version, os);
|
|
break;
|
|
case 'win32':
|
|
await choco(tool, version);
|
|
break;
|
|
case 'darwin':
|
|
await ghcup(tool, version, os);
|
|
break;
|
|
}
|
|
|
|
if (await isInstalled(tool, version, os)) return;
|
|
return failed(tool, version);
|
|
}
|
|
|
|
async function stack(version: string, os: OS): Promise<void> {
|
|
core.info(`Attempting to install stack ${version}`);
|
|
const build = {
|
|
linux: `linux-x86_64${version >= '2.3.1' ? '' : '-static'}`,
|
|
darwin: 'osx-x86_64',
|
|
win32: 'windows-x86_64'
|
|
}[os];
|
|
|
|
const url = `https://github.com/commercialhaskell/stack/releases/download/v${version}/stack-${version}-${build}.tar.gz`;
|
|
const p = await tc.downloadTool(`${url}`).then(tc.extractTar);
|
|
const [stackPath] = await glob(`${p}/stack*`, {
|
|
implicitDescendants: false
|
|
}).then(async g => g.glob());
|
|
await tc.cacheDir(stackPath, 'stack', version);
|
|
|
|
if (os === 'win32') core.exportVariable('STACK_ROOT', 'C:\\sr');
|
|
}
|
|
|
|
async function apt(tool: Tool, version: string): Promise<void> {
|
|
const toolName = tool === 'ghc' ? 'ghc' : 'cabal-install';
|
|
const v = tool === 'cabal' ? version.slice(0, 3) : version;
|
|
core.info(`Attempting to install ${toolName} ${v} using apt-get`);
|
|
// Ignore the return code so we can fall back to ghcup
|
|
await exec(`sudo -- sh -c "apt-get -y install ${toolName}-${v}"`, undefined, {
|
|
ignoreReturnCode: true
|
|
});
|
|
}
|
|
|
|
async function choco(tool: Tool, version: string): Promise<void> {
|
|
core.info(`Attempting to install ${tool} ${version} using chocolatey`);
|
|
// Choco tries to invoke `add-path` command on earlier versions of ghc, which has been deprecated and fails the step, so disable command execution during this.
|
|
console.log('::stop-commands::SetupHaskellStopCommands');
|
|
await exec(
|
|
'powershell',
|
|
[
|
|
'choco',
|
|
'install',
|
|
tool,
|
|
'--version',
|
|
version,
|
|
'-m',
|
|
'--no-progress',
|
|
'-r'
|
|
],
|
|
{
|
|
ignoreReturnCode: true
|
|
}
|
|
);
|
|
console.log('::SetupHaskellStopCommands::'); // Re-enable command execution
|
|
// Add GHC to path automatically because it does not add until the end of the step and we check the path.
|
|
if (tool == 'ghc') {
|
|
core.addPath(getChocoPath(tool, version));
|
|
}
|
|
}
|
|
|
|
async function ghcupBin(os: OS): Promise<string> {
|
|
const v = '0.1.8';
|
|
const cachedBin = tc.find('ghcup', v);
|
|
if (cachedBin) return join(cachedBin, 'ghcup');
|
|
|
|
const bin = await tc.downloadTool(
|
|
`https://downloads.haskell.org/ghcup/${v}/x86_64-${
|
|
os === 'darwin' ? 'apple-darwin' : 'linux'
|
|
}-ghcup-${v}`
|
|
);
|
|
await fs.chmod(bin, 0o755);
|
|
return join(await tc.cacheFile(bin, 'ghcup', 'ghcup', v), 'ghcup');
|
|
}
|
|
|
|
async function ghcup(tool: Tool, version: string, os: OS): Promise<void> {
|
|
core.info(`Attempting to install ${tool} ${version} using ghcup`);
|
|
const bin = await ghcupBin(os);
|
|
const returnCode = await exec(
|
|
bin,
|
|
[tool === 'ghc' ? 'install' : 'install-cabal', version],
|
|
{
|
|
ignoreReturnCode: true
|
|
}
|
|
);
|
|
if (returnCode === 0 && tool === 'ghc') await exec(bin, ['set', version]);
|
|
}
|
|
|
|
function getChocoPath(tool: Tool, version: string): string {
|
|
// Manually add the path because it won't happen until the end of the step normally
|
|
const pathArray = version.split('.');
|
|
const pathVersion =
|
|
pathArray.length > 3
|
|
? pathArray.slice(0, pathArray.length - 1).join('.')
|
|
: pathArray.join('.');
|
|
const chocoPath = join(
|
|
`${process.env.ChocolateyInstall}`,
|
|
'lib',
|
|
`${tool}.${version}`,
|
|
'tools',
|
|
tool === 'ghc' ? `${tool}-${pathVersion}` : `${tool}-${version}`, // choco trims the ghc version here
|
|
tool === 'ghc' ? 'bin' : ''
|
|
);
|
|
return chocoPath;
|
|
}
|