From afb8586fc490b186076f403c7f253e26d420d849 Mon Sep 17 00:00:00 2001 From: "Paulo F. Oliveira" Date: Mon, 7 Aug 2023 10:32:15 +0100 Subject: [PATCH] Feature tool-cache for Gleam and rebar3 (#223) * Make iterating over mirrors more flexible (and reusable) At the same time we fix an error, where we aren't using the mirrors for the OTP build :) * Make installation of cache-based tool callback -based This hopefully eases introduction of new languages at the cost of a bit of abstraction (there's an extensive comment on how to fill in the install options' object) We'll soon test this abstraction by making Gleam and rebar3 cached too * Increase our visibility when debugging * Improve on lessons learned to ease incorporation of further caches (starting with Gleam on Linux) * Cache Gleam on Windows * Cache Rebar3 on Linux Also, gets rid of all .sh * Cache Rebar3 on Windows Also, gets rid of all .ps1 * Act on CI results And also be consistent when it comes to using fs....Sync vs fs.promise.... * Move tests to a single file * Move main executable to a single file * Decrease number of changes to ease review * Expose function required for tests * Don't async/await when not required * Resolve post- merge-conflict resolution issues --- .eslintrc.yml | 3 +- dist/index.js | 695 +++++++++++++++++++--------------- package.json | 4 +- src/install-gleam.ps1 | 37 -- src/install-gleam.sh | 35 -- src/install-rebar3.ps1 | 32 -- src/install-rebar3.sh | 25 -- src/installer.js | 212 ----------- src/setup-beam.js | 484 +++++++++++++++++++---- test/problem-matchers.test.js | 82 ---- test/setup-beam.test.js | 165 ++++++-- 11 files changed, 936 insertions(+), 838 deletions(-) delete mode 100644 src/install-gleam.ps1 delete mode 100755 src/install-gleam.sh delete mode 100644 src/install-rebar3.ps1 delete mode 100755 src/install-rebar3.sh delete mode 100644 src/installer.js delete mode 100644 test/problem-matchers.test.js diff --git a/.eslintrc.yml b/.eslintrc.yml index f7a5240a..685b4340 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -7,11 +7,12 @@ extends: parserOptions: ecmaVersion: 2022 rules: - indent: [warn, 2] + indent: [warn, 2, {SwitchCase: 1}] max-len: [warn, 100] no-use-before-define: 0 operator-linebreak: 0 semi: 0 + implicit-arrow-linebreak: 0 settings: react: diff --git a/dist/index.js b/dist/index.js index fbebd827..934d4433 100644 --- a/dist/index.js +++ b/dist/index.js @@ -9814,225 +9814,6 @@ try { } catch (er) {} -/***/ }), - -/***/ 2127: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { - -const core = __nccwpck_require__(2186) -const { exec } = __nccwpck_require__(1514) -const tc = __nccwpck_require__(7784) -const path = __nccwpck_require__(1017) -const fs = __nccwpck_require__(7147) -const os = __nccwpck_require__(2037) - -/** - * Install Erlang/OTP. - * - * @param {string} osVersion - * @param {string} otpVersion - * @param {string[]} hexMirrors - */ -async function installOTP(osVersion, otpVersion, hexMirrors) { - let cmd - let args - - if (hexMirrors.length === 0) { - throw new Error( - `Could not install Erlang/OTP ${otpVersion} from any hex.pm mirror`, - ) - } - - const [hexMirror, ...hexMirrorsT] = hexMirrors - const fullVersion = `${osVersion}/${otpVersion}` - let cachedPath = tc.find('otp', fullVersion) - const OS = process.platform - - try { - if (OS === 'linux') { - if (!cachedPath) { - const tarPath = await tc.downloadTool( - `https://builds.hex.pm/builds/otp/${fullVersion}.tar.gz`, - ) - const extractPath = await tc.extractTar(tarPath, undefined, [ - 'zx', - '--strip-components=1', - ]) - cachedPath = await tc.cacheDir(extractPath, 'otp', fullVersion) - } - - cmd = path.join(cachedPath, 'Install') - args = ['-minimal', cachedPath] - await exec(cmd, args) - - const otpPath = path.join(cachedPath, 'bin') - - core.addPath(otpPath) - core.exportVariable('INSTALL_DIR_FOR_OTP', cachedPath) - - core.info('Installed Erlang/OTP version') - cmd = path.join(otpPath, 'erl') - args = ['-version'] - await exec(cmd, args) - } else if (OS === 'win32') { - if (!cachedPath) { - const exePath = await tc.downloadTool( - 'https://github.com/erlang/otp/releases/download/' + - `OTP-${otpVersion}/otp_win64_${otpVersion}.exe`, - ) - cachedPath = await tc.cacheFile(exePath, 'otp.exe', 'otp', fullVersion) - } - - const otpDir = path.join(process.env.RUNNER_TEMP, '.setup-beam', 'otp') - const otpPath = path.join(otpDir, 'bin') - - await fs.promises.mkdir(otpDir, { recursive: true }) - - cmd = path.join(cachedPath, 'otp.exe') - args = ['/S', `/D=${otpDir}`] - await exec(cmd, args) - - core.addPath(otpPath) - core.exportVariable('INSTALL_DIR_FOR_OTP', otpDir) - - core.info('Installed Erlang/OTP version') - - cmd = path.join(otpPath, 'erl.exe') - args = ['+V'] - await exec(cmd, args) - } - } catch (err) { - core.info(`Install OTP failed for mirror ${hexMirror}`) - core.info(`${err}\n${err.stack}`) - await installOTP(osVersion, otpVersion, hexMirrorsT) - } -} - -/** - * Install Elixir. - * - * @param {string} elixirVersion - * @param {string[]} hexMirrors - */ -async function installElixir(elixirVersion, hexMirrors) { - let cmd - let args - let options - - if (hexMirrors.length === 0) { - throw new Error( - `Could not install Elixir ${elixirVersion} from any hex.pm mirror`, - ) - } - const [hexMirror, ...hexMirrorsT] = hexMirrors - - try { - let cachedPath = tc.find('elixir', elixirVersion) - const OS = process.platform - - if (!cachedPath) { - const zipPath = await tc.downloadTool( - `${hexMirror}/builds/elixir/${elixirVersion}.zip`, - ) - const extractPath = await tc.extractZip(zipPath) - cachedPath = await tc.cacheDir(extractPath, 'elixir', elixirVersion) - } - - const elixirPath = path.join(cachedPath, 'bin') - const escriptsPath = path.join(os.homedir(), '.mix', 'escripts') - - core.addPath(elixirPath) - core.addPath(escriptsPath) - core.exportVariable('INSTALL_DIR_FOR_ELIXIR', cachedPath) - - core.info('Installed Elixir version') - - if (debugLoggingEnabled()) { - core.exportVariable('ELIXIR_CLI_ECHO', 'true') - } - - if (OS === 'linux') { - cmd = path.join(elixirPath, 'elixir') - args = ['-v'] - options = {} - } else if (OS === 'win32') { - cmd = path.join(elixirPath, 'elixir.bat') - args = ['-v'] - options = { windowsVerbatimArguments: true } - } - await exec(cmd, args, options) - - await fs.promises.mkdir(escriptsPath, { recursive: true }) - } catch (err) { - core.info(`Elixir install failed for mirror ${hexMirror}`) - core.info(`${err}\n${err.stack}`) - await installElixir(elixirVersion, hexMirrorsT) - } -} - -/** - * Install Gleam. - * - * @param {string} gleamVersion - */ -async function installGleam(gleamVersion) { - let cmd - let args - - const OS = process.platform - if (OS === 'linux') { - cmd = __nccwpck_require__.ab + "install-gleam.sh" - args = [gleamVersion] - await exec(__nccwpck_require__.ab + "install-gleam.sh", args) - } else if (OS === 'win32') { - cmd = `pwsh.exe ${path.join(__dirname, 'install-gleam.ps1')}` - args = [`-VSN:${gleamVersion}`] - await exec(cmd, args) - } -} - -/** - * Install rebar3. - * - * @param {string} rebar3Version - */ -async function installRebar3(rebar3Version) { - let cmd - let args - - const OS = process.platform - if (OS === 'linux') { - cmd = __nccwpck_require__.ab + "install-rebar3.sh" - args = [rebar3Version] - await exec(__nccwpck_require__.ab + "install-rebar3.sh", args) - } else if (OS === 'win32') { - cmd = `pwsh.exe ${path.join(__dirname, 'install-rebar3.ps1')}` - args = [`-VSN:${rebar3Version}`] - await exec(cmd, args) - } -} - -function checkPlatform() { - if (process.platform !== 'linux' && process.platform !== 'win32') { - throw new Error( - '@erlef/setup-beam only supports Ubuntu and Windows at this time', - ) - } -} - -function debugLoggingEnabled() { - return !!process.env.RUNNER_DEBUG -} - -module.exports = { - installOTP, - installElixir, - installGleam, - installRebar3, - checkPlatform, -} - - /***/ }), /***/ 7037: @@ -10040,18 +9821,19 @@ module.exports = { const core = __nccwpck_require__(2186) const { exec } = __nccwpck_require__(1514) +const tc = __nccwpck_require__(7784) const http = __nccwpck_require__(6255) const path = __nccwpck_require__(1017) const semver = __nccwpck_require__(1383) const fs = __nccwpck_require__(7147) -const installer = __nccwpck_require__(2127) +const os = __nccwpck_require__(2037) main().catch((err) => { core.setFailed(err.message) }) async function main() { - installer.checkPlatform() + checkPlatform() const versionFilePath = getInput('version-file', false) let versions @@ -10069,24 +9851,16 @@ async function main() { const elixirSpec = getInput('elixir-version', false, 'elixir', versions) const gleamSpec = getInput('gleam-version', false, 'gleam', versions) const rebar3Spec = getInput('rebar3-version', false, 'rebar', versions) - const hexMirrors = core.getMultilineInput('hexpm-mirrors', { - required: false, - }) if (otpSpec !== 'false') { - await installOTP(otpSpec, osVersion, hexMirrors) - - const elixirInstalled = await maybeInstallElixir( - elixirSpec, - otpSpec, - hexMirrors, - ) + await installOTP(otpSpec, osVersion) + const elixirInstalled = await maybeInstallElixir(elixirSpec, otpSpec) if (elixirInstalled === true) { const shouldMixRebar = getInput('install-rebar', false) - await mix(shouldMixRebar, 'rebar', hexMirrors) + await mix(shouldMixRebar, 'rebar') const shouldMixHex = getInput('install-hex', false) - await mix(shouldMixHex, 'hex', hexMirrors) + await mix(shouldMixHex, 'hex') } } else if (!gleamSpec) { throw new Error('otp-version=false is only available when installing Gleam') @@ -10096,33 +9870,43 @@ async function main() { await maybeInstallRebar3(rebar3Spec) } -async function installOTP(otpSpec, osVersion, hexMirrors) { - const otpVersion = await getOTPVersion(otpSpec, osVersion, hexMirrors) +async function installOTP(otpSpec, osVersion) { + const otpVersion = await getOTPVersion(otpSpec, osVersion) core.startGroup(`Installing Erlang/OTP ${otpVersion} - built on ${osVersion}`) - await installer.installOTP(osVersion, otpVersion, hexMirrors) + await doWithMirrors({ + hexMirrors: hexMirrorsInput(), + actionTitle: `install Erlang/OTP ${otpVersion}`, + action: async (hexMirror) => { + await install('otp', { + osVersion, + toolVersion: otpVersion, + hexMirror, + }) + }, + }) core.setOutput('otp-version', otpVersion) core.endGroup() return otpVersion } -async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) { +async function maybeInstallElixir(elixirSpec, otpSpec) { let installed = false if (elixirSpec) { - const elixirVersion = await getElixirVersion( - elixirSpec, - otpSpec, - hexMirrors, - ) + const elixirVersion = await getElixirVersion(elixirSpec, otpSpec) core.startGroup(`Installing Elixir ${elixirVersion}`) - await installer.installElixir(elixirVersion, hexMirrors) + await doWithMirrors({ + hexMirrors: hexMirrorsInput(), + actionTitle: `install Elixir ${elixirVersion}`, + action: async (hexMirror) => { + await install('elixir', { + toolVersion: elixirVersion, + hexMirror, + }) + }, + }) core.setOutput('elixir-version', elixirVersion) - - const disableProblemMatchers = getInput('disable_problem_matchers', false) - if (disableProblemMatchers === 'false') { - const elixirMatchers = __nccwpck_require__.ab + "elixir-matchers.json" - core.info(`##[add-matcher]${elixirMatchers}`) - } + maybeEnableElixirProblemMatchers() core.endGroup() installed = true @@ -10131,30 +9915,27 @@ async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) { return installed } -async function mixWithMirrors(cmd, args, hexMirrors) { - if (hexMirrors.length === 0) { - throw new Error('mix failed with every mirror') +function maybeEnableElixirProblemMatchers() { + const disableProblemMatchers = getInput('disable_problem_matchers', false) + if (disableProblemMatchers === 'false') { + const elixirMatchers = __nccwpck_require__.ab + "elixir-matchers.json" + core.info(`##[add-matcher]${elixirMatchers}`) } - - const [hexMirror, ...hexMirrorsT] = hexMirrors - process.env.HEX_MIRROR = hexMirror - try { - return await exec(cmd, args) - } catch (err) { - core.info( - `mix failed with mirror ${process.env.HEX_MIRROR} with message ${err.message})`, - ) - } - - return mixWithMirrors(cmd, args, hexMirrorsT) } -async function mix(shouldMix, what, hexMirrors) { +async function mix(shouldMix, what) { if (shouldMix === 'true') { const cmd = 'mix' const args = [`local.${what}`, '--force'] core.startGroup(`Running ${cmd} ${args}`) - await mixWithMirrors(cmd, args, hexMirrors) + await doWithMirrors({ + hexMirrors: hexMirrorsInput(), + actionTitle: `mix ${what}`, + action: async (hexMirror) => { + process.env.HEX_MIRROR = hexMirror + await exec(cmd, args) + }, + }) core.endGroup() } } @@ -10164,7 +9945,7 @@ async function maybeInstallGleam(gleamSpec) { if (gleamSpec) { const gleamVersion = await getGleamVersion(gleamSpec) core.startGroup(`Installing Gleam ${gleamVersion}`) - await installer.installGleam(gleamVersion) + await install('gleam', { toolVersion: gleamVersion }) core.setOutput('gleam-version', gleamVersion) core.addPath(`${process.env.RUNNER_TEMP}/.setup-beam/gleam/bin`) core.endGroup() @@ -10185,7 +9966,7 @@ async function maybeInstallRebar3(rebar3Spec) { rebar3Version = await getRebar3Version(rebar3Spec) } core.startGroup(`Installing rebar3 ${rebar3Version}`) - await installer.installRebar3(rebar3Version) + await install('rebar3', { toolVersion: rebar3Version }) core.setOutput('rebar3-version', rebar3Version) core.addPath(`${process.env.RUNNER_TEMP}/.setup-beam/rebar3/bin`) core.endGroup() @@ -10196,9 +9977,9 @@ async function maybeInstallRebar3(rebar3Spec) { return installed } -async function getOTPVersion(otpSpec0, osVersion, hexMirrors) { - const otpVersions = await getOTPVersions(osVersion, hexMirrors) - const spec = otpSpec0.replace(/^OTP-/, '') +async function getOTPVersion(otpSpec0, osVersion) { + const otpVersions = await getOTPVersions(osVersion) + let spec = otpSpec0.replace(/^OTP-/, '') const versions = otpVersions const otpVersion = getVersionFromSpec(spec, versions) if (otpVersion === null) { @@ -10211,15 +9992,15 @@ async function getOTPVersion(otpSpec0, osVersion, hexMirrors) { return otpVersion // from the reference, for download } -async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) { +async function getElixirVersion(exSpec0, otpVersion0) { const otpVersion = otpVersion0.match(/^([^-]+-)?(.+)$/)[2] const otpVersionMajor = otpVersion.match(/^([^.]+).*$/)[1] - const [otpVersionsForElixirMap, elixirVersions] = await getElixirVersions( - hexMirrors, - ) + + const [otpVersionsForElixirMap, elixirVersions] = await getElixirVersions() const spec = exSpec0.replace(/-otp-.*$/, '') const versions = elixirVersions const elixirVersionFromSpec = getVersionFromSpec(spec, versions) + if (elixirVersionFromSpec === null) { throw new Error( `Requested Elixir version (${exSpec0}) not found in version list ` + @@ -10283,12 +10064,19 @@ async function getRebar3Version(r3Spec) { return rebar3Version } -async function getOTPVersions(osVersion, hexMirrors) { +async function getOTPVersions(osVersion) { let otpVersionsListings let originListing if (process.platform === 'linux') { originListing = `/builds/otp/${osVersion}/builds.txt` - otpVersionsListings = await getWithMirrors(originListing, hexMirrors) + otpVersionsListings = await doWithMirrors({ + hexMirrors: hexMirrorsInput(), + actionTitle: `fetch ${originListing}`, + action: async (hexMirror) => { + const l = await get(`${hexMirror}${originListing}`, [null]) + return l + }, + }) } else if (process.platform === 'win32') { originListing = 'https://api.github.com/repos/erlang/otp/releases?per_page=100' @@ -10331,11 +10119,16 @@ async function getOTPVersions(osVersion, hexMirrors) { return otpVersions } -async function getElixirVersions(hexMirrors) { - const elixirVersionsListings = await getWithMirrors( - '/builds/elixir/builds.txt', - hexMirrors, - ) +async function getElixirVersions() { + const originListing = '/builds/elixir/builds.txt' + const elixirVersionsListings = await doWithMirrors({ + hexMirrors: hexMirrorsInput(), + actionTitle: `fetch ${originListing}`, + action: async (hexMirror) => { + const l = await get(`${hexMirror}${originListing}`, [null]) + return l + }, + }) const otpVersionsForElixirMap = {} const elixirVersions = {} @@ -10463,6 +10256,7 @@ function maybeCoerced(v) { } } catch { // some stuff can't be coerced, like 'main' + core.debug(`Was not able to coerce ${v} with semver`) ret = v } @@ -10552,21 +10346,6 @@ async function get(url0, pageIdxs) { return Promise.all(pageIdxs.map(getPage)) } -async function getWithMirrors(resourcePath, hexMirrors) { - if (hexMirrors.length === 0) { - throw new Error(`Could not fetch ${resourcePath} from any hex.pm mirror`) - } - - const [hexMirror, ...hexMirrorsT] = hexMirrors - try { - return await get(`${hexMirror}${resourcePath}`, [null]) - } catch (err) { - core.info(`get failed for URL ${hexMirror}${resourcePath}`) - } - - return getWithMirrors(resourcePath, hexMirrorsT) -} - function maybePrependWithV(v) { if (isVersion(v)) { return `v${v.replace('v', '')}` @@ -10687,12 +10466,334 @@ function debugLog(groupName, message) { ) } +function hexMirrorsInput() { + return core.getMultilineInput('hexpm-mirrors', { + required: false, + }) +} + +async function doWithMirrors(opts) { + const { hexMirrors, actionTitle, action } = opts + let actionRes + + if (hexMirrors.length === 0) { + throw new Error(`Could not ${actionTitle} from any hex.pm mirror`) + } + + const [hexMirror, ...hexMirrorsT] = hexMirrors + try { + actionRes = await action(hexMirror) + } catch (err) { + core.info( + `Action ${actionTitle} failed for mirror ${hexMirror}, with ${err}`, + ) + core.debug(`Stacktrace: ${err.stack}`) + actionRes = await doWithMirrors({ + hexMirrors: hexMirrorsT, + actionTitle, + action, + }) + } + + return actionRes +} + +async function install(toolName, opts) { + const { osVersion, toolVersion, hexMirror } = opts + const versionSpec = + osVersion !== undefined ? `${osVersion}/${toolVersion}` : toolVersion + let installOpts + + // The installOpts object is composed of supported processPlatform keys + // (e.g. 'linux', 'win32', or 'all' - in case there's no distinction between platforms) + // In each of these keys there's an object with keys: + // * downloadToolURL + // - where to fetch the downloadable from + // * extract + // - if the downloadable is compressed: how to extract it + // - return ['dir', targetDir] + // - if the downloadable is not compressed: a filename.ext you want to cache it under + // - return ['file', filenameWithExt] + // * postExtract + // - stuff to execute outside the cache scope (just after it's created) + // * reportVersion + /// - configuration elements on how to output the tool version, post-install + + switch (toolName) { + case 'otp': + installOpts = { + tool: 'Erlang/OTP', + linux: { + downloadToolURL: () => + `${hexMirror}/builds/otp/${versionSpec}.tar.gz`, + extract: async (file) => { + const dest = undefined + const flags = ['zx', '--strip-components=1'] + const targetDir = await tc.extractTar(file, dest, flags) + + return ['dir', targetDir] + }, + postExtract: async (cachePath) => { + const cmd = path.join(cachePath, 'Install') + const args = ['-minimal', cachePath] + await exec(cmd, args) + }, + reportVersion: () => { + const cmd = 'erl' + const args = ['-version'] + + return [cmd, args] + }, + }, + win32: { + downloadToolURL: () => + 'https://github.com/erlang/otp/releases/download/' + + `OTP-${toolVersion}/otp_win64_${toolVersion}.exe`, + extract: async () => ['file', 'otp.exe'], + postExtract: async (cachePath) => { + const cmd = path.join(cachePath, 'otp.exe') + const args = ['/S', `/D=${cachePath}`] + await exec(cmd, args) + }, + reportVersion: () => { + const cmd = 'erl.exe' + const args = ['+V'] + + return [cmd, args] + }, + }, + } + break + case 'elixir': + installOpts = { + tool: 'Elixir', + all: { + downloadToolURL: () => + `${hexMirror}/builds/elixir/${versionSpec}.zip`, + extract: async (file) => { + const targetDir = await tc.extractZip(file) + + return ['dir', targetDir] + }, + postExtract: async () => { + const escriptsPath = path.join(os.homedir(), '.mix', 'escripts') + fs.mkdirSync(escriptsPath, { recursive: true }) + core.addPath(escriptsPath) + + if (debugLoggingEnabled()) { + core.exportVariable('ELIXIR_CLI_ECHO', 'true') + } + }, + reportVersion: () => { + const cmd = 'elixir' + const args = ['-v'] + + return [cmd, args] + }, + }, + } + break + case 'gleam': + installOpts = { + tool: 'Gleam', + linux: { + downloadToolURL: () => { + let gz + if ( + versionSpec === 'nightly' || + semver.gt(versionSpec, 'v0.22.1') + ) { + gz = `gleam-${versionSpec}-x86_64-unknown-linux-musl.tar.gz` + } else { + gz = `gleam-${versionSpec}-linux-amd64.tar.gz` + } + + return `https://github.com/gleam-lang/gleam/releases/download/${versionSpec}/${gz}` + }, + extract: async (file) => { + const dest = undefined + const flags = ['zx'] + const targetDir = await tc.extractTar(file, dest, flags) + + return ['dir', targetDir] + }, + postExtract: async (cachePath) => { + const bindir = path.join(cachePath, 'bin') + const oldPath = path.join(cachePath, 'gleam') + const newPath = path.join(bindir, 'gleam') + fs.mkdirSync(bindir) + fs.renameSync(oldPath, newPath) + }, + reportVersion: () => { + const cmd = 'gleam' + const args = ['--version'] + + return [cmd, args] + }, + }, + win32: { + downloadToolURL: () => { + let zip + if ( + versionSpec === 'nightly' || + semver.gt(versionSpec, 'v0.22.1') + ) { + zip = `gleam-${versionSpec}-x86_64-pc-windows-msvc.zip` + } else { + zip = `gleam-${versionSpec}-windows-64bit.zip` + } + + return `https://github.com/gleam-lang/gleam/releases/download/${versionSpec}/${zip}` + }, + extract: async (file) => { + const targetDir = await tc.extractZip(file) + + return ['dir', targetDir] + }, + postExtract: async (cachePath) => { + const bindir = path.join(cachePath, 'bin') + const oldPath = path.join(cachePath, 'gleam.exe') + const newPath = path.join(bindir, 'gleam.exe') + fs.mkdirSync(bindir) + fs.renameSync(oldPath, newPath) + }, + reportVersion: () => { + const cmd = 'gleam.exe' + const args = ['--version'] + + return [cmd, args] + }, + }, + } + break + case 'rebar3': + installOpts = { + tool: 'Rebar3', + linux: { + downloadToolURL: () => { + let url + if (versionSpec === 'nightly') { + url = 'https://s3.amazonaws.com/rebar3-nightly/rebar3' + } else { + url = `https://github.com/erlang/rebar3/releases/download/${versionSpec}/rebar3` + } + + return url + }, + extract: async () => ['file', 'rebar3'], + postExtract: async (cachePath) => { + const bindir = path.join(cachePath, 'bin') + const oldPath = path.join(cachePath, 'rebar3') + const newPath = path.join(bindir, 'rebar3') + fs.mkdirSync(bindir) + fs.renameSync(oldPath, newPath) + fs.chmodSync(newPath, 0o755) + }, + reportVersion: () => { + const cmd = 'rebar3' + const args = ['version'] + + return [cmd, args] + }, + }, + win32: { + downloadToolURL: () => { + let url + if (versionSpec === 'nightly') { + url = 'https://s3.amazonaws.com/rebar3-nightly/rebar3' + } else { + url = `https://github.com/erlang/rebar3/releases/download/${versionSpec}/rebar3` + } + + return url + }, + extract: async () => ['file', 'rebar3'], + postExtract: async (cachePath) => { + const bindir = path.join(cachePath, 'bin') + const oldPath = path.join(cachePath, 'rebar3') + fs.mkdirSync(bindir) + fs.chmodSync(oldPath, 0o755) + + const ps1Filename = path.join(bindir, 'rebar3.ps1') + fs.writeFileSync(ps1Filename, `& escript.exe ${oldPath} \${args}`) + + const cmdFilename = path.join(bindir, 'rebar3.cmd') + fs.writeFileSync( + cmdFilename, + `@echo off\r\nescript.exe ${oldPath} %*`, + ) + }, + reportVersion: () => { + const cmd = 'rebar3.cmd' + const args = ['version'] + + return [cmd, args] + }, + }, + } + break + default: + throw new Error(`no installer for ${toolName}`) + } + + await installTool({ toolName, versionSpec, installOpts }) +} + +async function installTool(opts) { + const { toolName, versionSpec, installOpts } = opts + const platformOpts = installOpts[process.platform] || installOpts.all + let cachePath = tc.find(toolName, versionSpec) + + core.debug(`Checking if ${installOpts.tool} is already cached...`) + if (cachePath === '') { + core.debug(" ... it isn't!") + const downloadToolURL = platformOpts.downloadToolURL() + const file = await tc.downloadTool(downloadToolURL) + const [targetElemType, targetElem] = await platformOpts.extract(file) + + if (targetElemType === 'dir') { + cachePath = await tc.cacheDir(targetElem, toolName, versionSpec) + } else if (targetElemType === 'file') { + cachePath = await tc.cacheFile(file, targetElem, toolName, versionSpec) + } + } else { + core.debug(` ... it is, at ${cachePath}`) + } + + core.debug('Performing post extract operations...') + await platformOpts.postExtract(cachePath) + + core.debug(`Adding ${cachePath}'s bin to system path`) + core.addPath(path.join(cachePath, 'bin')) + + const installDirForVarName = `INSTALL_DIR_FOR_${toolName}`.toUpperCase() + core.debug(`Exporting ${installDirForVarName} as ${cachePath}`) + core.exportVariable(installDirForVarName, cachePath) + + core.info(`Installed ${installOpts.tool} version`) + const [cmd, args] = platformOpts.reportVersion() + await exec(cmd, args) +} + +function checkPlatform() { + if (process.platform !== 'linux' && process.platform !== 'win32') { + throw new Error( + '@erlef/setup-beam only supports Ubuntu and Windows at this time', + ) + } +} + +function debugLoggingEnabled() { + return !!process.env.RUNNER_DEBUG +} + module.exports = { getOTPVersion, getElixirVersion, getGleamVersion, getRebar3Version, getVersionFromSpec, + install, parseVersionFile, } diff --git a/package.json b/package.json index 002226f7..af67e2a6 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "jslint": "eslint src/**/*.js && eslint test/**/*.js", "licenses": "yarn licenses generate-disclaimer > 3RD_PARTY_LICENSES", "markdownlint": "markdownlint *.md ./github/**/*.md", - "shellcheck": "shellcheck src/install-*.sh .github/workflows/*.sh", - "test": "node test/setup-beam.test.js && node ./test/problem-matchers.test.js", + "shellcheck": "shellcheck .github/workflows/*.sh", + "test": "node test/setup-beam.test.js", "yamllint": "yamllint .github/workflows/**.yml && yamllint .*.yml && yamllint *.yml", "build-dist": "npm install && npm run build && npm run format && npm run markdownlint && npm run shellcheck && npm run yamllint && npm run jslint" }, diff --git a/src/install-gleam.ps1 b/src/install-gleam.ps1 deleted file mode 100644 index 828d1619..00000000 --- a/src/install-gleam.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -param([Parameter(Mandatory=$true)][string]${VSN}) - -$ErrorActionPreference="Stop" - -Set-Location ${Env:RUNNER_TEMP} - -$FILE_OUTPUT="gleam.zip" -$DIR_FOR_BIN=".setup-beam/gleam" - -function Version-Greater-Than([string]${THIS_ONE}, [string]${REFERENCE}) { - $THIS_ONE=$THIS_ONE.replace('v', '') - $THIS_ONE=$THIS_ONE.replace('rc', '') - $THIS_ONE=$THIS_ONE.replace('-', '') - return [version]${THIS_ONE} -gt [version]${REFERENCE} -} - -function Uses-LLVM-Triplets([string]${THIS_VSN}) { - return "${THIS_VSN}" -eq "nightly" -or (Version-Greater-Than "${THIS_VSN}" "0.22.1") -} - -if (Uses-LLVM-Triplets "$VSN") { - $FILE_INPUT="gleam-${VSN}-x86_64-pc-windows-msvc.zip" -} else { - $FILE_INPUT="gleam-${VSN}-windows-64bit.zip" -} - -$ProgressPreference="SilentlyContinue" -Invoke-WebRequest "https://github.com/gleam-lang/gleam/releases/download/${VSN}/${FILE_INPUT}" -OutFile "${FILE_OUTPUT}" -$ProgressPreference="Continue" -New-Item "${DIR_FOR_BIN}/bin" -ItemType Directory | Out-Null -$ProgressPreference="SilentlyContinue" -Expand-Archive -DestinationPath "${DIR_FOR_BIN}/bin" -Path "${FILE_OUTPUT}" -$ProgressPreference="Continue" -Write-Output "Installed Gleam version follows" -& "${DIR_FOR_BIN}/bin/gleam" "--version" | Write-Output - -"INSTALL_DIR_FOR_GLEAM=${Env:RUNNER_TEMP}/${DIR_FOR_BIN}" | Out-File -FilePath ${Env:GITHUB_ENV} -Encoding utf8 -Append diff --git a/src/install-gleam.sh b/src/install-gleam.sh deleted file mode 100755 index 6b060a9f..00000000 --- a/src/install-gleam.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -set -eo pipefail - -cd "${RUNNER_TEMP}" - -VSN="$1" -FILE_OUTPUT=gleam.tar.gz -DIR_FOR_BIN=.setup-beam/gleam - -version_gt() { - REFERENCE=$1 - test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$REFERENCE" -} - -uses_llvm_triplets() { - local VERSION="$1" - test "${VERSION}" = "nightly" || version_gt "${VERSION}" "v0.22.1" -} - -if uses_llvm_triplets "$VSN" -then - FILE_INPUT="gleam-${VSN}-x86_64-unknown-linux-musl.tar.gz" -else - FILE_INPUT="gleam-${VSN}-linux-amd64.tar.gz" -fi - -wget -q -O "${FILE_OUTPUT}" "https://github.com/gleam-lang/gleam/releases/download/${VSN}/${FILE_INPUT}" -mkdir -p "${DIR_FOR_BIN}/bin" -tar zxf "${FILE_OUTPUT}" -C "${DIR_FOR_BIN}/bin" - -echo "Installed Gleam version follows" -${DIR_FOR_BIN}/bin/gleam --version - -echo "INSTALL_DIR_FOR_GLEAM=${RUNNER_TEMP}/${DIR_FOR_BIN}" >> "${GITHUB_ENV}" diff --git a/src/install-rebar3.ps1 b/src/install-rebar3.ps1 deleted file mode 100644 index 7a30982f..00000000 --- a/src/install-rebar3.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -param([Parameter(Mandatory=$true)][string]${VSN}) - -$ErrorActionPreference="Stop" - -Set-Location ${Env:RUNNER_TEMP} - -$FILE_INPUT="rebar3" -$FILE_OUTPUT="rebar3" -$FILE_OUTPUT_PS1="rebar3.ps1" -$FILE_OUTPUT_CMD="rebar3.cmd" -$DIR_FOR_BIN=".setup-beam/rebar3" - -$ProgressPreference="SilentlyContinue" -$REBAR3_TARGET="https://github.com/erlang/rebar3/releases/download/${VSN}/${FILE_INPUT}" -$REBAR3_NIGHTLY="" -If ( ${VSN} -eq "nightly" ) -{ - $REBAR3_TARGET="https://s3.amazonaws.com/rebar3-nightly/rebar3" - $REBAR3_NIGHTLY=" (from nightly build)" -} -Invoke-WebRequest "${REBAR3_TARGET}" -OutFile "${FILE_OUTPUT}" -$ProgressPreference="Continue" -New-Item "${DIR_FOR_BIN}/bin" -ItemType Directory | Out-Null -Move-Item "${FILE_OUTPUT}" "${DIR_FOR_BIN}/bin" -Write-Output "& escript.exe ${PWD}/${DIR_FOR_BIN}/bin/${FILE_OUTPUT} `${args}" | Out-File -FilePath "${FILE_OUTPUT_PS1}" -Encoding utf8 -Append -Write-Output "@echo off`r`nescript.exe ${PWD}/${DIR_FOR_BIN}/bin/${FILE_OUTPUT} %*" | Out-File -FilePath "${FILE_OUTPUT_CMD}" -Encoding utf8 -Append -Move-Item "${FILE_OUTPUT_PS1}" "${DIR_FOR_BIN}/bin" -Move-Item "${FILE_OUTPUT_CMD}" "${DIR_FOR_BIN}/bin" -Write-Output "Installed rebar3 version${REBAR3_NIGHTLY} follows" -& "${DIR_FOR_BIN}/bin/rebar3.cmd" "version" | Write-Output - -"INSTALL_DIR_FOR_REBAR3=${Env:RUNNER_TEMP}/${DIR_FOR_BIN}" | Out-File -FilePath ${Env:GITHUB_ENV} -Encoding utf8 -Append diff --git a/src/install-rebar3.sh b/src/install-rebar3.sh deleted file mode 100755 index 6d182de2..00000000 --- a/src/install-rebar3.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -set -eo pipefail - -cd "${RUNNER_TEMP}" - -VSN=${1} -FILE_INPUT=rebar3 -FILE_OUTPUT=rebar3 -DIR_FOR_BIN=.setup-beam/rebar3 - -REBAR3_TARGET="https://github.com/erlang/rebar3/releases/download/${VSN}/${FILE_INPUT}" -REBAR3_NIGHTLY="" -if [ "${VSN}" == "nightly" ]; then - REBAR3_TARGET="https://s3.amazonaws.com/rebar3-nightly/rebar3" - REBAR3_NIGHTLY=" (from nightly build)" -fi -wget -q -O "${FILE_OUTPUT}" "${REBAR3_TARGET}" -mkdir -p "${DIR_FOR_BIN}/bin" -chmod +x "${FILE_OUTPUT}" -mv "${FILE_OUTPUT}" "${DIR_FOR_BIN}/bin" -echo "Installed rebar3 version${REBAR3_NIGHTLY} follows" -${DIR_FOR_BIN}/bin/rebar3 version - -echo "INSTALL_DIR_FOR_REBAR3=${RUNNER_TEMP}/${DIR_FOR_BIN}" >> "${GITHUB_ENV}" diff --git a/src/installer.js b/src/installer.js deleted file mode 100644 index d7a61f96..00000000 --- a/src/installer.js +++ /dev/null @@ -1,212 +0,0 @@ -const core = require('@actions/core') -const { exec } = require('@actions/exec') -const tc = require('@actions/tool-cache') -const path = require('path') -const fs = require('fs') -const os = require('os') - -/** - * Install Erlang/OTP. - * - * @param {string} osVersion - * @param {string} otpVersion - * @param {string[]} hexMirrors - */ -async function installOTP(osVersion, otpVersion, hexMirrors) { - let cmd - let args - - if (hexMirrors.length === 0) { - throw new Error( - `Could not install Erlang/OTP ${otpVersion} from any hex.pm mirror`, - ) - } - - const [hexMirror, ...hexMirrorsT] = hexMirrors - const fullVersion = `${osVersion}/${otpVersion}` - let cachedPath = tc.find('otp', fullVersion) - const OS = process.platform - - try { - if (OS === 'linux') { - if (!cachedPath) { - const tarPath = await tc.downloadTool( - `https://builds.hex.pm/builds/otp/${fullVersion}.tar.gz`, - ) - const extractPath = await tc.extractTar(tarPath, undefined, [ - 'zx', - '--strip-components=1', - ]) - cachedPath = await tc.cacheDir(extractPath, 'otp', fullVersion) - } - - cmd = path.join(cachedPath, 'Install') - args = ['-minimal', cachedPath] - await exec(cmd, args) - - const otpPath = path.join(cachedPath, 'bin') - - core.addPath(otpPath) - core.exportVariable('INSTALL_DIR_FOR_OTP', cachedPath) - - core.info('Installed Erlang/OTP version') - cmd = path.join(otpPath, 'erl') - args = ['-version'] - await exec(cmd, args) - } else if (OS === 'win32') { - if (!cachedPath) { - const exePath = await tc.downloadTool( - 'https://github.com/erlang/otp/releases/download/' + - `OTP-${otpVersion}/otp_win64_${otpVersion}.exe`, - ) - cachedPath = await tc.cacheFile(exePath, 'otp.exe', 'otp', fullVersion) - } - - const otpDir = path.join(process.env.RUNNER_TEMP, '.setup-beam', 'otp') - const otpPath = path.join(otpDir, 'bin') - - await fs.promises.mkdir(otpDir, { recursive: true }) - - cmd = path.join(cachedPath, 'otp.exe') - args = ['/S', `/D=${otpDir}`] - await exec(cmd, args) - - core.addPath(otpPath) - core.exportVariable('INSTALL_DIR_FOR_OTP', otpDir) - - core.info('Installed Erlang/OTP version') - - cmd = path.join(otpPath, 'erl.exe') - args = ['+V'] - await exec(cmd, args) - } - } catch (err) { - core.info(`Install OTP failed for mirror ${hexMirror}`) - core.info(`${err}\n${err.stack}`) - await installOTP(osVersion, otpVersion, hexMirrorsT) - } -} - -/** - * Install Elixir. - * - * @param {string} elixirVersion - * @param {string[]} hexMirrors - */ -async function installElixir(elixirVersion, hexMirrors) { - let cmd - let args - let options - - if (hexMirrors.length === 0) { - throw new Error( - `Could not install Elixir ${elixirVersion} from any hex.pm mirror`, - ) - } - const [hexMirror, ...hexMirrorsT] = hexMirrors - - try { - let cachedPath = tc.find('elixir', elixirVersion) - const OS = process.platform - - if (!cachedPath) { - const zipPath = await tc.downloadTool( - `${hexMirror}/builds/elixir/${elixirVersion}.zip`, - ) - const extractPath = await tc.extractZip(zipPath) - cachedPath = await tc.cacheDir(extractPath, 'elixir', elixirVersion) - } - - const elixirPath = path.join(cachedPath, 'bin') - const escriptsPath = path.join(os.homedir(), '.mix', 'escripts') - - core.addPath(elixirPath) - core.addPath(escriptsPath) - core.exportVariable('INSTALL_DIR_FOR_ELIXIR', cachedPath) - - core.info('Installed Elixir version') - - if (debugLoggingEnabled()) { - core.exportVariable('ELIXIR_CLI_ECHO', 'true') - } - - if (OS === 'linux') { - cmd = path.join(elixirPath, 'elixir') - args = ['-v'] - options = {} - } else if (OS === 'win32') { - cmd = path.join(elixirPath, 'elixir.bat') - args = ['-v'] - options = { windowsVerbatimArguments: true } - } - await exec(cmd, args, options) - - await fs.promises.mkdir(escriptsPath, { recursive: true }) - } catch (err) { - core.info(`Elixir install failed for mirror ${hexMirror}`) - core.info(`${err}\n${err.stack}`) - await installElixir(elixirVersion, hexMirrorsT) - } -} - -/** - * Install Gleam. - * - * @param {string} gleamVersion - */ -async function installGleam(gleamVersion) { - let cmd - let args - - const OS = process.platform - if (OS === 'linux') { - cmd = path.join(__dirname, 'install-gleam.sh') - args = [gleamVersion] - await exec(cmd, args) - } else if (OS === 'win32') { - cmd = `pwsh.exe ${path.join(__dirname, 'install-gleam.ps1')}` - args = [`-VSN:${gleamVersion}`] - await exec(cmd, args) - } -} - -/** - * Install rebar3. - * - * @param {string} rebar3Version - */ -async function installRebar3(rebar3Version) { - let cmd - let args - - const OS = process.platform - if (OS === 'linux') { - cmd = path.join(__dirname, 'install-rebar3.sh') - args = [rebar3Version] - await exec(cmd, args) - } else if (OS === 'win32') { - cmd = `pwsh.exe ${path.join(__dirname, 'install-rebar3.ps1')}` - args = [`-VSN:${rebar3Version}`] - await exec(cmd, args) - } -} - -function checkPlatform() { - if (process.platform !== 'linux' && process.platform !== 'win32') { - throw new Error( - '@erlef/setup-beam only supports Ubuntu and Windows at this time', - ) - } -} - -function debugLoggingEnabled() { - return !!process.env.RUNNER_DEBUG -} - -module.exports = { - installOTP, - installElixir, - installGleam, - installRebar3, - checkPlatform, -} diff --git a/src/setup-beam.js b/src/setup-beam.js index e752bfdf..015f3fc1 100644 --- a/src/setup-beam.js +++ b/src/setup-beam.js @@ -1,17 +1,18 @@ const core = require('@actions/core') const { exec } = require('@actions/exec') +const tc = require('@actions/tool-cache') const http = require('@actions/http-client') const path = require('path') const semver = require('semver') const fs = require('fs') -const installer = require('./installer') +const os = require('os') main().catch((err) => { core.setFailed(err.message) }) async function main() { - installer.checkPlatform() + checkPlatform() const versionFilePath = getInput('version-file', false) let versions @@ -29,24 +30,16 @@ async function main() { const elixirSpec = getInput('elixir-version', false, 'elixir', versions) const gleamSpec = getInput('gleam-version', false, 'gleam', versions) const rebar3Spec = getInput('rebar3-version', false, 'rebar', versions) - const hexMirrors = core.getMultilineInput('hexpm-mirrors', { - required: false, - }) if (otpSpec !== 'false') { - await installOTP(otpSpec, osVersion, hexMirrors) - - const elixirInstalled = await maybeInstallElixir( - elixirSpec, - otpSpec, - hexMirrors, - ) + await installOTP(otpSpec, osVersion) + const elixirInstalled = await maybeInstallElixir(elixirSpec, otpSpec) if (elixirInstalled === true) { const shouldMixRebar = getInput('install-rebar', false) - await mix(shouldMixRebar, 'rebar', hexMirrors) + await mix(shouldMixRebar, 'rebar') const shouldMixHex = getInput('install-hex', false) - await mix(shouldMixHex, 'hex', hexMirrors) + await mix(shouldMixHex, 'hex') } } else if (!gleamSpec) { throw new Error('otp-version=false is only available when installing Gleam') @@ -56,38 +49,43 @@ async function main() { await maybeInstallRebar3(rebar3Spec) } -async function installOTP(otpSpec, osVersion, hexMirrors) { - const otpVersion = await getOTPVersion(otpSpec, osVersion, hexMirrors) +async function installOTP(otpSpec, osVersion) { + const otpVersion = await getOTPVersion(otpSpec, osVersion) core.startGroup(`Installing Erlang/OTP ${otpVersion} - built on ${osVersion}`) - await installer.installOTP(osVersion, otpVersion, hexMirrors) + await doWithMirrors({ + hexMirrors: hexMirrorsInput(), + actionTitle: `install Erlang/OTP ${otpVersion}`, + action: async (hexMirror) => { + await install('otp', { + osVersion, + toolVersion: otpVersion, + hexMirror, + }) + }, + }) core.setOutput('otp-version', otpVersion) core.endGroup() return otpVersion } -async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) { +async function maybeInstallElixir(elixirSpec, otpSpec) { let installed = false if (elixirSpec) { - const elixirVersion = await getElixirVersion( - elixirSpec, - otpSpec, - hexMirrors, - ) + const elixirVersion = await getElixirVersion(elixirSpec, otpSpec) core.startGroup(`Installing Elixir ${elixirVersion}`) - await installer.installElixir(elixirVersion, hexMirrors) + await doWithMirrors({ + hexMirrors: hexMirrorsInput(), + actionTitle: `install Elixir ${elixirVersion}`, + action: async (hexMirror) => { + await install('elixir', { + toolVersion: elixirVersion, + hexMirror, + }) + }, + }) core.setOutput('elixir-version', elixirVersion) - - const disableProblemMatchers = getInput('disable_problem_matchers', false) - if (disableProblemMatchers === 'false') { - const elixirMatchers = path.join( - __dirname, - '..', - 'matchers', - 'elixir-matchers.json', - ) - core.info(`##[add-matcher]${elixirMatchers}`) - } + maybeEnableElixirProblemMatchers() core.endGroup() installed = true @@ -96,30 +94,32 @@ async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) { return installed } -async function mixWithMirrors(cmd, args, hexMirrors) { - if (hexMirrors.length === 0) { - throw new Error('mix failed with every mirror') - } - - const [hexMirror, ...hexMirrorsT] = hexMirrors - process.env.HEX_MIRROR = hexMirror - try { - return await exec(cmd, args) - } catch (err) { - core.info( - `mix failed with mirror ${process.env.HEX_MIRROR} with message ${err.message})`, +function maybeEnableElixirProblemMatchers() { + const disableProblemMatchers = getInput('disable_problem_matchers', false) + if (disableProblemMatchers === 'false') { + const elixirMatchers = path.join( + __dirname, + '..', + 'matchers', + 'elixir-matchers.json', ) + core.info(`##[add-matcher]${elixirMatchers}`) } - - return mixWithMirrors(cmd, args, hexMirrorsT) } -async function mix(shouldMix, what, hexMirrors) { +async function mix(shouldMix, what) { if (shouldMix === 'true') { const cmd = 'mix' const args = [`local.${what}`, '--force'] core.startGroup(`Running ${cmd} ${args}`) - await mixWithMirrors(cmd, args, hexMirrors) + await doWithMirrors({ + hexMirrors: hexMirrorsInput(), + actionTitle: `mix ${what}`, + action: async (hexMirror) => { + process.env.HEX_MIRROR = hexMirror + await exec(cmd, args) + }, + }) core.endGroup() } } @@ -129,7 +129,7 @@ async function maybeInstallGleam(gleamSpec) { if (gleamSpec) { const gleamVersion = await getGleamVersion(gleamSpec) core.startGroup(`Installing Gleam ${gleamVersion}`) - await installer.installGleam(gleamVersion) + await install('gleam', { toolVersion: gleamVersion }) core.setOutput('gleam-version', gleamVersion) core.addPath(`${process.env.RUNNER_TEMP}/.setup-beam/gleam/bin`) core.endGroup() @@ -150,7 +150,7 @@ async function maybeInstallRebar3(rebar3Spec) { rebar3Version = await getRebar3Version(rebar3Spec) } core.startGroup(`Installing rebar3 ${rebar3Version}`) - await installer.installRebar3(rebar3Version) + await install('rebar3', { toolVersion: rebar3Version }) core.setOutput('rebar3-version', rebar3Version) core.addPath(`${process.env.RUNNER_TEMP}/.setup-beam/rebar3/bin`) core.endGroup() @@ -161,9 +161,9 @@ async function maybeInstallRebar3(rebar3Spec) { return installed } -async function getOTPVersion(otpSpec0, osVersion, hexMirrors) { - const otpVersions = await getOTPVersions(osVersion, hexMirrors) - const spec = otpSpec0.replace(/^OTP-/, '') +async function getOTPVersion(otpSpec0, osVersion) { + const otpVersions = await getOTPVersions(osVersion) + let spec = otpSpec0.replace(/^OTP-/, '') const versions = otpVersions const otpVersion = getVersionFromSpec(spec, versions) if (otpVersion === null) { @@ -176,15 +176,15 @@ async function getOTPVersion(otpSpec0, osVersion, hexMirrors) { return otpVersion // from the reference, for download } -async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) { +async function getElixirVersion(exSpec0, otpVersion0) { const otpVersion = otpVersion0.match(/^([^-]+-)?(.+)$/)[2] const otpVersionMajor = otpVersion.match(/^([^.]+).*$/)[1] - const [otpVersionsForElixirMap, elixirVersions] = await getElixirVersions( - hexMirrors, - ) + + const [otpVersionsForElixirMap, elixirVersions] = await getElixirVersions() const spec = exSpec0.replace(/-otp-.*$/, '') const versions = elixirVersions const elixirVersionFromSpec = getVersionFromSpec(spec, versions) + if (elixirVersionFromSpec === null) { throw new Error( `Requested Elixir version (${exSpec0}) not found in version list ` + @@ -248,12 +248,19 @@ async function getRebar3Version(r3Spec) { return rebar3Version } -async function getOTPVersions(osVersion, hexMirrors) { +async function getOTPVersions(osVersion) { let otpVersionsListings let originListing if (process.platform === 'linux') { originListing = `/builds/otp/${osVersion}/builds.txt` - otpVersionsListings = await getWithMirrors(originListing, hexMirrors) + otpVersionsListings = await doWithMirrors({ + hexMirrors: hexMirrorsInput(), + actionTitle: `fetch ${originListing}`, + action: async (hexMirror) => { + const l = await get(`${hexMirror}${originListing}`, [null]) + return l + }, + }) } else if (process.platform === 'win32') { originListing = 'https://api.github.com/repos/erlang/otp/releases?per_page=100' @@ -296,11 +303,16 @@ async function getOTPVersions(osVersion, hexMirrors) { return otpVersions } -async function getElixirVersions(hexMirrors) { - const elixirVersionsListings = await getWithMirrors( - '/builds/elixir/builds.txt', - hexMirrors, - ) +async function getElixirVersions() { + const originListing = '/builds/elixir/builds.txt' + const elixirVersionsListings = await doWithMirrors({ + hexMirrors: hexMirrorsInput(), + actionTitle: `fetch ${originListing}`, + action: async (hexMirror) => { + const l = await get(`${hexMirror}${originListing}`, [null]) + return l + }, + }) const otpVersionsForElixirMap = {} const elixirVersions = {} @@ -428,6 +440,7 @@ function maybeCoerced(v) { } } catch { // some stuff can't be coerced, like 'main' + core.debug(`Was not able to coerce ${v} with semver`) ret = v } @@ -517,21 +530,6 @@ async function get(url0, pageIdxs) { return Promise.all(pageIdxs.map(getPage)) } -async function getWithMirrors(resourcePath, hexMirrors) { - if (hexMirrors.length === 0) { - throw new Error(`Could not fetch ${resourcePath} from any hex.pm mirror`) - } - - const [hexMirror, ...hexMirrorsT] = hexMirrors - try { - return await get(`${hexMirror}${resourcePath}`, [null]) - } catch (err) { - core.info(`get failed for URL ${hexMirror}${resourcePath}`) - } - - return getWithMirrors(resourcePath, hexMirrorsT) -} - function maybePrependWithV(v) { if (isVersion(v)) { return `v${v.replace('v', '')}` @@ -652,11 +650,333 @@ function debugLog(groupName, message) { ) } +function hexMirrorsInput() { + return core.getMultilineInput('hexpm-mirrors', { + required: false, + }) +} + +async function doWithMirrors(opts) { + const { hexMirrors, actionTitle, action } = opts + let actionRes + + if (hexMirrors.length === 0) { + throw new Error(`Could not ${actionTitle} from any hex.pm mirror`) + } + + const [hexMirror, ...hexMirrorsT] = hexMirrors + try { + actionRes = await action(hexMirror) + } catch (err) { + core.info( + `Action ${actionTitle} failed for mirror ${hexMirror}, with ${err}`, + ) + core.debug(`Stacktrace: ${err.stack}`) + actionRes = await doWithMirrors({ + hexMirrors: hexMirrorsT, + actionTitle, + action, + }) + } + + return actionRes +} + +async function install(toolName, opts) { + const { osVersion, toolVersion, hexMirror } = opts + const versionSpec = + osVersion !== undefined ? `${osVersion}/${toolVersion}` : toolVersion + let installOpts + + // The installOpts object is composed of supported processPlatform keys + // (e.g. 'linux', 'win32', or 'all' - in case there's no distinction between platforms) + // In each of these keys there's an object with keys: + // * downloadToolURL + // - where to fetch the downloadable from + // * extract + // - if the downloadable is compressed: how to extract it + // - return ['dir', targetDir] + // - if the downloadable is not compressed: a filename.ext you want to cache it under + // - return ['file', filenameWithExt] + // * postExtract + // - stuff to execute outside the cache scope (just after it's created) + // * reportVersion + /// - configuration elements on how to output the tool version, post-install + + switch (toolName) { + case 'otp': + installOpts = { + tool: 'Erlang/OTP', + linux: { + downloadToolURL: () => + `${hexMirror}/builds/otp/${versionSpec}.tar.gz`, + extract: async (file) => { + const dest = undefined + const flags = ['zx', '--strip-components=1'] + const targetDir = await tc.extractTar(file, dest, flags) + + return ['dir', targetDir] + }, + postExtract: async (cachePath) => { + const cmd = path.join(cachePath, 'Install') + const args = ['-minimal', cachePath] + await exec(cmd, args) + }, + reportVersion: () => { + const cmd = 'erl' + const args = ['-version'] + + return [cmd, args] + }, + }, + win32: { + downloadToolURL: () => + 'https://github.com/erlang/otp/releases/download/' + + `OTP-${toolVersion}/otp_win64_${toolVersion}.exe`, + extract: async () => ['file', 'otp.exe'], + postExtract: async (cachePath) => { + const cmd = path.join(cachePath, 'otp.exe') + const args = ['/S', `/D=${cachePath}`] + await exec(cmd, args) + }, + reportVersion: () => { + const cmd = 'erl.exe' + const args = ['+V'] + + return [cmd, args] + }, + }, + } + break + case 'elixir': + installOpts = { + tool: 'Elixir', + all: { + downloadToolURL: () => + `${hexMirror}/builds/elixir/${versionSpec}.zip`, + extract: async (file) => { + const targetDir = await tc.extractZip(file) + + return ['dir', targetDir] + }, + postExtract: async () => { + const escriptsPath = path.join(os.homedir(), '.mix', 'escripts') + fs.mkdirSync(escriptsPath, { recursive: true }) + core.addPath(escriptsPath) + + if (debugLoggingEnabled()) { + core.exportVariable('ELIXIR_CLI_ECHO', 'true') + } + }, + reportVersion: () => { + const cmd = 'elixir' + const args = ['-v'] + + return [cmd, args] + }, + }, + } + break + case 'gleam': + installOpts = { + tool: 'Gleam', + linux: { + downloadToolURL: () => { + let gz + if ( + versionSpec === 'nightly' || + semver.gt(versionSpec, 'v0.22.1') + ) { + gz = `gleam-${versionSpec}-x86_64-unknown-linux-musl.tar.gz` + } else { + gz = `gleam-${versionSpec}-linux-amd64.tar.gz` + } + + return `https://github.com/gleam-lang/gleam/releases/download/${versionSpec}/${gz}` + }, + extract: async (file) => { + const dest = undefined + const flags = ['zx'] + const targetDir = await tc.extractTar(file, dest, flags) + + return ['dir', targetDir] + }, + postExtract: async (cachePath) => { + const bindir = path.join(cachePath, 'bin') + const oldPath = path.join(cachePath, 'gleam') + const newPath = path.join(bindir, 'gleam') + fs.mkdirSync(bindir) + fs.renameSync(oldPath, newPath) + }, + reportVersion: () => { + const cmd = 'gleam' + const args = ['--version'] + + return [cmd, args] + }, + }, + win32: { + downloadToolURL: () => { + let zip + if ( + versionSpec === 'nightly' || + semver.gt(versionSpec, 'v0.22.1') + ) { + zip = `gleam-${versionSpec}-x86_64-pc-windows-msvc.zip` + } else { + zip = `gleam-${versionSpec}-windows-64bit.zip` + } + + return `https://github.com/gleam-lang/gleam/releases/download/${versionSpec}/${zip}` + }, + extract: async (file) => { + const targetDir = await tc.extractZip(file) + + return ['dir', targetDir] + }, + postExtract: async (cachePath) => { + const bindir = path.join(cachePath, 'bin') + const oldPath = path.join(cachePath, 'gleam.exe') + const newPath = path.join(bindir, 'gleam.exe') + fs.mkdirSync(bindir) + fs.renameSync(oldPath, newPath) + }, + reportVersion: () => { + const cmd = 'gleam.exe' + const args = ['--version'] + + return [cmd, args] + }, + }, + } + break + case 'rebar3': + installOpts = { + tool: 'Rebar3', + linux: { + downloadToolURL: () => { + let url + if (versionSpec === 'nightly') { + url = 'https://s3.amazonaws.com/rebar3-nightly/rebar3' + } else { + url = `https://github.com/erlang/rebar3/releases/download/${versionSpec}/rebar3` + } + + return url + }, + extract: async () => ['file', 'rebar3'], + postExtract: async (cachePath) => { + const bindir = path.join(cachePath, 'bin') + const oldPath = path.join(cachePath, 'rebar3') + const newPath = path.join(bindir, 'rebar3') + fs.mkdirSync(bindir) + fs.renameSync(oldPath, newPath) + fs.chmodSync(newPath, 0o755) + }, + reportVersion: () => { + const cmd = 'rebar3' + const args = ['version'] + + return [cmd, args] + }, + }, + win32: { + downloadToolURL: () => { + let url + if (versionSpec === 'nightly') { + url = 'https://s3.amazonaws.com/rebar3-nightly/rebar3' + } else { + url = `https://github.com/erlang/rebar3/releases/download/${versionSpec}/rebar3` + } + + return url + }, + extract: async () => ['file', 'rebar3'], + postExtract: async (cachePath) => { + const bindir = path.join(cachePath, 'bin') + const oldPath = path.join(cachePath, 'rebar3') + fs.mkdirSync(bindir) + fs.chmodSync(oldPath, 0o755) + + const ps1Filename = path.join(bindir, 'rebar3.ps1') + fs.writeFileSync(ps1Filename, `& escript.exe ${oldPath} \${args}`) + + const cmdFilename = path.join(bindir, 'rebar3.cmd') + fs.writeFileSync( + cmdFilename, + `@echo off\r\nescript.exe ${oldPath} %*`, + ) + }, + reportVersion: () => { + const cmd = 'rebar3.cmd' + const args = ['version'] + + return [cmd, args] + }, + }, + } + break + default: + throw new Error(`no installer for ${toolName}`) + } + + await installTool({ toolName, versionSpec, installOpts }) +} + +async function installTool(opts) { + const { toolName, versionSpec, installOpts } = opts + const platformOpts = installOpts[process.platform] || installOpts.all + let cachePath = tc.find(toolName, versionSpec) + + core.debug(`Checking if ${installOpts.tool} is already cached...`) + if (cachePath === '') { + core.debug(" ... it isn't!") + const downloadToolURL = platformOpts.downloadToolURL() + const file = await tc.downloadTool(downloadToolURL) + const [targetElemType, targetElem] = await platformOpts.extract(file) + + if (targetElemType === 'dir') { + cachePath = await tc.cacheDir(targetElem, toolName, versionSpec) + } else if (targetElemType === 'file') { + cachePath = await tc.cacheFile(file, targetElem, toolName, versionSpec) + } + } else { + core.debug(` ... it is, at ${cachePath}`) + } + + core.debug('Performing post extract operations...') + await platformOpts.postExtract(cachePath) + + core.debug(`Adding ${cachePath}'s bin to system path`) + core.addPath(path.join(cachePath, 'bin')) + + const installDirForVarName = `INSTALL_DIR_FOR_${toolName}`.toUpperCase() + core.debug(`Exporting ${installDirForVarName} as ${cachePath}`) + core.exportVariable(installDirForVarName, cachePath) + + core.info(`Installed ${installOpts.tool} version`) + const [cmd, args] = platformOpts.reportVersion() + await exec(cmd, args) +} + +function checkPlatform() { + if (process.platform !== 'linux' && process.platform !== 'win32') { + throw new Error( + '@erlef/setup-beam only supports Ubuntu and Windows at this time', + ) + } +} + +function debugLoggingEnabled() { + return !!process.env.RUNNER_DEBUG +} + module.exports = { getOTPVersion, getElixirVersion, getGleamVersion, getRebar3Version, getVersionFromSpec, + install, parseVersionFile, } diff --git a/test/problem-matchers.test.js b/test/problem-matchers.test.js deleted file mode 100644 index 5e21011f..00000000 --- a/test/problem-matchers.test.js +++ /dev/null @@ -1,82 +0,0 @@ -const assert = require('assert') -const core = require('@actions/core') -const { problemMatcher } = require('../matchers/elixir-matchers.json') - -async function all() { - await testElixirMixCompileError() - await testElixirMixCompileWarning() - await testElixirMixTestFailure() - await testElixirCredoOutputDefault() -} - -async function testElixirMixCompileError() { - const [matcher] = problemMatcher.find( - ({ owner }) => owner === 'elixir-mixCompileError', - ).pattern - - const output = '** (CompileError) lib/test.ex:16: undefined function err/0' - const [message, , file, line] = output.match(matcher.regexp) - assert.equal(file, 'lib/test.ex') - assert.equal(line, '16') - assert.equal(message, output) -} - -async function testElixirMixCompileWarning() { - const [messagePattern, filePattern] = problemMatcher.find( - ({ owner }) => owner === 'elixir-mixCompileWarning', - ).pattern - - const firstOutput = - 'warning: variable "err" does not exist and is being expanded to "err()"' - const secondOutput = ' lib/test.ex:16: Test.hello/0' - - const [, message] = firstOutput.match(messagePattern.regexp) - assert.equal( - message, - 'variable "err" does not exist and is being expanded to "err()"', - ) - - const [, file, line] = secondOutput.match(filePattern.regexp) - assert.equal(file, 'lib/test.ex') - assert.equal(line, '16') -} - -async function testElixirMixTestFailure() { - const [messagePattern, filePattern] = problemMatcher.find( - ({ owner }) => owner === 'elixir-mixTestFailure', - ).pattern - - const firstOutput = '1) test throws (TestTest)' - const secondOutput = ' test/test_test.exs:9' - - const [, message] = firstOutput.match(messagePattern.regexp) - assert.equal(message, 'test throws (TestTest)') - - const [, file, line] = secondOutput.match(filePattern.regexp) - assert.equal(file, 'test/test_test.exs') - assert.equal(line, '9') -} - -async function testElixirCredoOutputDefault() { - const [messagePattern, filePattern] = problemMatcher.find( - ({ owner }) => owner === 'elixir-credoOutputDefault', - ).pattern - - const firstOutput = '┃ [F] → Function is too complex (CC is 29, max is 9).' - const secondOutput = '┃ lib/test.ex:15:7 #(Test.hello)' - - const [, message] = firstOutput.match(messagePattern.regexp) - assert.equal(message, 'Function is too complex (CC is 29, max is 9).') - - const [, file, line, column] = secondOutput.match(filePattern.regexp) - assert.equal(file, 'lib/test.ex') - assert.equal(line, '15') - assert.equal(column, '7') -} - -all() - .then(() => process.exit(0)) - .catch((err) => { - core.error(err) - process.exit(1) - }) diff --git a/test/setup-beam.test.js b/test/setup-beam.test.js index 4e8b628d..539f8008 100644 --- a/test/setup-beam.test.js +++ b/test/setup-beam.test.js @@ -4,13 +4,13 @@ simulateInput('rebar3-version', '3.20') simulateInput('install-rebar', 'true') simulateInput('install-hex', 'true') simulateInput('github-token', process.env.GITHUB_TOKEN) -simulateInput('hexpm-mirrors', ['https://builds.hex.pm']) +simulateInput('hexpm-mirrors', 'https://builds.hex.pm', { multiline: true }) const assert = require('assert') const fs = require('fs') const core = require('@actions/core') const setupBeam = require('../src/setup-beam') -const installer = require('../src/installer') +const { problemMatcher } = require('../matchers/elixir-matchers.json') async function all() { await testFailInstallOTP() @@ -26,6 +26,11 @@ async function all() { await testGetVersionFromSpec() await testParseVersionFile() + + await testElixirMixCompileError() + await testElixirMixCompileWarning() + await testElixirMixTestFailure() + await testElixirCredoOutputDefault() } async function testFailInstallOTP() { @@ -33,7 +38,11 @@ async function testFailInstallOTP() { const otpVersion = 'OTP-23.2' assert.rejects( async () => { - await installer.installOTP(otpOSVersion, otpVersion) + await setupBeam.install('otp', { + hexMirror: 'https://builds.hex.pm', + otpOSVersion, + otpVersion, + }) }, (err) => { assert.ok(err instanceof Error) @@ -49,7 +58,10 @@ async function testFailInstallElixir() { exVersion = '0.11' assert.rejects( async () => { - await installer.installElixir(exVersion) + await setupBeam.install('elixir', { + hexMirror: 'https://builds.hex.pm', + exVersion, + }) }, (err) => { assert.ok(err instanceof Error) @@ -61,7 +73,10 @@ async function testFailInstallElixir() { exVersion = 'v1.0.0-otp-17' assert.rejects( async () => { - await installer.installElixir(exVersion) + await setupBeam.install('elixir', { + hexMirror: 'https://builds.hex.pm', + exVersion, + }) }, (err) => { assert.ok(err instanceof Error) @@ -75,7 +90,7 @@ async function testFailInstallGleam() { const gleamVersion = '0.1.3' assert.rejects( async () => { - await installer.installGleam(gleamVersion) + await setupBeam.install('gleam', { gleamVersion }) }, (err) => { assert.ok(err instanceof Error) @@ -89,7 +104,7 @@ async function testFailInstallRebar3() { const r3Version = '0.14.4' assert.rejects( async () => { - await installer.installRebar3(r3Version) + await setupBeam.install('rebar3', { r3Version }) }, (err) => { assert.ok(err instanceof Error) @@ -105,7 +120,11 @@ async function testOTPVersions() { let spec let osVersion let before - const hexMirrors = ['https://repo.hex.pm', 'https://cdn.jsdelivr.net/hex'] + const hexMirrors = simulateInput( + 'hexpm-mirrors', + 'https://repo.hex.pm, https://cdn.jsdelivr.net/hex', + { multiline: true }, + ) if (process.platform === 'linux') { before = simulateInput('version-type', 'strict') @@ -119,25 +138,25 @@ async function testOTPVersions() { spec = '19.3.x' osVersion = 'ubuntu-16.04' expected = 'OTP-19.3.6.13' - got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) + got = await setupBeam.getOTPVersion(spec, osVersion) assert.deepStrictEqual(got, expected) spec = '^19.3.6' osVersion = 'ubuntu-16.04' expected = 'OTP-19.3.6.13' - got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) + got = await setupBeam.getOTPVersion(spec, osVersion) assert.deepStrictEqual(got, expected) spec = '^19.3' osVersion = 'ubuntu-18.04' expected = 'OTP-19.3.6.13' - got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) + got = await setupBeam.getOTPVersion(spec, osVersion) assert.deepStrictEqual(got, expected) spec = '20' osVersion = 'ubuntu-20.04' expected = 'OTP-20.3.8.26' - got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) + got = await setupBeam.getOTPVersion(spec, osVersion) assert.deepStrictEqual(got, expected) spec = '20.3.8.26' @@ -149,19 +168,19 @@ async function testOTPVersions() { spec = '20.x' osVersion = 'ubuntu-20.04' expected = 'OTP-20.3.8.26' - got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) + got = await setupBeam.getOTPVersion(spec, osVersion) assert.deepStrictEqual(got, expected) spec = '20.0' osVersion = 'ubuntu-20.04' expected = 'OTP-20.0.5' - got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) + got = await setupBeam.getOTPVersion(spec, osVersion) assert.deepStrictEqual(got, expected) spec = '20.0.x' osVersion = 'ubuntu-20.04' expected = 'OTP-20.0.5' - got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) + got = await setupBeam.getOTPVersion(spec, osVersion) assert.deepStrictEqual(got, expected) } @@ -169,21 +188,23 @@ async function testOTPVersions() { spec = '24.0.1' osVersion = 'windows-latest' expected = '24.0.1' - got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) + got = await setupBeam.getOTPVersion(spec, osVersion) assert.deepStrictEqual(got, expected) spec = '23.2.x' osVersion = 'windows-2016' expected = '23.2.7' - got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) + got = await setupBeam.getOTPVersion(spec, osVersion) assert.deepStrictEqual(got, expected) spec = '23.0' osVersion = 'windows-2019' expected = '23.0.4' - got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) + got = await setupBeam.getOTPVersion(spec, osVersion) assert.deepStrictEqual(got, expected) } + + simulateInput('hexpm-mirrors', hexMirrors, { multiline: true }) } async function testElixirVersions() { @@ -192,31 +213,33 @@ async function testElixirVersions() { let spec let otpVersion let before - const hexMirrors = ['https://repo.hex.pm'] + const hexMirrors = simulateInput('hexpm-mirrors', 'https://repo.hex.pm', { + multiline: true, + }) spec = '1.1.x' otpVersion = 'OTP-17' expected = 'v1.1.1-otp-17' - got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) + got = await setupBeam.getElixirVersion(spec, otpVersion) assert.deepStrictEqual(got, expected) spec = '1.10.4' otpVersion = 'OTP-23' expected = 'v1.10.4-otp-23' - got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) + got = await setupBeam.getElixirVersion(spec, otpVersion) assert.deepStrictEqual(got, expected) spec = '1.12.1' otpVersion = 'OTP-24.0.2' expected = 'v1.12.1-otp-24' - got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) + got = await setupBeam.getElixirVersion(spec, otpVersion) assert.deepStrictEqual(got, expected) before = simulateInput('version-type', 'strict') spec = '1.14.0' otpVersion = 'main' expected = 'v1.14.0' - got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) + got = await setupBeam.getElixirVersion(spec, otpVersion) assert.deepStrictEqual(got, expected) simulateInput('version-type', before) @@ -224,7 +247,7 @@ async function testElixirVersions() { spec = 'v1.11.0-rc.0' otpVersion = 'OTP-23' expected = 'v1.11.0-rc.0-otp-23' - got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) + got = await setupBeam.getElixirVersion(spec, otpVersion) assert.deepStrictEqual(got, expected) simulateInput('version-type', before) @@ -232,7 +255,7 @@ async function testElixirVersions() { spec = 'v1.11.0-rc.0-otp-23' otpVersion = 'OTP-23' expected = 'v1.11.0-rc.0-otp-23' - got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) + got = await setupBeam.getElixirVersion(spec, otpVersion) assert.deepStrictEqual(got, expected) simulateInput('version-type', before) @@ -240,7 +263,7 @@ async function testElixirVersions() { spec = 'v1.11.0' otpVersion = '22.3.4.2' expected = 'v1.11.0-otp-22' - got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) + got = await setupBeam.getElixirVersion(spec, otpVersion) assert.deepStrictEqual(got, expected) simulateInput('version-type', before) @@ -248,9 +271,11 @@ async function testElixirVersions() { spec = 'main' otpVersion = '23.1' expected = 'main-otp-23' - got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) + got = await setupBeam.getElixirVersion(spec, otpVersion) assert.deepStrictEqual(got, expected) simulateInput('version-type', before) + + simulateInput('hexpm-mirrors', hexMirrors, { multiline: true }) } async function testGleamVersions() { @@ -499,16 +524,16 @@ rebar ${rebar3}` assert.strictEqual(appVersions.get('elixir'), elixir) assert.ok(async () => { - await installer.installOTP(erlang) + await setupBeam.install('otp', { toolVersion: erlang }) }) assert.ok(async () => { - await installer.installElixir(elixir) + await setupBeam.install('elixir', { toolVersion: elixir }) }) assert.ok(async () => { - await installer.installGleam(gleam) + await setupBeam.install('gleam', { toolVersion: gleam }) }) assert.ok(async () => { - await installer.installRebar3(rebar3) + await setupBeam.install('rebar3', { toolVersion: rebar3 }) }) simulateInput('otp-version', otpVersion) @@ -517,12 +542,86 @@ rebar ${rebar3}` simulateInput('rebar3-version', rebar3Version) } +async function testElixirMixCompileError() { + const [matcher] = problemMatcher.find( + ({ owner }) => owner === 'elixir-mixCompileError', + ).pattern + + const output = '** (CompileError) lib/test.ex:16: undefined function err/0' + const [message, , file, line] = output.match(matcher.regexp) + assert.equal(file, 'lib/test.ex') + assert.equal(line, '16') + assert.equal(message, output) +} + +async function testElixirMixCompileWarning() { + const [messagePattern, filePattern] = problemMatcher.find( + ({ owner }) => owner === 'elixir-mixCompileWarning', + ).pattern + + const firstOutput = + 'warning: variable "err" does not exist and is being expanded to "err()"' + const secondOutput = ' lib/test.ex:16: Test.hello/0' + + const [, message] = firstOutput.match(messagePattern.regexp) + assert.equal( + message, + 'variable "err" does not exist and is being expanded to "err()"', + ) + + const [, file, line] = secondOutput.match(filePattern.regexp) + assert.equal(file, 'lib/test.ex') + assert.equal(line, '16') +} + +async function testElixirMixTestFailure() { + const [messagePattern, filePattern] = problemMatcher.find( + ({ owner }) => owner === 'elixir-mixTestFailure', + ).pattern + + const firstOutput = '1) test throws (TestTest)' + const secondOutput = ' test/test_test.exs:9' + + const [, message] = firstOutput.match(messagePattern.regexp) + assert.equal(message, 'test throws (TestTest)') + + const [, file, line] = secondOutput.match(filePattern.regexp) + assert.equal(file, 'test/test_test.exs') + assert.equal(line, '9') +} + +async function testElixirCredoOutputDefault() { + const [messagePattern, filePattern] = problemMatcher.find( + ({ owner }) => owner === 'elixir-credoOutputDefault', + ).pattern + + const firstOutput = '┃ [F] → Function is too complex (CC is 29, max is 9).' + const secondOutput = '┃ lib/test.ex:15:7 #(Test.hello)' + + const [, message] = firstOutput.match(messagePattern.regexp) + assert.equal(message, 'Function is too complex (CC is 29, max is 9).') + + const [, file, line, column] = secondOutput.match(filePattern.regexp) + assert.equal(file, 'lib/test.ex') + assert.equal(line, '15') + assert.equal(column, '7') +} + function unsimulateInput(key) { return simulateInput(key, '') } -function simulateInput(key, value) { - const before = process.env[input(key)] +function simulateInput(key, value0, opts) { + const { multiline } = opts || {} + const before = process.env[input(key, opts)] + let value = value0 + if (multiline) { + if (value.indexOf(', ') !== -1) { + value = value0.replace(/, /g, '\n') + } else { + value = value0.replace(/\n/g, ', ') + } + } process.env[input(key)] = value return before }