From f6c0c51af837bf0b97906b41ad712fba70b87dbe Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Fri, 5 Jan 2024 14:53:15 -0600 Subject: [PATCH] Support the special version "MIN" --- README.md | 6 ++++++ action.yml | 4 ++++ package-lock.json | 13 +++++++++++- package.json | 3 ++- src/installer.ts | 50 +++++++++++++++++++++++++++++++++++++++++----- src/setup-julia.ts | 3 ++- 6 files changed, 71 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 27d927e8..4b3ce869 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,11 @@ This action sets up a Julia environment for use in actions by downloading a spec # # Default: false show-versioninfo: '' + + # Set the path to the project directory or file to use when resolving some versions (e.g. MIN). + # + # Default: '.' + project: '' ``` ### Outputs @@ -111,6 +116,7 @@ You can either specify specific Julia versions or version ranges. If you specify - `~1.3.0-0` is a **tilde** version range that includes _all_ pre-releases of `1.3.0`. It matches all versions `≥ 1.3.0-` and `< 1.4.0`. - `nightly` will install the latest nightly build. - `1.7-nightly` will install the latest nightly build for the upcoming 1.7 release. This version will only be available during certain phases of the Julia release cycle. +- `MIN` will install the minimum supported version of Julia compatible with the project. Internally the action uses node's semver package to resolve version ranges. Its [documentation](https://github.com/npm/node-semver#advanced-range-syntax) contains more details on the version range syntax. You can test what version will be selected for a given input in this JavaScript [REPL](https://repl.it/@SaschaMann/setup-julia-version-logic). diff --git a/action.yml b/action.yml index f961da72..e441778c 100644 --- a/action.yml +++ b/action.yml @@ -17,6 +17,10 @@ inputs: description: 'Display InteractiveUtils.versioninfo() after installing' required: false default: 'false' + project: + description: 'The path to the project directory or file to use when resolving some versions (e.g. MIN)' + required: false + default: '.' outputs: julia-version: description: 'The installed Julia version. May vary from the version input if a version range was given as input.' diff --git a/package-lock.json b/package-lock.json index 0b95ecb2..0fa47630 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "@actions/io": "^1.1.3", "@actions/tool-cache": "^2.0.1", "async-retry": "^1.3.3", - "semver": "^7.5.4" + "semver": "^7.5.4", + "toml": "^3.0.0" }, "devDependencies": { "@types/async-retry": "^1.4.8", @@ -4991,6 +4992,11 @@ "node": ">=8.0" } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "node_modules/ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", @@ -9021,6 +9027,11 @@ "is-number": "^7.0.0" } }, + "toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", diff --git a/package.json b/package.json index adace452..362427d0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "@actions/io": "^1.1.3", "@actions/tool-cache": "^2.0.1", "async-retry": "^1.3.3", - "semver": "^7.5.4" + "semver": "^7.5.4", + "toml": "^3.0.0" }, "devDependencies": { "@types/async-retry": "^1.4.8", diff --git a/src/installer.ts b/src/installer.ts index 25aede59..2f65cde4 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -9,6 +9,7 @@ import * as path from 'path' import retry = require('async-retry') import * as semver from 'semver' +import "toml"; // Translations between actions input and Julia arch names const osMap = { @@ -76,15 +77,54 @@ export async function getJuliaVersions(versionInfo): Promise { return versions } -export function getJuliaVersion(availableReleases: string[], versionInput: string, includePrerelease: boolean = false): string { +/** + * @returns An array of version ranges compatible with the Julia project + */ +function getProjectJuliaCompatVersions(projectInput: string): Promise { + let compatVersions: string[] = [] + let projectFile: string + + if (fs.statSync(projectInput).isFile()) { + projectFile = projectInput + } else { + for (let projectFilename in ["JuliaProject.toml", "Project.toml"]) { + let p = path.join(projectInput, projectFilename) + if (fs.statSync(projectFile).isFile()) { + projectFile = p + break + } + } + } + + if (!projectFile) { + throw new Error(`Unable to locate project file with project input: ${projectInput}`) + } + + const meta = toml.parse(fs.readFileSync(projectFile)) + for (let versionRange in meta.compat?.julia?.split(",")) { + compatVersions.push(versionRange.trim()) + } + + return compatVersions +} + +export function getJuliaVersion(availableReleases: string[], versionInput: string, includePrerelease: boolean = false, projectInput: string = undefined): string { + let version: string + if (semver.valid(versionInput) == versionInput || versionInput.endsWith('nightly')) { // versionInput is a valid version or a nightly version, use it directly - return versionInput + version = versionInput + } else if (versionInput == "MIN") { + // Resolve "MIN" to the minimum supported Julia version compatible with the project file + let versionRanges = getProjectJuliaCompatVersions(projectInput) + let minVersions = versionRanges.map(v => semver.minSatisfying(availableReleases, v, {includePrerelease})) + version = semver.sort(minCompatVersions.filter(v => v !== null))[0] + } else { + // Use the highest available version that matches versionInput + version = semver.maxSatisfying(availableReleases, versionInput, {includePrerelease}) } - // Use the highest available version that matches versionInput - let version = semver.maxSatisfying(availableReleases, versionInput, {includePrerelease}) - if (version == null) { + if (!version) { throw new Error(`Could not find a Julia version that matches ${versionInput}`) } diff --git a/src/setup-julia.ts b/src/setup-julia.ts index 1f309232..2a874b6e 100644 --- a/src/setup-julia.ts +++ b/src/setup-julia.ts @@ -42,6 +42,7 @@ async function run() { const versionInput = core.getInput('version') const includePrereleases = core.getInput('include-all-prereleases') == 'true' const originalArchInput = core.getInput('arch') + const projectInput = core.getInput('project') // It can easily happen that, for example, a workflow file contains an input `version: ${{ matrix.julia-version }}` // while the strategy matrix only contains a key `${{ matrix.version }}`. @@ -59,7 +60,7 @@ async function run() { const versionInfo = await installer.getJuliaVersionInfo() const availableReleases = await installer.getJuliaVersions(versionInfo) - const version = installer.getJuliaVersion(availableReleases, versionInput, includePrereleases) + const version = installer.getJuliaVersion(availableReleases, versionInput, includePrereleases, projectInput) core.debug(`selected Julia version: ${arch}/${version}`) core.setOutput('julia-version', version)