2023-10-10 20:59:54 +08:00
|
|
|
import * as os from 'os';
|
|
|
|
import * as path from 'path';
|
|
|
|
import * as core from '@actions/core';
|
|
|
|
import * as tc from '@actions/tool-cache';
|
|
|
|
import * as semver from 'semver';
|
|
|
|
import * as httpm from '@actions/http-client';
|
2023-12-05 21:52:09 +08:00
|
|
|
import * as ifm from '@actions/http-client/lib/interfaces';
|
2023-10-10 20:59:54 +08:00
|
|
|
import * as exec from '@actions/exec';
|
2023-12-05 21:52:09 +08:00
|
|
|
|
2023-10-10 20:59:54 +08:00
|
|
|
import fs from 'fs';
|
2023-12-05 21:52:09 +08:00
|
|
|
import * as http from 'http';
|
2023-10-10 20:59:54 +08:00
|
|
|
|
|
|
|
import {
|
|
|
|
IS_WINDOWS,
|
|
|
|
IGraalPyManifestRelease,
|
|
|
|
createSymlinkInFolder,
|
|
|
|
isNightlyKeyword,
|
|
|
|
getBinaryDirectory,
|
|
|
|
getNextPageUrl
|
|
|
|
} from './utils';
|
|
|
|
|
|
|
|
const TOKEN = core.getInput('token');
|
|
|
|
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
|
|
|
|
|
|
|
|
export async function installGraalPy(
|
|
|
|
graalpyVersion: string,
|
|
|
|
architecture: string,
|
|
|
|
allowPreReleases: boolean,
|
|
|
|
releases: IGraalPyManifestRelease[] | undefined
|
|
|
|
) {
|
|
|
|
let downloadDir;
|
|
|
|
|
|
|
|
releases = releases ?? (await getAvailableGraalPyVersions());
|
|
|
|
|
|
|
|
if (!releases || !releases.length) {
|
|
|
|
throw new Error('No release was found in GraalPy version.json');
|
|
|
|
}
|
|
|
|
|
|
|
|
let releaseData = findRelease(releases, graalpyVersion, architecture, false);
|
|
|
|
|
|
|
|
if (allowPreReleases && (!releaseData || !releaseData.foundAsset)) {
|
|
|
|
// check for pre-release
|
|
|
|
core.info(
|
|
|
|
[
|
|
|
|
`Stable GraalPy version ${graalpyVersion} with arch ${architecture} not found`,
|
|
|
|
`Trying pre-release versions`
|
|
|
|
].join(os.EOL)
|
|
|
|
);
|
|
|
|
releaseData = findRelease(releases, graalpyVersion, architecture, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!releaseData || !releaseData.foundAsset) {
|
|
|
|
throw new Error(
|
|
|
|
`GraalPy version ${graalpyVersion} with arch ${architecture} not found`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const {foundAsset, resolvedGraalPyVersion} = releaseData;
|
|
|
|
const downloadUrl = `${foundAsset.browser_download_url}`;
|
|
|
|
|
|
|
|
core.info(`Downloading GraalPy from "${downloadUrl}" ...`);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const graalpyPath = await tc.downloadTool(downloadUrl, undefined, AUTH);
|
|
|
|
|
|
|
|
core.info('Extracting downloaded archive...');
|
|
|
|
downloadDir = await tc.extractTar(graalpyPath);
|
|
|
|
|
|
|
|
// root folder in archive can have unpredictable name so just take the first folder
|
|
|
|
// downloadDir is unique folder under TEMP and can't contain any other folders
|
|
|
|
const archiveName = fs.readdirSync(downloadDir)[0];
|
|
|
|
|
|
|
|
const toolDir = path.join(downloadDir, archiveName);
|
|
|
|
let installDir = toolDir;
|
|
|
|
if (!isNightlyKeyword(resolvedGraalPyVersion)) {
|
|
|
|
installDir = await tc.cacheDir(
|
|
|
|
toolDir,
|
|
|
|
'GraalPy',
|
|
|
|
resolvedGraalPyVersion,
|
|
|
|
architecture
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const binaryPath = getBinaryDirectory(installDir);
|
|
|
|
await createGraalPySymlink(binaryPath, resolvedGraalPyVersion);
|
|
|
|
await installPip(binaryPath);
|
|
|
|
|
|
|
|
return {installDir, resolvedGraalPyVersion};
|
|
|
|
} catch (err) {
|
|
|
|
if (err instanceof Error) {
|
|
|
|
// Rate limit?
|
|
|
|
if (
|
|
|
|
err instanceof tc.HTTPError &&
|
|
|
|
(err.httpStatusCode === 403 || err.httpStatusCode === 429)
|
|
|
|
) {
|
|
|
|
core.info(
|
|
|
|
`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
core.info(err.message);
|
|
|
|
}
|
|
|
|
if (err.stack !== undefined) {
|
|
|
|
core.debug(err.stack);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getAvailableGraalPyVersions() {
|
|
|
|
const http: httpm.HttpClient = new httpm.HttpClient('tool-cache');
|
|
|
|
|
2023-12-05 21:52:09 +08:00
|
|
|
const headers: http.OutgoingHttpHeaders = {};
|
2023-10-10 20:59:54 +08:00
|
|
|
if (AUTH) {
|
|
|
|
headers.authorization = AUTH;
|
|
|
|
}
|
|
|
|
|
|
|
|
let url: string | null =
|
|
|
|
'https://api.github.com/repos/oracle/graalpython/releases';
|
|
|
|
const result: IGraalPyManifestRelease[] = [];
|
|
|
|
do {
|
2023-12-05 21:52:09 +08:00
|
|
|
const response: ifm.TypedResponse<IGraalPyManifestRelease[]> =
|
2023-10-10 20:59:54 +08:00
|
|
|
await http.getJson(url, headers);
|
|
|
|
if (!response.result) {
|
|
|
|
throw new Error(
|
|
|
|
`Unable to retrieve the list of available GraalPy versions from '${url}'`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
result.push(...response.result);
|
|
|
|
url = getNextPageUrl(response);
|
|
|
|
} while (url);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function createGraalPySymlink(
|
|
|
|
graalpyBinaryPath: string,
|
|
|
|
graalpyVersion: string
|
|
|
|
) {
|
|
|
|
const version = semver.coerce(graalpyVersion)!;
|
|
|
|
const pythonBinaryPostfix = semver.major(version);
|
|
|
|
const pythonMinor = semver.minor(version);
|
|
|
|
const graalpyMajorMinorBinaryPostfix = `${pythonBinaryPostfix}.${pythonMinor}`;
|
|
|
|
const binaryExtension = IS_WINDOWS ? '.exe' : '';
|
|
|
|
|
|
|
|
core.info('Creating symlinks...');
|
|
|
|
createSymlinkInFolder(
|
|
|
|
graalpyBinaryPath,
|
|
|
|
`graalpy${binaryExtension}`,
|
|
|
|
`python${pythonBinaryPostfix}${binaryExtension}`,
|
|
|
|
true
|
|
|
|
);
|
|
|
|
|
|
|
|
createSymlinkInFolder(
|
|
|
|
graalpyBinaryPath,
|
|
|
|
`graalpy${binaryExtension}`,
|
|
|
|
`python${binaryExtension}`,
|
|
|
|
true
|
|
|
|
);
|
|
|
|
|
|
|
|
createSymlinkInFolder(
|
|
|
|
graalpyBinaryPath,
|
|
|
|
`graalpy${binaryExtension}`,
|
|
|
|
`graalpy${graalpyMajorMinorBinaryPostfix}${binaryExtension}`,
|
|
|
|
true
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function installPip(pythonLocation: string) {
|
|
|
|
core.info(
|
|
|
|
"Installing pip (GraalPy doesn't update pip because it uses a patched version of pip)"
|
|
|
|
);
|
|
|
|
const pythonBinary = path.join(pythonLocation, 'python');
|
|
|
|
await exec.exec(`${pythonBinary} -m ensurepip --default-pip`);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function graalPyTagToVersion(tag: string) {
|
|
|
|
const versionPattern = /.*-(\d+\.\d+\.\d+(?:\.\d+)?)((?:a|b|rc))?(\d*)?/;
|
|
|
|
const match = tag.match(versionPattern);
|
|
|
|
if (match && match[2]) {
|
|
|
|
return `${match[1]}-${match[2]}.${match[3]}`;
|
|
|
|
} else if (match) {
|
|
|
|
return match[1];
|
|
|
|
} else {
|
|
|
|
return tag.replace(/.*-/, '');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function findRelease(
|
|
|
|
releases: IGraalPyManifestRelease[],
|
|
|
|
graalpyVersion: string,
|
|
|
|
architecture: string,
|
|
|
|
includePrerelease: boolean
|
|
|
|
) {
|
|
|
|
const options = {includePrerelease: includePrerelease};
|
|
|
|
const filterReleases = releases.filter(item => {
|
|
|
|
const isVersionSatisfied = semver.satisfies(
|
|
|
|
graalPyTagToVersion(item.tag_name),
|
|
|
|
graalpyVersion,
|
|
|
|
options
|
|
|
|
);
|
|
|
|
return (
|
|
|
|
isVersionSatisfied && !!findAsset(item, architecture, process.platform)
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!filterReleases.length) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const sortedReleases = filterReleases.sort((previous, current) =>
|
|
|
|
semver.compare(
|
|
|
|
semver.coerce(graalPyTagToVersion(current.tag_name))!,
|
|
|
|
semver.coerce(graalPyTagToVersion(previous.tag_name))!
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
const foundRelease = sortedReleases[0];
|
|
|
|
const foundAsset = findAsset(foundRelease, architecture, process.platform);
|
|
|
|
|
|
|
|
return {
|
|
|
|
foundAsset,
|
|
|
|
resolvedGraalPyVersion: graalPyTagToVersion(foundRelease.tag_name)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function toGraalPyPlatform(platform: string) {
|
|
|
|
switch (platform) {
|
|
|
|
case 'win32':
|
|
|
|
return 'windows';
|
|
|
|
case 'darwin':
|
|
|
|
return 'macos';
|
|
|
|
}
|
|
|
|
return platform;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function toGraalPyArchitecture(architecture: string) {
|
|
|
|
switch (architecture) {
|
|
|
|
case 'x64':
|
|
|
|
return 'amd64';
|
|
|
|
case 'arm64':
|
|
|
|
return 'aarch64';
|
|
|
|
}
|
|
|
|
return architecture;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function findAsset(
|
|
|
|
item: IGraalPyManifestRelease,
|
|
|
|
architecture: string,
|
|
|
|
platform: string
|
|
|
|
) {
|
|
|
|
const graalpyArch = toGraalPyArchitecture(architecture);
|
|
|
|
const graalpyPlatform = toGraalPyPlatform(platform);
|
|
|
|
const found = item.assets.filter(
|
|
|
|
file =>
|
|
|
|
file.name.startsWith('graalpy') &&
|
|
|
|
file.name.endsWith(`-${graalpyPlatform}-${graalpyArch}.tar.gz`)
|
|
|
|
);
|
|
|
|
/*
|
|
|
|
In the future there could be more variants of GraalPy for a single release. Pick the shortest name, that one is the most likely to be the primary variant.
|
|
|
|
*/
|
|
|
|
found.sort((f1, f2) => f1.name.length - f2.name.length);
|
|
|
|
return found[0];
|
|
|
|
}
|