mirror of
https://github.com/julia-actions/setup-julia.git
synced 2026-02-11 18:46:53 +08:00
Resolve min as the earliest compatible Julia version (compatible with the user's project) (#202)
* Support the special version "MIN" * Support JULIA_PROJECT * Add tests * Add forgotten test fixtures * Get latest prerelease/release * No special pre-release behaviour * Add test for NPM semver difference * Robust test suite * Disallow less-than-equal * Refactor validJuliaCompatRange to return a validRange * Rename MIN to min * Rename getProjectFile to getProjectFilePath * Comment on "project" input * Additional tests for getProjectFilePath * Add comment on `juliaCompatRange` Co-authored-by: Dilum Aluthge <dilum@aluthge.com> * Update dependencies --------- Co-authored-by: Dilum Aluthge <dilum@aluthge.com>
This commit is contained in:
@@ -71,6 +71,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`).
|
||||
#
|
||||
# Defaults to using JULIA_PROJECT if defined, otherwise '.'
|
||||
project: ''
|
||||
```
|
||||
|
||||
### Outputs
|
||||
@@ -121,6 +126,7 @@ You can either specify specific Julia versions or version ranges. If you specify
|
||||
- `'pre'` will install the latest prerelease build (RCs, betas, and alphas).
|
||||
- `'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 earliest supported version of Julia compatible with the project. Especially useful in monorepos.
|
||||
|
||||
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).
|
||||
|
||||
|
||||
0
__tests__/fixtures/PkgA/Project.toml
Normal file
0
__tests__/fixtures/PkgA/Project.toml
Normal file
0
__tests__/fixtures/PkgB/JuliaProject.toml
Normal file
0
__tests__/fixtures/PkgB/JuliaProject.toml
Normal file
0
__tests__/fixtures/PkgC/JuliaProject.toml
Normal file
0
__tests__/fixtures/PkgC/JuliaProject.toml
Normal file
0
__tests__/fixtures/PkgC/Project.toml
Normal file
0
__tests__/fixtures/PkgC/Project.toml
Normal file
@@ -38,6 +38,135 @@ process.env['RUNNER_TOOL_CACHE'] = toolDir
|
||||
process.env['RUNNER_TEMP'] = tempDir
|
||||
|
||||
import * as installer from '../src/installer'
|
||||
import exp from 'constants'
|
||||
|
||||
describe("getProjectFilePath tests", () => {
|
||||
let orgJuliaProject
|
||||
let orgWorkingDir
|
||||
|
||||
beforeEach(() => {
|
||||
orgJuliaProject = process.env["JULIA_PROJECT"]
|
||||
orgWorkingDir = process.cwd()
|
||||
delete process.env["JULIA_PROJECT"]
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env["JULIA_PROJECT"] = orgJuliaProject
|
||||
process.chdir(orgWorkingDir)
|
||||
})
|
||||
|
||||
it("Can determine project file is missing", () => {
|
||||
expect(() => installer.getProjectFilePath("DNE.toml")).toThrow("Unable to locate project file")
|
||||
expect(() => installer.getProjectFilePath(fixtureDir)).toThrow("Unable to locate project file")
|
||||
expect(() => installer.getProjectFilePath()).toThrow("Unable to locate project file")
|
||||
})
|
||||
|
||||
it('Can determine project file from a directory', () => {
|
||||
expect(installer.getProjectFilePath(path.join(fixtureDir, "PkgA"))).toEqual(path.join(fixtureDir, "PkgA", "Project.toml"))
|
||||
expect(installer.getProjectFilePath(path.join(fixtureDir, "PkgB"))).toEqual(path.join(fixtureDir, "PkgB", "JuliaProject.toml"))
|
||||
})
|
||||
|
||||
it("Prefers using JuliaProject.toml over Project.toml", () => {
|
||||
expect(installer.getProjectFilePath(path.join(fixtureDir, "PkgC"))).toEqual(path.join(fixtureDir, "PkgC", "JuliaProject.toml"))
|
||||
})
|
||||
|
||||
it("Can determine project from JULIA_PROJECT", () => {
|
||||
process.env["JULIA_PROJECT"] = path.join(fixtureDir, "PkgA")
|
||||
expect(installer.getProjectFilePath()).toEqual(path.join(fixtureDir, "PkgA", "Project.toml"))
|
||||
})
|
||||
|
||||
it("Can determine project from the current working directory", () => {
|
||||
process.chdir(path.join(fixtureDir, "PkgA"));
|
||||
expect(installer.getProjectFilePath()).toEqual("Project.toml")
|
||||
})
|
||||
|
||||
it("Ignores JULIA_PROJECT when argument is used", () => {
|
||||
process.env["JULIA_PROJECT"] = path.join(fixtureDir, "PkgB")
|
||||
expect(installer.getProjectFilePath(path.join(fixtureDir, "PkgA"))).toEqual(path.join(fixtureDir, "PkgA", "Project.toml"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("validJuliaCompatRange tests", () => {
|
||||
it('Handles default caret specifier', () => {
|
||||
expect(installer.validJuliaCompatRange("1")).toEqual(semver.validRange("^1"))
|
||||
expect(installer.validJuliaCompatRange("1.2")).toEqual(semver.validRange("^1.2"))
|
||||
expect(installer.validJuliaCompatRange("1.2.3")).toEqual(semver.validRange("^1.2.3"))
|
||||
|
||||
// TODO: Pkg.jl currently does not support pre-release entries in compat so ideally this would fail
|
||||
expect(installer.validJuliaCompatRange("1.2.3-rc1")).toEqual(semver.validRange("^1.2.3-rc1"))
|
||||
})
|
||||
|
||||
it('Handle surrounding whitespace', () => {
|
||||
expect(installer.validJuliaCompatRange(" 1")).toEqual(semver.validRange("^1"))
|
||||
expect(installer.validJuliaCompatRange("1 ")).toEqual(semver.validRange("^1"))
|
||||
expect(installer.validJuliaCompatRange(" 1 ")).toEqual(semver.validRange("^1"))
|
||||
})
|
||||
|
||||
it('Handles version ranges with specifiers', () => {
|
||||
expect(installer.validJuliaCompatRange("^1.2.3")).toEqual(semver.validRange("^1.2.3"))
|
||||
expect(installer.validJuliaCompatRange("~1.2.3")).toEqual(semver.validRange("~1.2.3"))
|
||||
expect(installer.validJuliaCompatRange("=1.2.3")).toEqual(semver.validRange("=1.2.3"))
|
||||
expect(installer.validJuliaCompatRange(">=1.2.3")).toEqual(">=1.2.3")
|
||||
expect(installer.validJuliaCompatRange("≥1.2.3")).toEqual(">=1.2.3")
|
||||
expect(installer.validJuliaCompatRange("<1.2.3")).toEqual("<1.2.3")
|
||||
})
|
||||
|
||||
it('Handles whitespace after specifiers', () => {
|
||||
expect(installer.validJuliaCompatRange("^ 1.2.3")).toBeNull()
|
||||
expect(installer.validJuliaCompatRange("~ 1.2.3")).toBeNull()
|
||||
expect(installer.validJuliaCompatRange("= 1.2.3")).toBeNull()
|
||||
expect(installer.validJuliaCompatRange(">= 1.2.3")).toEqual(">=1.2.3")
|
||||
expect(installer.validJuliaCompatRange("≥ 1.2.3")).toEqual(">=1.2.3")
|
||||
expect(installer.validJuliaCompatRange("< 1.2.3")).toEqual("<1.2.3")
|
||||
})
|
||||
|
||||
it('Handles hypen ranges', () => {
|
||||
expect(installer.validJuliaCompatRange("1.2.3 - 4.5.6")).toEqual(semver.validRange("1.2.3 - 4.5.6"))
|
||||
expect(installer.validJuliaCompatRange("1.2.3-rc1 - 4.5.6")).toEqual(semver.validRange("1.2.3-rc1 - 4.5.6"))
|
||||
expect(installer.validJuliaCompatRange("1.2.3-rc1-4.5.6")).toEqual(semver.validRange("^1.2.3-rc1-4.5.6")) // A version number and not a hypen range
|
||||
expect(installer.validJuliaCompatRange("1.2.3-rc1 -4.5.6")).toBeNull()
|
||||
expect(installer.validJuliaCompatRange("1.2.3-rc1- 4.5.6")).toBeNull() // Whitespace separate version ranges
|
||||
})
|
||||
|
||||
it("Returns null AND operator on version ranges", () => {
|
||||
expect(installer.validJuliaCompatRange("")).toBeNull()
|
||||
expect(installer.validJuliaCompatRange("1 2 3")).toBeNull()
|
||||
expect(installer.validJuliaCompatRange("1- 2")).toBeNull()
|
||||
expect(installer.validJuliaCompatRange("<1 <1")).toBeNull()
|
||||
expect(installer.validJuliaCompatRange("< 1 < 1")).toBeNull()
|
||||
expect(installer.validJuliaCompatRange("< 1 < 1")).toBeNull()
|
||||
})
|
||||
|
||||
it('Returns null with invalid specifiers', () => {
|
||||
expect(installer.validJuliaCompatRange("<=1.2.3")).toBeNull()
|
||||
expect(installer.validJuliaCompatRange("≤1.2.3")).toBeNull()
|
||||
expect(installer.validJuliaCompatRange("*")).toBeNull()
|
||||
})
|
||||
|
||||
it("Handles OR operator on version ranges", () => {
|
||||
expect(installer.validJuliaCompatRange("1, 2, 3")).toEqual(semver.validRange("^1 || ^2 || ^3"))
|
||||
expect(installer.validJuliaCompatRange("1, 2 - 3, ≥ 4")).toEqual(semver.validRange("^1 || >=2 <=3 || >=4"))
|
||||
expect(installer.validJuliaCompatRange(",")).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("readJuliaCompatRange tests", () => {
|
||||
it('Can determine Julia compat entries', () => {
|
||||
const toml = '[compat]\njulia = "1, ^1.1, ~1.2, >=1.3, 1.4 - 1.5"'
|
||||
expect(installer.readJuliaCompatRange(toml)).toEqual(semver.validRange("^1 || ^1.1 || ~1.2 || >=1.3 || 1.4 - 1.5"))
|
||||
})
|
||||
|
||||
it('Throws with invalid version ranges', () => {
|
||||
expect(() => installer.readJuliaCompatRange('[compat]\njulia = ""')).toThrow("Invalid version range")
|
||||
expect(() => installer.readJuliaCompatRange('[compat]\njulia = "1 2 3"')).toThrow("Invalid version range")
|
||||
})
|
||||
|
||||
it('Handle missing compat entries', () => {
|
||||
expect(installer.readJuliaCompatRange("")).toEqual("*")
|
||||
expect(installer.readJuliaCompatRange("[compat]")).toEqual("*")
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe('version matching tests', () => {
|
||||
describe('specific versions', () => {
|
||||
@@ -95,6 +224,34 @@ describe('version matching tests', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('julia compat versions', () => {
|
||||
it('Understands "min"', () => {
|
||||
let versions = ["1.6.7", "1.7.1-rc1", "1.7.1-rc2", "1.7.1", "1.7.2", "1.8.0"]
|
||||
expect(installer.getJuliaVersion(versions, "min", false, "^1.7")).toEqual("1.7.1")
|
||||
expect(installer.getJuliaVersion(versions, "min", true, "^1.7")).toEqual("1.7.1-rc1")
|
||||
|
||||
versions = ["1.6.7", "1.7.3-rc1", "1.7.3-rc2", "1.8.0"]
|
||||
expect(installer.getJuliaVersion(versions, "min", false, "^1.7")).toEqual("1.8.0")
|
||||
expect(installer.getJuliaVersion(versions, "min", true, "^1.7")).toEqual("1.7.3-rc1")
|
||||
|
||||
expect(installer.getJuliaVersion(versions, "min", false, "~1.7 || ~1.8 || ~1.9")).toEqual("1.8.0")
|
||||
expect(installer.getJuliaVersion(versions, "min", true, "~1.7 || ~1.8 || ~1.9")).toEqual("1.7.3-rc1")
|
||||
expect(installer.getJuliaVersion(versions, "min", false, "~1.8 || ~1.7 || ~1.9")).toEqual("1.8.0")
|
||||
expect(installer.getJuliaVersion(versions, "min", true, "~1.8 || ~1.7 || ~1.9")).toEqual("1.7.3-rc1")
|
||||
|
||||
expect(installer.getJuliaVersion(versions, "min", false, "1.7 - 1.9")).toEqual("1.8.0")
|
||||
expect(installer.getJuliaVersion(versions, "min", true, "1.7 - 1.9")).toEqual("1.7.3-rc1")
|
||||
|
||||
expect(installer.getJuliaVersion(versions, "min", true, "< 1.9.0")).toEqual("1.6.7")
|
||||
expect(installer.getJuliaVersion(versions, "min", true, ">= 1.6.0")).toEqual("1.6.7")
|
||||
|
||||
// NPM's semver package treats "1.7" as "~1.7" instead of "^1.7" like Julia
|
||||
expect(() => installer.getJuliaVersion(versions, "min", false, "1.7")).toThrow("Could not find a Julia version that matches")
|
||||
|
||||
expect(() => installer.getJuliaVersion(versions, "min", true, "")).toThrow("Julia project file does not specify a compat for Julia")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('installer tests', () => {
|
||||
|
||||
@@ -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: '' # Special value which fallsback to using JULIA_PROJECT if defined, otherwise "."
|
||||
outputs:
|
||||
julia-version:
|
||||
description: 'The installed Julia version. May vary from the version input if a version range was given as input.'
|
||||
|
||||
108
lib/installer.js
generated
108
lib/installer.js
generated
@@ -34,6 +34,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getJuliaVersionInfo = getJuliaVersionInfo;
|
||||
exports.getJuliaVersions = getJuliaVersions;
|
||||
exports.getProjectFilePath = getProjectFilePath;
|
||||
exports.validJuliaCompatRange = validJuliaCompatRange;
|
||||
exports.readJuliaCompatRange = readJuliaCompatRange;
|
||||
exports.getJuliaVersion = getJuliaVersion;
|
||||
exports.getFileInfo = getFileInfo;
|
||||
exports.getDownloadURL = getDownloadURL;
|
||||
@@ -48,6 +51,7 @@ const os = __importStar(require("os"));
|
||||
const path = __importStar(require("path"));
|
||||
const retry = require("async-retry");
|
||||
const semver = __importStar(require("semver"));
|
||||
const toml = __importStar(require("toml"));
|
||||
const LTS_VERSION = '1.6';
|
||||
const MAJOR_VERSION = '1'; // Could be deduced from versions.json
|
||||
// Translations between actions input and Julia arch names
|
||||
@@ -112,20 +116,106 @@ function getJuliaVersions(versionInfo) {
|
||||
return versions;
|
||||
});
|
||||
}
|
||||
function getJuliaVersion(availableReleases, versionInput, includePrerelease = false) {
|
||||
/**
|
||||
* @returns The path to the Julia project file
|
||||
*/
|
||||
function getProjectFilePath(projectInput = "") {
|
||||
let projectFilePath = "";
|
||||
// Default value for projectInput
|
||||
if (!projectInput) {
|
||||
projectInput = process.env.JULIA_PROJECT || ".";
|
||||
}
|
||||
if (fs.existsSync(projectInput) && fs.lstatSync(projectInput).isFile()) {
|
||||
projectFilePath = projectInput;
|
||||
}
|
||||
else {
|
||||
for (let projectFilename of ["JuliaProject.toml", "Project.toml"]) {
|
||||
let p = path.join(projectInput, projectFilename);
|
||||
if (fs.existsSync(p) && fs.lstatSync(p).isFile()) {
|
||||
projectFilePath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!projectFilePath) {
|
||||
throw new Error(`Unable to locate project file with project input: ${projectInput}`);
|
||||
}
|
||||
return projectFilePath;
|
||||
}
|
||||
/**
|
||||
* @returns A valid NPM semver range from a Julia compat range or null if it's not valid
|
||||
*/
|
||||
function validJuliaCompatRange(compatRange) {
|
||||
let ranges = [];
|
||||
for (let range of compatRange.split(",")) {
|
||||
range = range.trim();
|
||||
// An empty range isn't supported by Julia
|
||||
if (!range) {
|
||||
return null;
|
||||
}
|
||||
// NPM's semver doesn't understand unicode characters such as `≥` so we'll convert to alternatives
|
||||
range = range.replace("≥", ">=").replace("≤", "<=");
|
||||
// Cleanup whitespace. Julia only allows whitespace between the specifier and version with certain specifiers
|
||||
range = range.replace(/\s+/g, " ").replace(/(?<=(>|>=|≥|<)) (?=\d)/g, "");
|
||||
if (!semver.validRange(range) || range.split(/(?<! -) (?!- )/).length > 1 || range.startsWith("<=") || range === "*") {
|
||||
return null;
|
||||
}
|
||||
else if (range.search(/^\d/) === 0 && !range.includes(" ")) {
|
||||
// Compat version is just a basic version number (e.g. 1.2.3). Since Julia's Pkg.jl's uses caret
|
||||
// as the default specifier (e.g. `1.2.3 == ^1.2.3`) and NPM's semver uses tilde as the default
|
||||
// specifier (e.g. `1.2.3 == 1.2.x == ~1.2.3`) we will introduce the caret specifier to ensure the
|
||||
// orignal intent is respected.
|
||||
// https://pkgdocs.julialang.org/v1/compatibility/#Version-specifier-format
|
||||
// https://github.com/npm/node-semver#x-ranges-12x-1x-12-
|
||||
range = "^" + range;
|
||||
}
|
||||
ranges.push(range);
|
||||
}
|
||||
return semver.validRange(ranges.join(" || "));
|
||||
}
|
||||
/**
|
||||
* @returns An array of version ranges compatible with the Julia project
|
||||
*/
|
||||
function readJuliaCompatRange(projectFileContent) {
|
||||
var _a;
|
||||
let compatRange;
|
||||
let meta = toml.parse(projectFileContent);
|
||||
if (((_a = meta.compat) === null || _a === void 0 ? void 0 : _a.julia) !== undefined) {
|
||||
compatRange = validJuliaCompatRange(meta.compat.julia);
|
||||
}
|
||||
else {
|
||||
compatRange = "*";
|
||||
}
|
||||
if (!compatRange) {
|
||||
throw new Error(`Invalid version range found in Julia compat: ${compatRange}`);
|
||||
}
|
||||
return compatRange;
|
||||
}
|
||||
function getJuliaVersion(availableReleases, versionInput, includePrerelease = false, juliaCompatRange = "") {
|
||||
// Note: `juliaCompatRange` is ignored unless `versionInput` is `min`
|
||||
let version;
|
||||
if (semver.valid(versionInput) == versionInput || versionInput.endsWith('nightly')) {
|
||||
// versionInput is a valid version or a nightly version, use it directly
|
||||
return versionInput;
|
||||
version = versionInput;
|
||||
}
|
||||
if (versionInput == 'lts') {
|
||||
return getJuliaVersion(availableReleases, LTS_VERSION, false);
|
||||
else if (versionInput == "min") {
|
||||
// Resolve "min" to the minimum supported Julia version compatible with the project file
|
||||
if (!juliaCompatRange) {
|
||||
throw new Error('Unable to use version "min" when the Julia project file does not specify a compat for Julia');
|
||||
}
|
||||
version = semver.minSatisfying(availableReleases, juliaCompatRange, { includePrerelease });
|
||||
}
|
||||
if (versionInput == 'pre') {
|
||||
return getJuliaVersion(availableReleases, MAJOR_VERSION, true);
|
||||
else if (versionInput == "lts") {
|
||||
version = semver.maxSatisfying(availableReleases, LTS_VERSION, { includePrerelease: false });
|
||||
}
|
||||
// Use the highest available version that matches versionInput
|
||||
let version = semver.maxSatisfying(availableReleases, versionInput, { includePrerelease });
|
||||
if (version == null) {
|
||||
else if (versionInput == "pre") {
|
||||
version = semver.maxSatisfying(availableReleases, MAJOR_VERSION, { includePrerelease: true });
|
||||
}
|
||||
else {
|
||||
// Use the highest available version that matches versionInput
|
||||
version = semver.maxSatisfying(availableReleases, versionInput, { includePrerelease });
|
||||
}
|
||||
if (!version) {
|
||||
throw new Error(`Could not find a Julia version that matches ${versionInput}`);
|
||||
}
|
||||
// GitHub tags start with v, remove it
|
||||
|
||||
9
lib/setup-julia.js
generated
9
lib/setup-julia.js
generated
@@ -72,6 +72,7 @@ function run() {
|
||||
const versionInput = core.getInput('version').trim();
|
||||
const includePrereleases = core.getInput('include-all-prereleases').trim() == 'true';
|
||||
const originalArchInput = core.getInput('arch').trim();
|
||||
const projectInput = core.getInput('project').trim(); // Julia project file
|
||||
// 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 }}`.
|
||||
// In that case, we want the action to fail, rather than trying to download julia from an URL that's missing parts and 404ing.
|
||||
@@ -100,9 +101,15 @@ function run() {
|
||||
// before we index into the `archSynonyms` dict.
|
||||
const arch = archSynonyms[processedArchInput.toLowerCase()];
|
||||
core.debug(`Mapped the "arch" from ${processedArchInput} to ${arch}`);
|
||||
// Determine the Julia compat ranges as specified by the Project.toml only for special versions that require them.
|
||||
let juliaCompatRange = "";
|
||||
if (versionInput === "min") {
|
||||
const projectFilePath = installer.getProjectFilePath(projectInput);
|
||||
juliaCompatRange = installer.readJuliaCompatRange(fs.readFileSync(projectFilePath).toString());
|
||||
}
|
||||
const versionInfo = yield installer.getJuliaVersionInfo();
|
||||
const availableReleases = yield installer.getJuliaVersions(versionInfo);
|
||||
const version = installer.getJuliaVersion(availableReleases, versionInput, includePrereleases);
|
||||
const version = installer.getJuliaVersion(availableReleases, versionInput, includePrereleases, juliaCompatRange);
|
||||
core.debug(`selected Julia version: ${arch}/${version}`);
|
||||
core.setOutput('julia-version', version);
|
||||
// Search in cache
|
||||
|
||||
4897
package-lock.json
generated
4897
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,8 @@
|
||||
"@actions/io": "^1.1.3",
|
||||
"@actions/tool-cache": "^2.0.1",
|
||||
"async-retry": "^1.3.3",
|
||||
"semver": "^7.6.3"
|
||||
"semver": "^7.6.3",
|
||||
"toml": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/async-retry": "^1.4.8",
|
||||
|
||||
120
src/installer.ts
120
src/installer.ts
@@ -9,6 +9,7 @@ import * as path from 'path'
|
||||
import retry = require('async-retry')
|
||||
|
||||
import * as semver from 'semver'
|
||||
import * as toml from 'toml'
|
||||
|
||||
const LTS_VERSION = '1.6'
|
||||
const MAJOR_VERSION = '1' // Could be deduced from versions.json
|
||||
@@ -79,23 +80,116 @@ export async function getJuliaVersions(versionInfo): Promise<string[]> {
|
||||
return versions
|
||||
}
|
||||
|
||||
export function getJuliaVersion(availableReleases: string[], versionInput: string, includePrerelease: boolean = false): string {
|
||||
/**
|
||||
* @returns The path to the Julia project file
|
||||
*/
|
||||
export function getProjectFilePath(projectInput: string = ""): string {
|
||||
let projectFilePath: string = ""
|
||||
|
||||
// Default value for projectInput
|
||||
if (!projectInput) {
|
||||
projectInput = process.env.JULIA_PROJECT || "."
|
||||
}
|
||||
|
||||
if (fs.existsSync(projectInput) && fs.lstatSync(projectInput).isFile()) {
|
||||
projectFilePath = projectInput
|
||||
} else {
|
||||
for (let projectFilename of ["JuliaProject.toml", "Project.toml"]) {
|
||||
let p = path.join(projectInput, projectFilename)
|
||||
if (fs.existsSync(p) && fs.lstatSync(p).isFile()) {
|
||||
projectFilePath = p
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectFilePath) {
|
||||
throw new Error(`Unable to locate project file with project input: ${projectInput}`)
|
||||
}
|
||||
|
||||
return projectFilePath
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A valid NPM semver range from a Julia compat range or null if it's not valid
|
||||
*/
|
||||
export function validJuliaCompatRange(compatRange: string): string | null {
|
||||
let ranges: Array<string> = []
|
||||
for(let range of compatRange.split(",")) {
|
||||
range = range.trim()
|
||||
|
||||
// An empty range isn't supported by Julia
|
||||
if (!range) {
|
||||
return null
|
||||
}
|
||||
|
||||
// NPM's semver doesn't understand unicode characters such as `≥` so we'll convert to alternatives
|
||||
range = range.replace("≥", ">=").replace("≤", "<=")
|
||||
|
||||
// Cleanup whitespace. Julia only allows whitespace between the specifier and version with certain specifiers
|
||||
range = range.replace(/\s+/g, " ").replace(/(?<=(>|>=|≥|<)) (?=\d)/g, "")
|
||||
|
||||
if (!semver.validRange(range) || range.split(/(?<! -) (?!- )/).length > 1 || range.startsWith("<=") || range === "*") {
|
||||
return null
|
||||
} else if (range.search(/^\d/) === 0 && !range.includes(" ")) {
|
||||
// Compat version is just a basic version number (e.g. 1.2.3). Since Julia's Pkg.jl's uses caret
|
||||
// as the default specifier (e.g. `1.2.3 == ^1.2.3`) and NPM's semver uses tilde as the default
|
||||
// specifier (e.g. `1.2.3 == 1.2.x == ~1.2.3`) we will introduce the caret specifier to ensure the
|
||||
// orignal intent is respected.
|
||||
// https://pkgdocs.julialang.org/v1/compatibility/#Version-specifier-format
|
||||
// https://github.com/npm/node-semver#x-ranges-12x-1x-12-
|
||||
range = "^" + range
|
||||
}
|
||||
|
||||
ranges.push(range)
|
||||
}
|
||||
|
||||
return semver.validRange(ranges.join(" || "))
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns An array of version ranges compatible with the Julia project
|
||||
*/
|
||||
export function readJuliaCompatRange(projectFileContent: string): string {
|
||||
let compatRange: string | null
|
||||
let meta = toml.parse(projectFileContent)
|
||||
|
||||
if (meta.compat?.julia !== undefined) {
|
||||
compatRange = validJuliaCompatRange(meta.compat.julia)
|
||||
} else {
|
||||
compatRange = "*"
|
||||
}
|
||||
|
||||
if (!compatRange) {
|
||||
throw new Error(`Invalid version range found in Julia compat: ${compatRange}`)
|
||||
}
|
||||
|
||||
return compatRange
|
||||
}
|
||||
|
||||
export function getJuliaVersion(availableReleases: string[], versionInput: string, includePrerelease: boolean = false, juliaCompatRange: string = ""): string {
|
||||
// Note: `juliaCompatRange` is ignored unless `versionInput` is `min`
|
||||
let version: string | null
|
||||
|
||||
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
|
||||
if (!juliaCompatRange) {
|
||||
throw new Error('Unable to use version "min" when the Julia project file does not specify a compat for Julia')
|
||||
}
|
||||
version = semver.minSatisfying(availableReleases, juliaCompatRange, {includePrerelease})
|
||||
} else if (versionInput == "lts") {
|
||||
version = semver.maxSatisfying(availableReleases, LTS_VERSION, { includePrerelease: false });
|
||||
} else if (versionInput == "pre") {
|
||||
version = semver.maxSatisfying(availableReleases, MAJOR_VERSION, { includePrerelease: true });
|
||||
} else {
|
||||
// Use the highest available version that matches versionInput
|
||||
version = semver.maxSatisfying(availableReleases, versionInput, {includePrerelease})
|
||||
}
|
||||
|
||||
if (versionInput == 'lts') {
|
||||
return getJuliaVersion(availableReleases, LTS_VERSION, false)
|
||||
}
|
||||
|
||||
if (versionInput == 'pre') {
|
||||
return getJuliaVersion(availableReleases, MAJOR_VERSION, true)
|
||||
}
|
||||
|
||||
// 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}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ async function run() {
|
||||
const versionInput = core.getInput('version').trim()
|
||||
const includePrereleases = core.getInput('include-all-prereleases').trim() == 'true'
|
||||
const originalArchInput = core.getInput('arch').trim()
|
||||
const projectInput = core.getInput('project').trim() // Julia project file
|
||||
|
||||
// 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 }}`.
|
||||
@@ -74,9 +75,16 @@ async function run() {
|
||||
const arch = archSynonyms[processedArchInput.toLowerCase()]
|
||||
core.debug(`Mapped the "arch" from ${processedArchInput} to ${arch}`)
|
||||
|
||||
// Determine the Julia compat ranges as specified by the Project.toml only for special versions that require them.
|
||||
let juliaCompatRange: string = "";
|
||||
if (versionInput === "min") {
|
||||
const projectFilePath = installer.getProjectFilePath(projectInput)
|
||||
juliaCompatRange = installer.readJuliaCompatRange(fs.readFileSync(projectFilePath).toString())
|
||||
}
|
||||
|
||||
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, juliaCompatRange)
|
||||
core.debug(`selected Julia version: ${arch}/${version}`)
|
||||
core.setOutput('julia-version', version)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user