mirror of
https://github.com/erlef/setup-beam.git
synced 2026-07-22 22:56:07 +00:00
54d4f15c75
* Link to the elixir/erlang compatibility page Currently when a user encounters an incompatibility between the requested elixir and erlang versions it isn't clear how to figure out what the compatible versions are. This is the current error as shown to users: > Error: Requested Elixir / Erlang/OTP version (1.11.3 / 24.2) not found in version list (did you check Compatibility between Elixir and Erlang/OTP?) Example failure: https://github.com/ScenicFramework/scenic/pull/318/checks This PR adds a link to https://hexdocs.pm/elixir/compatibility-and-deprecations.html so user's can easily see what versions are compatible. * Update dist/index.js * Act on review comment: on > at --------- Co-authored-by: Paulo F. Oliveira <paulo.ferraz.oliveira@gmail.com>
585 lines
16 KiB
JavaScript
585 lines
16 KiB
JavaScript
const core = require('@actions/core')
|
|
const { exec } = require('@actions/exec')
|
|
const http = require('@actions/http-client')
|
|
const path = require('path')
|
|
const semver = require('semver')
|
|
const fs = require('fs')
|
|
const installer = require('./installer')
|
|
|
|
main().catch((err) => {
|
|
core.setFailed(err.message)
|
|
})
|
|
|
|
async function main() {
|
|
installer.checkPlatform()
|
|
|
|
const versionFilePath = getInput('version-file', false)
|
|
let versions
|
|
if (versionFilePath) {
|
|
if (!isStrictVersion()) {
|
|
throw new Error(
|
|
"you have to set version-type=strict if you're using version-file",
|
|
)
|
|
}
|
|
versions = parseVersionFile(versionFilePath)
|
|
}
|
|
|
|
const osVersion = getRunnerOSVersion()
|
|
const otpSpec = getInput('otp-version', true, 'erlang', versions)
|
|
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,
|
|
)
|
|
|
|
if (elixirInstalled === true) {
|
|
const shouldMixRebar = getInput('install-rebar', false)
|
|
await mix(shouldMixRebar, 'rebar', hexMirrors)
|
|
const shouldMixHex = getInput('install-hex', false)
|
|
await mix(shouldMixHex, 'hex', hexMirrors)
|
|
}
|
|
} else if (!gleamSpec) {
|
|
throw new Error('otp-version=false is only available when installing Gleam')
|
|
}
|
|
|
|
await maybeInstallGleam(gleamSpec)
|
|
await maybeInstallRebar3(rebar3Spec)
|
|
}
|
|
|
|
async function installOTP(otpSpec, osVersion, hexMirrors) {
|
|
const otpVersion = await getOTPVersion(otpSpec, osVersion, hexMirrors)
|
|
core.startGroup(`Installing Erlang/OTP ${otpVersion} - built on ${osVersion}`)
|
|
await installer.installOTP(osVersion, otpVersion, hexMirrors)
|
|
core.setOutput('otp-version', otpVersion)
|
|
core.endGroup()
|
|
|
|
return otpVersion
|
|
}
|
|
|
|
async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) {
|
|
let installed = false
|
|
|
|
if (elixirSpec) {
|
|
const elixirVersion = await getElixirVersion(
|
|
elixirSpec,
|
|
otpSpec,
|
|
hexMirrors,
|
|
)
|
|
core.startGroup(`Installing Elixir ${elixirVersion}`)
|
|
await installer.installElixir(elixirVersion, hexMirrors)
|
|
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}`)
|
|
}
|
|
core.endGroup()
|
|
|
|
installed = true
|
|
}
|
|
|
|
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})`,
|
|
)
|
|
}
|
|
return mixWithMirrors(cmd, args, hexMirrorsT)
|
|
}
|
|
|
|
async function mix(shouldMix, what, hexMirrors) {
|
|
if (shouldMix === 'true') {
|
|
const cmd = 'mix'
|
|
const args = [`local.${what}`, '--force']
|
|
core.startGroup(`Running ${cmd} ${args}`)
|
|
await mixWithMirrors(cmd, args, hexMirrors)
|
|
core.endGroup()
|
|
}
|
|
}
|
|
|
|
async function maybeInstallGleam(gleamSpec) {
|
|
let installed = false
|
|
|
|
if (gleamSpec) {
|
|
const gleamVersion = await getGleamVersion(gleamSpec)
|
|
core.startGroup(`Installing Gleam ${gleamVersion}`)
|
|
await installer.installGleam(gleamVersion)
|
|
core.setOutput('gleam-version', gleamVersion)
|
|
core.addPath(`${process.env.RUNNER_TEMP}/.setup-beam/gleam/bin`)
|
|
core.endGroup()
|
|
|
|
installed = true
|
|
}
|
|
|
|
return installed
|
|
}
|
|
|
|
async function maybeInstallRebar3(rebar3Spec) {
|
|
let installed = false
|
|
let rebar3Version
|
|
|
|
if (rebar3Spec) {
|
|
if (rebar3Spec === 'nightly') {
|
|
rebar3Version = 'nightly'
|
|
} else {
|
|
rebar3Version = await getRebar3Version(rebar3Spec)
|
|
}
|
|
core.startGroup(`Installing rebar3 ${rebar3Version}`)
|
|
await installer.installRebar3(rebar3Version)
|
|
core.setOutput('rebar3-version', rebar3Version)
|
|
core.addPath(`${process.env.RUNNER_TEMP}/.setup-beam/rebar3/bin`)
|
|
core.endGroup()
|
|
|
|
installed = true
|
|
}
|
|
|
|
return installed
|
|
}
|
|
|
|
async function getOTPVersion(otpSpec0, osVersion, hexMirrors) {
|
|
const otpVersions = await getOTPVersions(osVersion, hexMirrors)
|
|
let otpSpec = otpSpec0 // might be a branch (?)
|
|
const otpVersion = getVersionFromSpec(
|
|
otpSpec,
|
|
Array.from(otpVersions.keys()).sort(),
|
|
)
|
|
if (isVersion(otpSpec0)) {
|
|
otpSpec = `OTP-${otpSpec0}` // ... it's a version!
|
|
}
|
|
if (otpVersion === null) {
|
|
throw new Error(
|
|
`Requested Erlang/OTP version (${otpSpec0}) not found in version list ` +
|
|
"(should you be using option 'version-type': 'strict'?)",
|
|
)
|
|
}
|
|
|
|
return otpVersions.get(otpVersion) // from the reference, for download
|
|
}
|
|
|
|
async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) {
|
|
const otpVersion = otpVersion0.match(/^([^-]+-)?(.+)$/)[2]
|
|
const otpVersionMajor = otpVersion.match(/^([^.]+).*$/)[1]
|
|
const elixirVersions = await getElixirVersions(hexMirrors)
|
|
const semverVersions = Array.from(elixirVersions.keys()).sort()
|
|
const exSpec = exSpec0.replace(/-otp-.*$/, '')
|
|
const elixirVersionFromSpec = getVersionFromSpec(exSpec, semverVersions, true)
|
|
let elixirVersionForDownload = elixirVersionFromSpec
|
|
if (isVersion(otpVersionMajor)) {
|
|
elixirVersionForDownload = `${elixirVersionFromSpec}-otp-${otpVersionMajor}`
|
|
}
|
|
if (elixirVersionFromSpec === null) {
|
|
throw new Error(
|
|
`Requested Elixir version (${exSpec0}) not found in version list ` +
|
|
"(should you be using option 'version-type': 'strict'?)",
|
|
)
|
|
}
|
|
|
|
const elixirVersionComp = elixirVersions.get(elixirVersionFromSpec)
|
|
if (
|
|
(elixirVersionComp && elixirVersionComp.includes(otpVersionMajor)) ||
|
|
!isVersion(otpVersionMajor)
|
|
) {
|
|
core.info(
|
|
`Using Elixir ${elixirVersionFromSpec} (built for Erlang/OTP ${otpVersionMajor})`,
|
|
)
|
|
} else {
|
|
throw new Error(
|
|
`Requested Elixir / Erlang/OTP version (${exSpec0} / ${otpVersion0}) not ` +
|
|
'found in version list (did you check Compatibility between Elixir and Erlang/OTP?).' +
|
|
'Elixir and Erlang/OTP compatibility can be found on: https://hexdocs.pm/elixir/compatibility-and-deprecations.html',
|
|
)
|
|
}
|
|
|
|
return maybePrependWithV(elixirVersionForDownload)
|
|
}
|
|
|
|
async function getGleamVersion(gleamSpec0) {
|
|
const gleamSpec = gleamSpec0.match(/^v?(.+)$/)
|
|
const gleamVersions = await getGleamVersions()
|
|
const gleamVersion = getVersionFromSpec(gleamSpec[1], gleamVersions, true)
|
|
if (gleamVersion === null) {
|
|
throw new Error(
|
|
`Requested Gleam version (${gleamSpec0}) not found in version list ` +
|
|
"(should you be using option 'version-type': 'strict'?)",
|
|
)
|
|
}
|
|
|
|
return maybePrependWithV(gleamVersion, gleamVersion)
|
|
}
|
|
|
|
async function getRebar3Version(r3Spec) {
|
|
const rebar3Versions = await getRebar3Versions()
|
|
const rebar3Version = getVersionFromSpec(r3Spec, rebar3Versions)
|
|
if (rebar3Version === null) {
|
|
throw new Error(
|
|
`Requested rebar3 version (${r3Spec}) not found in version list ` +
|
|
"(should you be using option 'version-type': 'strict'?)",
|
|
)
|
|
}
|
|
|
|
return rebar3Version
|
|
}
|
|
|
|
async function getOTPVersions(osVersion, hexMirrors) {
|
|
let otpVersionsListings
|
|
let originListing
|
|
if (process.platform === 'linux') {
|
|
originListing = `/builds/otp/${osVersion}/builds.txt`
|
|
otpVersionsListings = await getWithMirrors(originListing, hexMirrors)
|
|
} else if (process.platform === 'win32') {
|
|
originListing =
|
|
'https://api.github.com/repos/erlang/otp/releases?per_page=100'
|
|
otpVersionsListings = await get(originListing, [1, 2, 3])
|
|
}
|
|
debugLog(`OTP versions listings from ${originListing}`, otpVersionsListings)
|
|
|
|
const otpVersions = new Map()
|
|
|
|
if (process.platform === 'linux') {
|
|
otpVersionsListings
|
|
.trim()
|
|
.split('\n')
|
|
.forEach((line) => {
|
|
const otpMatch = line
|
|
.match(/^([^ ]+)?( .+)/)[1]
|
|
.match(/^([^-]+-)?(.+)$/)
|
|
const otpVersion = otpMatch[2]
|
|
otpVersions.set(otpVersion, otpMatch[0]) // we keep the original for later reference
|
|
})
|
|
} else if (process.platform === 'win32') {
|
|
otpVersionsListings.forEach((otpVersionsListing) => {
|
|
jsonParseAsList(otpVersionsListing)
|
|
.map((x) => x.assets)
|
|
.flat()
|
|
.filter((x) => x.name.match(/^otp_win64_.*.exe$/))
|
|
.forEach((x) => {
|
|
const otpMatch = x.name.match(/^otp_win64_(.*).exe$/)
|
|
const otpVersion = otpMatch[1]
|
|
otpVersions.set(otpVersion, otpVersion)
|
|
})
|
|
})
|
|
}
|
|
return otpVersions
|
|
}
|
|
|
|
async function getElixirVersions(hexMirrors) {
|
|
const elixirVersionsListings = await getWithMirrors(
|
|
'/builds/elixir/builds.txt',
|
|
hexMirrors,
|
|
)
|
|
const otpVersionsForElixirMap = new Map()
|
|
|
|
elixirVersionsListings
|
|
.trim()
|
|
.split('\n')
|
|
.forEach((line) => {
|
|
const elixirMatch =
|
|
line.match(/^v?(.+)-otp-([^ ]+)/) || line.match(/^v?([^ ]+)/)
|
|
const elixirVersion = maybePrependWithV(elixirMatch[1])
|
|
const otpVersion = elixirMatch[2]
|
|
const otpVersions = otpVersionsForElixirMap.get(elixirVersion) || []
|
|
if (otpVersion) {
|
|
// -otp- present (special case)
|
|
otpVersions.push(otpVersion)
|
|
}
|
|
otpVersionsForElixirMap.set(elixirVersion, otpVersions)
|
|
})
|
|
|
|
return otpVersionsForElixirMap
|
|
}
|
|
|
|
async function getGleamVersions() {
|
|
const resultJSONs = await get(
|
|
'https://api.github.com/repos/gleam-lang/gleam/releases?per_page=100',
|
|
[1, 2, 3],
|
|
)
|
|
const gleamVersionsListing = []
|
|
resultJSONs.forEach((resultJSON) => {
|
|
jsonParseAsList(resultJSON)
|
|
.map((x) => x.tag_name)
|
|
.sort()
|
|
.forEach((v) => gleamVersionsListing.push(v))
|
|
})
|
|
return gleamVersionsListing
|
|
}
|
|
|
|
async function getRebar3Versions() {
|
|
const resultJSONs = await get(
|
|
'https://api.github.com/repos/erlang/rebar3/releases?per_page=100',
|
|
[1, 2, 3],
|
|
)
|
|
const rebar3VersionsListing = []
|
|
resultJSONs.forEach((resultJSON) => {
|
|
jsonParseAsList(resultJSON)
|
|
.map((x) => x.tag_name)
|
|
.sort()
|
|
.forEach((v) => rebar3VersionsListing.push(v))
|
|
})
|
|
return rebar3VersionsListing
|
|
}
|
|
|
|
function isStrictVersion() {
|
|
return getInput('version-type', false) === 'strict'
|
|
}
|
|
|
|
function getVersionFromSpec(spec, versions, maybePrependWithV0) {
|
|
let version = null
|
|
|
|
if (spec.match(/rc/) || isStrictVersion()) {
|
|
version = spec
|
|
}
|
|
|
|
if (version === null) {
|
|
// We keep a map of semver => "spec" in order to use semver ranges to find appropriate versions
|
|
const versionsMap = versions.sort(sortVersions).reduce((acc, v) => {
|
|
if (!v.match(/rc/)) {
|
|
// release candidates are opt-in
|
|
acc[maybeCoerced(v)] = v
|
|
}
|
|
return acc
|
|
}, {})
|
|
const rangeForMax = semver.validRange(spec)
|
|
if (rangeForMax) {
|
|
version =
|
|
versionsMap[semver.maxSatisfying(Object.keys(versionsMap), rangeForMax)]
|
|
} else {
|
|
version = versionsMap[maybeCoerced(spec)]
|
|
}
|
|
}
|
|
|
|
let v = version === null || version === undefined ? null : version
|
|
if (maybePrependWithV0 && v != null) {
|
|
v = maybePrependWithV(v)
|
|
}
|
|
return v
|
|
}
|
|
|
|
function maybeCoerced(v) {
|
|
let ret
|
|
|
|
try {
|
|
ret = semver.coerce(v).version
|
|
} catch {
|
|
// some stuff can't be coerced, like 'master'
|
|
ret = v
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
function sortVersions(left, right) {
|
|
let ret = 0
|
|
const newL = verAsComparableStr(left)
|
|
const newR = verAsComparableStr(right)
|
|
|
|
function verAsComparableStr(ver) {
|
|
const matchGroups = 5
|
|
const verSpec = /([^.]+)?\.?([^.]+)?\.?([^.]+)?\.?([^.]+)?\.?([^.]+)?/
|
|
const matches = ver.match(verSpec).splice(1, matchGroups)
|
|
return matches.reduce((acc, v) => acc + (v || '0').padStart(3, '0'), '')
|
|
}
|
|
|
|
if (newL < newR) {
|
|
ret = -1
|
|
} else if (newL > newR) {
|
|
ret = 1
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
function getRunnerOSVersion() {
|
|
const ImageOSToContainer = {
|
|
ubuntu18: 'ubuntu-18.04',
|
|
ubuntu20: 'ubuntu-20.04',
|
|
ubuntu22: 'ubuntu-22.04',
|
|
win19: 'windows-2019',
|
|
win22: 'windows-2022',
|
|
}
|
|
const containerFromEnvImageOS = ImageOSToContainer[process.env.ImageOS]
|
|
|
|
if (!containerFromEnvImageOS) {
|
|
throw new Error(
|
|
"Tried to map a target OS from env. variable 'ImageOS' (got " +
|
|
`${process.env.ImageOS}` +
|
|
"), but failed. If you're using a " +
|
|
"self-hosted runner, you should set 'env': 'ImageOS': ... to one of the following: " +
|
|
"['" +
|
|
`${Object.keys(ImageOSToContainer).join("', '")}` +
|
|
"']",
|
|
)
|
|
}
|
|
|
|
return containerFromEnvImageOS
|
|
}
|
|
|
|
async function get(url0, pageIdxs) {
|
|
async function getPage(pageIdx) {
|
|
const url = new URL(url0)
|
|
const headers = {}
|
|
const GithubToken = getInput('github-token', false)
|
|
|
|
if (GithubToken && url.host === 'api.github.com') {
|
|
headers.authorization = `Bearer ${GithubToken}`
|
|
}
|
|
|
|
if (pageIdx !== null) {
|
|
url.searchParams.append('page', pageIdx)
|
|
}
|
|
|
|
const httpClient = new http.HttpClient('setup-beam', [], {
|
|
allowRetries: true,
|
|
maxRetries: 3,
|
|
})
|
|
|
|
const response = await httpClient.get(url, headers)
|
|
|
|
if (response.statusCode >= 400 && response.statusCode <= 599) {
|
|
throw new Error(
|
|
`Got ${response.statusCode} from ${url}. Exiting with error`,
|
|
)
|
|
}
|
|
|
|
return response.readBody()
|
|
}
|
|
|
|
if (pageIdxs[0] === null) {
|
|
return getPage(null)
|
|
}
|
|
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', '')}`
|
|
}
|
|
return v
|
|
}
|
|
|
|
function isVersion(v) {
|
|
return /^v?\d+/.test(v)
|
|
}
|
|
|
|
function getInput(inputName, required, alternativeName, alternatives) {
|
|
const alternativeValue = (alternatives || new Map()).get(alternativeName)
|
|
let input = core.getInput(inputName, {
|
|
required: alternativeValue ? false : required,
|
|
})
|
|
// We can't have both input and alternativeValue set
|
|
if (input && alternativeValue) {
|
|
throw new Error(
|
|
`Found input ${inputName}=${input} (from the YML) \
|
|
alongside ${alternativeName}=${alternativeValue} \
|
|
(from the version file). You must choose one or the other.`,
|
|
)
|
|
} else if (!input) {
|
|
input = alternativeValue
|
|
}
|
|
return input
|
|
}
|
|
|
|
function parseVersionFile(versionFilePath0) {
|
|
const versionFilePath = path.join(
|
|
process.env.GITHUB_WORKSPACE,
|
|
versionFilePath0,
|
|
)
|
|
if (!fs.existsSync(versionFilePath)) {
|
|
throw new Error(
|
|
`The specified version file, ${versionFilePath0}, does not exist`,
|
|
)
|
|
}
|
|
core.startGroup(`Parsing version file at ${versionFilePath0}`)
|
|
const appVersions = new Map()
|
|
const versions = fs.readFileSync(versionFilePath, 'utf8')
|
|
// For the time being we parse .tool-versions
|
|
// If we ever start parsing something else, this should
|
|
// become default in a new option named e.g. version-file-type
|
|
versions.split('\n').forEach((line) => {
|
|
const appVersion = line.match(/^([^ ]+)[ ]+([^ #]+)/)
|
|
if (appVersion) {
|
|
const app = appVersion[1]
|
|
if (['erlang', 'elixir', 'gleam', 'rebar'].includes(app)) {
|
|
const [, , version] = appVersion
|
|
core.info(`Consuming ${app} at version ${version}`)
|
|
appVersions.set(app, version)
|
|
}
|
|
}
|
|
})
|
|
if (!appVersions.size) {
|
|
core.info('There was apparently nothing to consume')
|
|
} else {
|
|
core.info('... done!')
|
|
}
|
|
core.endGroup()
|
|
|
|
return appVersions
|
|
}
|
|
|
|
function jsonParseAsList(maybeJson) {
|
|
try {
|
|
const obj = JSON.parse(maybeJson)
|
|
if (!Array.isArray(obj)) {
|
|
throw new Error('expected a list!')
|
|
}
|
|
return obj
|
|
} catch (exc) {
|
|
throw new Error(
|
|
`Got an exception when trying to parse non-JSON list ${maybeJson}: ${exc}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
function debugLog(groupName, message) {
|
|
if (process.env.RUNNER_DEBUG) {
|
|
core.startGroup(`Debugging for ${groupName}`)
|
|
core.debug(message)
|
|
core.endGroup()
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getOTPVersion,
|
|
getElixirVersion,
|
|
getGleamVersion,
|
|
getRebar3Version,
|
|
getVersionFromSpec,
|
|
parseVersionFile,
|
|
}
|