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:
Curtis Vogt
2024-08-30 10:58:16 -05:00
committed by GitHub
parent b83c8a20db
commit 014c323ee0
13 changed files with 1483 additions and 3831 deletions

108
lib/installer.js generated
View File

@@ -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
View File

@@ -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