Add Windows and macOS Support (#1)
* Re-base onto upstream master * Use os over operating-system per json schema suggestion * Upgrade dependencies * Update action.yml syntax * Restructure repo on latest typescript-action * Update workflow.yml to use same node version as action.yml * Pull in improvements from open PRs. Fix package.json scripts * Type function return arguments * Don't eslint automatic formatting changes * Initial implementation of ghc+cabal install for linux/mac/windows * Update README * Fix cabal version in action.yml * Implement initial simplistic test suite * Test action with CI * Use chocolatey directly to install ghc and cabal * Try pre-installed tool on linux first * Clean up documentation * Expand README * Test super old GHC on ubuntu * Implement support for stack * Update documentation about Stack support * Test stack install in Github Actions CI authored-by: Jared Weakly <jweakly@galois.com>
This commit is contained in:
169
src/installer.ts
169
src/installer.ts
@@ -1,38 +1,153 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {exec} from '@actions/exec';
|
||||
import * as io from '@actions/io';
|
||||
import * as tc from '@actions/tool-cache';
|
||||
import {promises as fs, readFileSync} from 'fs';
|
||||
import {safeLoad} from 'js-yaml';
|
||||
import {join} from 'path';
|
||||
|
||||
export function findHaskellGHCVersion(baseInstallDir: string, version: string) {
|
||||
return _findHaskellToolVersion(baseInstallDir, 'ghc', version);
|
||||
export interface ProgramOpt {
|
||||
enable: boolean;
|
||||
version: string;
|
||||
install: (version: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function findHaskellCabalVersion(
|
||||
baseInstallDir: string,
|
||||
version: string
|
||||
) {
|
||||
return _findHaskellToolVersion(baseInstallDir, 'cabal', version);
|
||||
export interface Options {
|
||||
ghc: ProgramOpt;
|
||||
cabal: ProgramOpt;
|
||||
stack: ProgramOpt & {setup: boolean};
|
||||
}
|
||||
|
||||
export function _findHaskellToolVersion(
|
||||
baseInstallDir: string,
|
||||
tool: string,
|
||||
version: string
|
||||
) {
|
||||
if (!baseInstallDir) {
|
||||
throw new Error('baseInstallDir parameter is required');
|
||||
}
|
||||
if (!tool) {
|
||||
throw new Error('toolName parameter is required');
|
||||
}
|
||||
if (!version) {
|
||||
throw new Error('versionSpec parameter is required');
|
||||
export interface Defaults {
|
||||
ghc: {version: string};
|
||||
cabal: {version: string};
|
||||
}
|
||||
|
||||
export function getDefaults(): Defaults {
|
||||
const actionYml = safeLoad(
|
||||
readFileSync(join(__dirname, '..', 'action.yml'), 'utf8')
|
||||
);
|
||||
return {
|
||||
ghc: {version: actionYml.inputs['ghc-version'].default},
|
||||
cabal: {version: actionYml.inputs['cabal-version'].default}
|
||||
};
|
||||
}
|
||||
|
||||
export function getOpts(def: Defaults): Options {
|
||||
const stackNoGlobal = core.getInput('stack-no-global') !== '';
|
||||
const stackVersion = core.getInput('stack-version');
|
||||
const stackSetupGhc = core.getInput('stack-setup-ghc') !== '';
|
||||
|
||||
const errors = [];
|
||||
if (stackNoGlobal && stackVersion === '') {
|
||||
errors.push('stack-version is required if stack-no-global is set');
|
||||
}
|
||||
|
||||
const toolPath: string = path.join(baseInstallDir, tool, version, 'bin');
|
||||
if (fs.existsSync(toolPath)) {
|
||||
core.debug(`Found tool in cache ${tool} ${version}`);
|
||||
core.addPath(toolPath);
|
||||
if (stackSetupGhc && stackVersion === '') {
|
||||
errors.push('stack-version is required if stack-setup-ghc is set');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join('\n'));
|
||||
}
|
||||
|
||||
return {
|
||||
ghc: {
|
||||
version: core.getInput('ghc-version') || def.ghc.version,
|
||||
enable: !stackNoGlobal,
|
||||
install: installGHC
|
||||
},
|
||||
cabal: {
|
||||
version: core.getInput('cabal-version') || def.cabal.version,
|
||||
enable: !stackNoGlobal,
|
||||
install: installCabal
|
||||
},
|
||||
stack: {
|
||||
version: stackVersion,
|
||||
enable: stackVersion !== '',
|
||||
install: installStack,
|
||||
setup: core.getInput('stack-setup-ghc') !== ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const installCabal = async (version: string): Promise<void> =>
|
||||
installTool('cabal', version);
|
||||
|
||||
export const installGHC = async (version: string): Promise<void> =>
|
||||
installTool('ghc', version);
|
||||
|
||||
export async function installStack(version: string): Promise<void> {
|
||||
const info =
|
||||
version === 'latest'
|
||||
? 'Installing the latest version'
|
||||
: `Installing version ${version}`;
|
||||
core.startGroup(`${info} of stack`);
|
||||
const platformMap = ({
|
||||
linux: 'linux-x86_64-static',
|
||||
darwin: 'osx-x86_64',
|
||||
win32: 'windows-x86_64'
|
||||
} as unknown) as Record<NodeJS.Platform, string>;
|
||||
|
||||
const name = `stack-${version}-${platformMap[process.platform]}`;
|
||||
const url =
|
||||
version === 'latest'
|
||||
? `get.haskellstack.org/stable/${platformMap[process.platform]}`
|
||||
: `github.com/commercialhaskell/stack/releases/download/v${version}/${name}`;
|
||||
|
||||
const stack = await tc.downloadTool(`https://${url}.tar.gz`);
|
||||
const p = await tc.extractTar(stack);
|
||||
|
||||
// Less janky than figuring out how to ./p/*/stack. (Not by much)
|
||||
const stackPath =
|
||||
(await fs.readdir(p, {withFileTypes: true}))
|
||||
.flatMap(d => (d.isDirectory() ? [d.name] : []))
|
||||
.find(f => f.startsWith('stack')) ?? '';
|
||||
|
||||
const cachedTool = await tc.cacheDir(join(p, stackPath), 'stack', version);
|
||||
core.addPath(cachedTool);
|
||||
|
||||
core.endGroup();
|
||||
}
|
||||
|
||||
type Tool = 'cabal' | 'ghc';
|
||||
async function installTool(tool: Tool, version: string): Promise<void> {
|
||||
core.startGroup(`Installing ${tool}`);
|
||||
// Currently only linux comes pre-installed with some versions of GHC.
|
||||
// They're intalled to /opt. Let's see if we can save ourselves a download
|
||||
if (process.platform === 'linux') {
|
||||
// Cabal is installed to /opt/cabal/x.x but cabal's full version is X.X.Y.Z
|
||||
const v = tool === 'cabal' ? version.slice(0, 3) : version;
|
||||
try {
|
||||
const p = join('/opt', tool, v, 'bin');
|
||||
await fs.access(p);
|
||||
core.debug(`Using pre-installed ${tool} ${version}`);
|
||||
core.addPath(p);
|
||||
core.endGroup();
|
||||
return;
|
||||
} catch {
|
||||
// oh well, we tried
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const cmd = ['choco', 'install', tool, '--version', version];
|
||||
const flags = ['-m', '--no-progress', '-r'];
|
||||
await exec('powershell', cmd.concat(flags));
|
||||
|
||||
const t = `${tool}.${version}`;
|
||||
const p = ['lib', t, 'tools', t, tool === 'ghc' ? 'bin' : ''];
|
||||
core.addPath(join(process.env.ChocolateyInstall || '', ...p));
|
||||
} else {
|
||||
throw new Error(`Version ${version} of ${tool} not found`);
|
||||
const ghcup = await tc.downloadTool(
|
||||
'https://raw.githubusercontent.com/haskell/ghcup/master/ghcup'
|
||||
);
|
||||
await fs.chmod(ghcup, 0o755);
|
||||
await io.mkdirP(join(process.env.HOME || '', '.ghcup', 'bin'));
|
||||
await exec(ghcup, [tool === 'ghc' ? 'install' : 'install-cabal', version]);
|
||||
|
||||
const p = tool === 'ghc' ? ['ghc', version] : [];
|
||||
core.addPath(join(process.env.HOME || '', '.ghcup', ...p, 'bin'));
|
||||
}
|
||||
core.endGroup();
|
||||
}
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import * as core from '@actions/core';
|
||||
import {findHaskellGHCVersion, findHaskellCabalVersion} from './installer';
|
||||
import {getOpts, getDefaults} from './installer';
|
||||
import {exec} from '@actions/exec';
|
||||
|
||||
// ghc and cabal are installed directly to /opt so use that directlly instead of
|
||||
// copying over to the toolcache dir.
|
||||
const baseInstallDir = '/opt';
|
||||
const defaultGHCVersion = '8.6.5';
|
||||
const defaultCabalVersion = '3.0';
|
||||
|
||||
async function run() {
|
||||
(async () => {
|
||||
try {
|
||||
let ghcVersion = core.getInput('ghc-version');
|
||||
if (!ghcVersion) {
|
||||
ghcVersion = defaultGHCVersion;
|
||||
}
|
||||
findHaskellGHCVersion(baseInstallDir, ghcVersion);
|
||||
const opts = getOpts(getDefaults());
|
||||
core.info('Preparing to setup a Haskell environment');
|
||||
core.debug(`Options are: ${JSON.stringify(opts)}`);
|
||||
|
||||
let cabalVersion = core.getInput('cabal-version');
|
||||
if (!cabalVersion) {
|
||||
cabalVersion = defaultCabalVersion;
|
||||
for (const [tool, o] of Object.entries(opts)) {
|
||||
if (o.enable) {
|
||||
core.info(`Installing ${tool} version ${o.version}`);
|
||||
await o.install(o.version);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.stack.setup) {
|
||||
core.startGroup('Pre-installing GHC with stack');
|
||||
await exec('stack', ['setup', opts.ghc.version]);
|
||||
core.endGroup();
|
||||
}
|
||||
findHaskellCabalVersion(baseInstallDir, cabalVersion);
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user