diff --git a/.eslintignore b/.eslintignore index c16efb62..188b6866 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ dist/ +lib/ .out/ __tests__/ node_modules/ diff --git a/.gitignore b/.gitignore index 1da4d09f..e29e50ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules dist-newstyle .out +**/tsbuildinfo .eslintcache diff --git a/.lintstagedrc.js b/.lintstagedrc.js index 3fe7ae29..fb180d91 100644 --- a/.lintstagedrc.js +++ b/.lintstagedrc.js @@ -1,6 +1,6 @@ module.exports = { '!(*test).{js,ts}': 'eslint --cache --fix', - '!(*test).ts': () => ['ncc build', 'git add dist'], + '!(*test).ts': () => ['npm run bundle', 'git add dist/ lib/'], 'src/**/*.ts': () => 'tsc -p tsconfig.json', '*.{js,ts,json,md}': 'prettier --write' }; diff --git a/__tests__/find-haskell.test.ts b/__tests__/find-haskell.test.ts index 77b58976..042b2bfe 100644 --- a/__tests__/find-haskell.test.ts +++ b/__tests__/find-haskell.test.ts @@ -1,5 +1,4 @@ import {getOpts, getDefaults, Tool} from '../src/opts'; -import {getInput} from '@actions/core'; import * as supported_versions from '../src/versions.json'; const def = getDefaults(); @@ -9,12 +8,6 @@ const latestVersions = { stack: supported_versions.stack[0] }; -const mkName = (s: string): string => - `INPUT_${s.replace(/ /g, '_').toUpperCase()}`; - -const setupEnv = (o: Record): void => - Object.entries(o).forEach(([k, v]) => v && (process.env[mkName(k)] = `${v}`)); - const forAll = (fn: (t: Tool) => any) => (['ghc', 'cabal', 'stack'] as const).forEach(fn); @@ -36,42 +29,32 @@ describe('actions/setup-haskell', () => { it('Supported versions are parsed from JSON correctly', () => forAll(t => expect(def[t].supported).toBe(supported_versions[t]))); - it('[meta] Setup Env works', () => { - setupEnv({input: 'value'}); - const i = getInput('input'); - expect(i).toEqual('value'); - }); - it('getOpts grabs defaults correctly from environment', () => { - setupEnv({}); - const options = getOpts(def); + const options = getOpts(def, {}); forAll(t => expect(options[t].raw).toBe(def[t].version)); }); it('Versions resolve correctly', () => { const v = {ghc: '8.6.5', cabal: '2.4.1.0', stack: '2.1.3'}; - setupEnv({ + const options = getOpts(def, { 'stack-version': '2.1', 'ghc-version': '8.6', 'cabal-version': '2.4' }); - const options = getOpts(def); forAll(t => expect(options[t].resolved).toBe(v[t])); }); it('"latest" Versions resolve correctly', () => { - setupEnv({ + const options = getOpts(def, { 'stack-version': 'latest', 'ghc-version': 'latest', 'cabal-version': 'latest' }); - const options = getOpts(def); forAll(t => expect(options[t].resolved).toBe(latestVersions[t])); }); it('Enabling stack does not disable GHC or Cabal', () => { - setupEnv({'enable-stack': 'true'}); - const {ghc, cabal, stack} = getOpts(def); + const {ghc, cabal, stack} = getOpts(def, {'enable-stack': 'true'}); expect({ ghc: ghc.enable, stack: stack.enable, @@ -80,8 +63,10 @@ describe('actions/setup-haskell', () => { }); it('Enabling stack-no-global disables GHC and Cabal', () => { - setupEnv({'enable-stack': 'true', 'stack-no-global': 'true'}); - const {ghc, cabal, stack} = getOpts(def); + const {ghc, cabal, stack} = getOpts(def, { + 'enable-stack': 'true', + 'stack-no-global': 'true' + }); expect({ ghc: ghc.enable, cabal: cabal.enable, @@ -90,12 +75,10 @@ describe('actions/setup-haskell', () => { }); it('Enabling stack-no-global without setting enable-stack errors', () => { - setupEnv({'stack-no-global': 'true'}); - expect(() => getOpts(def)).toThrow(); + expect(() => getOpts(def, {'stack-no-global': 'true'})).toThrow(); }); it('Enabling stack-setup-ghc without setting enable-stack errors', () => { - setupEnv({'stack-setup-ghc': 'true'}); - expect(() => getOpts(def)).toThrow(); + expect(() => getOpts(def, {'stack-setup-ghc': 'true'})).toThrow(); }); }); diff --git a/dist/index.js b/dist/index.js index 0f6f063f..a90b6a3b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -40,7 +40,7 @@ module.exports = /******/ // the startup function /******/ function startup() { /******/ // Load entry module and return exports -/******/ return __webpack_require__(661); +/******/ return __webpack_require__(131); /******/ }; /******/ /******/ // run startup @@ -868,16 +868,16 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getOpts = exports.getDefaults = void 0; +exports.getOpts = exports.getDefaults = exports.yamlInputs = void 0; const core = __importStar(__webpack_require__(470)); const fs_1 = __webpack_require__(747); const js_yaml_1 = __webpack_require__(414); const path_1 = __webpack_require__(622); const supported_versions = __importStar(__webpack_require__(447)); +exports.yamlInputs = js_yaml_1.safeLoad(fs_1.readFileSync(__webpack_require__.ab + "action.yml", 'utf8')).inputs; function getDefaults() { - const inpts = js_yaml_1.safeLoad(fs_1.readFileSync(__webpack_require__.ab + "action.yml", 'utf8')).inputs; const mkVersion = (v, vs) => ({ - version: resolve(inpts[v].default, vs), + version: resolve(exports.yamlInputs[v].default, vs), supported: vs }); return { @@ -893,14 +893,14 @@ function resolve(version, supported) { ? supported[0] : (_a = supported.find(v => v.startsWith(version))) !== null && _a !== void 0 ? _a : version; } -function getOpts({ ghc, cabal, stack }) { - const stackNoGlobal = core.getInput('stack-no-global') !== ''; - const stackSetupGhc = core.getInput('stack-setup-ghc') !== ''; - const stackEnable = core.getInput('enable-stack') !== ''; +function getOpts({ ghc, cabal, stack }, inputs) { + const stackNoGlobal = inputs['stack-no-global'] === 'true'; + const stackSetupGhc = inputs['stack-setup-ghc'] === 'true'; + const stackEnable = inputs['enable-stack'] === 'true'; const verInpt = { - ghc: core.getInput('ghc-version') || ghc.version, - cabal: core.getInput('cabal-version') || cabal.version, - stack: core.getInput('stack-version') || stack.version + ghc: inputs['ghc-version'] || ghc.version, + cabal: inputs['cabal-version'] || cabal.version, + stack: inputs['stack-version'] || stack.version }; const errors = []; if (stackNoGlobal && !stackEnable) { @@ -927,7 +927,7 @@ function getOpts({ ghc, cabal, stack }) { raw: verInpt.stack, resolved: resolve(verInpt.stack, stack.supported), enable: stackEnable, - setup: core.getInput('stack-setup-ghc') !== '' + setup: inputs['stack-setup-ghc'] !== '' } }; // eslint-disable-next-line github/array-foreach @@ -1137,6 +1137,42 @@ exports.issueCommand = issueCommand; module.exports = require("child_process"); +/***/ }), + +/***/ 131: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(__webpack_require__(470)); +const opts_1 = __webpack_require__(54); +const setup_haskell_1 = __importDefault(__webpack_require__(661)); +setup_haskell_1.default(Object.fromEntries(Object.keys(opts_1.yamlInputs).map(k => [k, core.getInput(k)]))); + + /***/ }), /***/ 139: @@ -8690,10 +8726,10 @@ async function cabalConfig() { }); return out.toString().trim().split('\n').slice(-1)[0].trim(); } -(async () => { +async function run(inputs) { try { core.info('Preparing to setup a Haskell environment'); - const opts = opts_1.getOpts(opts_1.getDefaults()); + const opts = opts_1.getOpts(opts_1.getDefaults(), inputs); for (const [t, { resolved }] of Object.entries(opts).filter(o => o[1].enable)) await core.group(`Installing ${t} version ${resolved}`, async () => installer_1.installTool(t, resolved, process.platform)); if (opts.stack.setup) @@ -8717,7 +8753,8 @@ async function cabalConfig() { catch (error) { core.setFailed(error.message); } -})(); +} +exports.default = run; /***/ }), diff --git a/docs/contributors.md b/docs/contributors.md index ec461487..d8789d07 100644 --- a/docs/contributors.md +++ b/docs/contributors.md @@ -3,7 +3,8 @@ ### Checkin - Do checkin source (src) -- Do checkin build output (dist) +- Do checkin action build output (dist) +- Do checkin library build output (lib) ### Dependencies @@ -16,7 +17,7 @@ git add abc.ext # Add the files you've changed. This git commit -m "Informative commit message" # Commit. This will run Husky ``` -During the commit step, Husky will take care of formatting all files with [Prettier](https://github.com/prettier/prettier). It will also bundle the code into a single `dist/index.js` file. +During the commit step, Husky will take care of formatting all files with [Prettier](https://github.com/prettier/prettier). It will also bundle the code into a single `dist/index.js` file and output the traspiled library under `lib/`. Finally, it will make sure these changes are appropriately included in your commit--no further work is needed. ## Versions diff --git a/lib/installer.d.ts b/lib/installer.d.ts new file mode 100644 index 00000000..48c2a1ba --- /dev/null +++ b/lib/installer.d.ts @@ -0,0 +1,6 @@ +import type {OS, Tool} from './opts'; +export declare function installTool( + tool: Tool, + version: string, + os: OS +): Promise; diff --git a/lib/installer.js b/lib/installer.js new file mode 100644 index 00000000..73549288 --- /dev/null +++ b/lib/installer.js @@ -0,0 +1,241 @@ +'use strict'; +var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + } + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', {enumerable: true, value: v}); + } + : function (o, v) { + o['default'] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; +Object.defineProperty(exports, '__esModule', {value: true}); +exports.installTool = void 0; +const core = __importStar(require('@actions/core')); +const exec_1 = require('@actions/exec'); +const io_1 = require('@actions/io'); +const glob_1 = require('@actions/glob'); +const tc = __importStar(require('@actions/tool-cache')); +const fs_1 = require('fs'); +const path_1 = require('path'); +function failed(tool, version) { + throw new Error(`All install methods for ${tool} ${version} failed`); +} +async function success(tool, version, path) { + core.addPath(path); + core.setOutput(`${tool}-path`, path); + core.setOutput(`${tool}-exe`, await io_1.which(tool)); + core.info( + `Found ${tool} ${version} in cache at path ${path}. Setup successful.` + ); + return true; +} +function warn(tool, version) { + 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, version, os) { + 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: [], + 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_1.promises + .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_1.exec(await ghcupBin(os), ['set', version]); + return success(tool, version, installedPath); + } + } + if (tool === 'cabal' && os !== 'win32') { + const installedPath = await fs_1.promises + .access(`${ghcupPath}/cabal`) + .then(() => ghcupPath) + .catch(() => undefined); + if (installedPath) return success(tool, version, installedPath); + } + return false; +} +async function installTool(tool, version, os) { + 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); +} +exports.installTool = installTool; +async function stack(version, os) { + 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_1 + .create(`${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, version) { + 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_1.exec( + `sudo -- sh -c "apt-get -y install ${toolName}-${v}"`, + undefined, + { + ignoreReturnCode: true + } + ); +} +async function choco(tool, version) { + 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_1.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) { + const v = '0.1.8'; + const cachedBin = tc.find('ghcup', v); + if (cachedBin) return path_1.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_1.promises.chmod(bin, 0o755); + return path_1.join(await tc.cacheFile(bin, 'ghcup', 'ghcup', v), 'ghcup'); +} +async function ghcup(tool, version, os) { + core.info(`Attempting to install ${tool} ${version} using ghcup`); + const bin = await ghcupBin(os); + const returnCode = await exec_1.exec( + bin, + [tool === 'ghc' ? 'install' : 'install-cabal', version], + { + ignoreReturnCode: true + } + ); + if (returnCode === 0 && tool === 'ghc') + await exec_1.exec(bin, ['set', version]); +} +function getChocoPath(tool, version) { + // 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 = path_1.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; +} diff --git a/lib/opts.d.ts b/lib/opts.d.ts new file mode 100644 index 00000000..3e62205c --- /dev/null +++ b/lib/opts.d.ts @@ -0,0 +1,31 @@ +export declare type OS = 'linux' | 'darwin' | 'win32'; +export declare type Tool = 'cabal' | 'ghc' | 'stack'; +export interface ProgramOpt { + enable: boolean; + raw: string; + resolved: string; +} +export interface Options { + ghc: ProgramOpt; + cabal: ProgramOpt; + stack: ProgramOpt & { + setup: boolean; + }; +} +declare type Version = { + version: string; + supported: string[]; +}; +export declare type Defaults = Record; +export declare const yamlInputs: Record< + string, + { + default: string; + } +>; +export declare function getDefaults(): Defaults; +export declare function getOpts( + {ghc, cabal, stack}: Defaults, + inputs: Record +): Options; +export {}; diff --git a/lib/opts.js b/lib/opts.js new file mode 100644 index 00000000..20f3cd82 --- /dev/null +++ b/lib/opts.js @@ -0,0 +1,114 @@ +'use strict'; +var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + } + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', {enumerable: true, value: v}); + } + : function (o, v) { + o['default'] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; +Object.defineProperty(exports, '__esModule', {value: true}); +exports.getOpts = exports.getDefaults = exports.yamlInputs = void 0; +const core = __importStar(require('@actions/core')); +const fs_1 = require('fs'); +const js_yaml_1 = require('js-yaml'); +const path_1 = require('path'); +const supported_versions = __importStar(require('./versions.json')); +exports.yamlInputs = js_yaml_1.safeLoad( + fs_1.readFileSync(path_1.join(__dirname, '..', 'action.yml'), 'utf8') +).inputs; +function getDefaults() { + const mkVersion = (v, vs) => ({ + version: resolve(exports.yamlInputs[v].default, vs), + supported: vs + }); + return { + ghc: mkVersion('ghc-version', supported_versions.ghc), + cabal: mkVersion('cabal-version', supported_versions.cabal), + stack: mkVersion('stack-version', supported_versions.stack) + }; +} +exports.getDefaults = getDefaults; +function resolve(version, supported) { + var _a; + return version === 'latest' + ? supported[0] + : (_a = supported.find(v => v.startsWith(version))) !== null && + _a !== void 0 + ? _a + : version; +} +function getOpts({ghc, cabal, stack}, inputs) { + const stackNoGlobal = inputs['stack-no-global'] === 'true'; + const stackSetupGhc = inputs['stack-setup-ghc'] === 'true'; + const stackEnable = inputs['enable-stack'] === 'true'; + const verInpt = { + ghc: inputs['ghc-version'] || ghc.version, + cabal: inputs['cabal-version'] || cabal.version, + stack: inputs['stack-version'] || stack.version + }; + const errors = []; + if (stackNoGlobal && !stackEnable) { + errors.push('enable-stack is required if stack-no-global is set'); + } + if (stackSetupGhc && !stackEnable) { + errors.push('enable-stack is required if stack-setup-ghc is set'); + } + if (errors.length > 0) { + throw new Error(errors.join('\n')); + } + const opts = { + ghc: { + raw: verInpt.ghc, + resolved: resolve(verInpt.ghc, ghc.supported), + enable: !stackNoGlobal + }, + cabal: { + raw: verInpt.cabal, + resolved: resolve(verInpt.cabal, cabal.supported), + enable: !stackNoGlobal + }, + stack: { + raw: verInpt.stack, + resolved: resolve(verInpt.stack, stack.supported), + enable: stackEnable, + setup: inputs['stack-setup-ghc'] !== '' + } + }; + // eslint-disable-next-line github/array-foreach + Object.values(opts) + .filter(t => t.enable && t.raw !== t.resolved) + .forEach(t => core.info(`Resolved ${t.raw} to ${t.resolved}`)); + core.debug(`Options are: ${JSON.stringify(opts)}`); + return opts; +} +exports.getOpts = getOpts; diff --git a/lib/setup-haskell.d.ts b/lib/setup-haskell.d.ts new file mode 100644 index 00000000..10f93666 --- /dev/null +++ b/lib/setup-haskell.d.ts @@ -0,0 +1 @@ +export default function run(inputs: Record): Promise; diff --git a/lib/setup-haskell.js b/lib/setup-haskell.js new file mode 100644 index 00000000..468144d6 --- /dev/null +++ b/lib/setup-haskell.js @@ -0,0 +1,83 @@ +'use strict'; +var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + } + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', {enumerable: true, value: v}); + } + : function (o, v) { + o['default'] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; +Object.defineProperty(exports, '__esModule', {value: true}); +const core = __importStar(require('@actions/core')); +const fs = __importStar(require('fs')); +const opts_1 = require('./opts'); +const installer_1 = require('./installer'); +const exec_1 = require('@actions/exec'); +async function cabalConfig() { + let out = Buffer.from(''); + const append = b => (out = Buffer.concat([out, b])); + await exec_1.exec('cabal', ['--help'], { + silent: true, + listeners: {stdout: append, stderr: append} + }); + return out.toString().trim().split('\n').slice(-1)[0].trim(); +} +async function run(inputs) { + try { + core.info('Preparing to setup a Haskell environment'); + const opts = opts_1.getOpts(opts_1.getDefaults(), inputs); + for (const [t, {resolved}] of Object.entries(opts).filter(o => o[1].enable)) + await core.group(`Installing ${t} version ${resolved}`, async () => + installer_1.installTool(t, resolved, process.platform) + ); + if (opts.stack.setup) + await core.group('Pre-installing GHC with stack', async () => + exec_1.exec('stack', ['setup', opts.ghc.resolved]) + ); + if (opts.cabal.enable) + await core.group('Setting up cabal', async () => { + await exec_1.exec('cabal', ['user-config', 'update'], {silent: true}); + const configFile = await cabalConfig(); + if (process.platform === 'win32') { + fs.appendFileSync(configFile, 'store-dir: C:\\sr\n'); + core.setOutput('cabal-store', 'C:\\sr'); + } else { + core.setOutput('cabal-store', `${process.env.HOME}/.cabal/store`); + } + await exec_1.exec('cabal user-config update'); + if (!opts.stack.enable) await exec_1.exec('cabal update'); + }); + } catch (error) { + core.setFailed(error.message); + } +} +exports.default = run; diff --git a/lib/versions.json b/lib/versions.json new file mode 100644 index 00000000..8219fd36 --- /dev/null +++ b/lib/versions.json @@ -0,0 +1,40 @@ +{ + "ghc": [ + "8.10.2", + "8.10.1", + "8.8.4", + "8.8.3", + "8.8.2", + "8.8.1", + "8.6.5", + "8.6.4", + "8.6.3", + "8.6.2", + "8.6.1", + "8.4.4", + "8.4.3", + "8.4.2", + "8.4.1", + "8.2.2", + "8.0.2", + "7.10.3" + ], + "cabal": ["3.2.0.0", "3.0.0.0", "2.4.1.0", "2.4.0.0", "2.2.0.0"], + "stack": [ + "2.3.1", + "2.1.3", + "2.1.1", + "1.9.3", + "1.9.1", + "1.7.1", + "1.6.5", + "1.6.3", + "1.6.1", + "1.5.1", + "1.5.0", + "1.4.0", + "1.3.2", + "1.3.0", + "1.2.0" + ] +} diff --git a/package.json b/package.json index 27d632c6..651953f4 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "setup-haskell", "version": "1.0.0", - "private": true, "description": "setup haskell action", - "main": "src/setup-haskell.ts", + "main": "lib/setup-haskell", + "types": "lib/setup-haskell", "scripts": { + "bundle": "ncc build src/main.ts && tsc -p tsconfig-lib.json", "test": "jest" }, "repository": { @@ -20,6 +21,13 @@ ], "author": "GitHub", "license": "MIT", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib" + ], "dependencies": { "@actions/core": "^1.2.4", "@actions/exec": "^1.0.4", diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 00000000..61a8ddbd --- /dev/null +++ b/src/main.ts @@ -0,0 +1,7 @@ +import * as core from '@actions/core'; +import {yamlInputs} from './opts'; +import run from './setup-haskell'; + +run( + Object.fromEntries(Object.keys(yamlInputs).map(k => [k, core.getInput(k)])) +); diff --git a/src/opts.ts b/src/opts.ts index 24ee1ebb..32c0f2dd 100644 --- a/src/opts.ts +++ b/src/opts.ts @@ -22,13 +22,13 @@ export interface Options { type Version = {version: string; supported: string[]}; export type Defaults = Record; -export function getDefaults(): Defaults { - const inpts = safeLoad( - readFileSync(join(__dirname, '..', 'action.yml'), 'utf8') - ).inputs; +export const yamlInputs: Record = safeLoad( + readFileSync(join(__dirname, '..', 'action.yml'), 'utf8') +).inputs; +export function getDefaults(): Defaults { const mkVersion = (v: string, vs: string[]): Version => ({ - version: resolve(inpts[v].default, vs), + version: resolve(yamlInputs[v].default, vs), supported: vs }); @@ -45,14 +45,17 @@ function resolve(version: string, supported: string[]): string { : supported.find(v => v.startsWith(version)) ?? version; } -export function getOpts({ghc, cabal, stack}: Defaults): Options { - const stackNoGlobal = core.getInput('stack-no-global') !== ''; - const stackSetupGhc = core.getInput('stack-setup-ghc') !== ''; - const stackEnable = core.getInput('enable-stack') !== ''; +export function getOpts( + {ghc, cabal, stack}: Defaults, + inputs: Record +): Options { + const stackNoGlobal = inputs['stack-no-global'] === 'true'; + const stackSetupGhc = inputs['stack-setup-ghc'] === 'true'; + const stackEnable = inputs['enable-stack'] === 'true'; const verInpt = { - ghc: core.getInput('ghc-version') || ghc.version, - cabal: core.getInput('cabal-version') || cabal.version, - stack: core.getInput('stack-version') || stack.version + ghc: inputs['ghc-version'] || ghc.version, + cabal: inputs['cabal-version'] || cabal.version, + stack: inputs['stack-version'] || stack.version }; const errors = []; @@ -83,7 +86,7 @@ export function getOpts({ghc, cabal, stack}: Defaults): Options { raw: verInpt.stack, resolved: resolve(verInpt.stack, stack.supported), enable: stackEnable, - setup: core.getInput('stack-setup-ghc') !== '' + setup: inputs['stack-setup-ghc'] !== '' } }; diff --git a/src/setup-haskell.ts b/src/setup-haskell.ts index 591ae586..da5764ba 100644 --- a/src/setup-haskell.ts +++ b/src/setup-haskell.ts @@ -15,10 +15,13 @@ async function cabalConfig(): Promise { return out.toString().trim().split('\n').slice(-1)[0].trim(); } -(async () => { +export default async function run( + inputs: Record +): Promise { try { core.info('Preparing to setup a Haskell environment'); - const opts = getOpts(getDefaults()); + + const opts = getOpts(getDefaults(), inputs); for (const [t, {resolved}] of Object.entries(opts).filter(o => o[1].enable)) await core.group(`Installing ${t} version ${resolved}`, async () => @@ -48,4 +51,4 @@ async function cabalConfig(): Promise { } catch (error) { core.setFailed(error.message); } -})(); +} diff --git a/tsconfig-lib.json b/tsconfig-lib.json new file mode 100644 index 00000000..85d1743d --- /dev/null +++ b/tsconfig-lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "lib/", + "tsBuildInfoFile": "lib/tsbuildinfo", + "baseUrl": "./" + }, + "exclude": ["src/main.ts", "**/*.test.ts", "lib"] +} diff --git a/tsconfig.json b/tsconfig.json index 39d78e85..e8341cab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,15 +8,14 @@ "es2020.symbol.wellknown" ], "module": "commonjs", - "outDir": ".out", - "tsBuildInfoFile": ".out/tsbuildinfo", "incremental": true, - "rootDir": "./src", "strict": true, "noImplicitAny": true, "esModuleInterop": true, - "moduleResolution": "node", - "resolveJsonModule": true + "resolveJsonModule": true, + "outDir": ".out", + "tsBuildInfoFile": ".out/tsbuidlinfo", + "rootDir": "src/" }, - "exclude": ["node_modules", "**/*.test.ts"] + "exclude": ["**/*.test.ts"] }