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'; import * as exec from '@actions/exec'; import fs from 'fs'; import { IS_WINDOWS, IGraalPyManifestAsset, IGraalPyManifestRelease, isNightlyKeyword, } from './utils'; export async function installGraalPy( graalpyVersion: string, architecture: string, allowPreReleases: boolean, releases: IGraalPyManifestRelease[] | undefined ) { let downloadDir; releases = releases ?? (await getAvailableGraalPyVersions()); if (!releases || releases.length === 0) { 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); 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 = getGraalPyBinaryPath(installDir); 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 url = 'https://api.github.com/repos/oracle/graalpython/releases'; const http: httpm.HttpClient = new httpm.HttpClient('tool-cache'); const response = await http.getJson(url); if (!response.result) { throw new Error( `Unable to retrieve the list of available GraalPy versions from '${url}'` ); } return response.result; } async function installPip(pythonLocation: string) { core.info('Installing and updating pip'); const pythonBinary = path.join(pythonLocation, 'python'); await exec.exec(`${pythonBinary} -m ensurepip`); await exec.exec( `${pythonLocation}/python -m pip install --ignore-installed 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 === 0) { return null; } const sortedReleases = filterReleases.sort((previous, current) => { return ( semver.compare( semver.coerce(graalPyTagToVersion(current.tag_name))!, semver.coerce(graalPyTagToVersion(previous.tag_name))! ) || 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) }; } /** Get GraalPy binary location from the tool of installation directory * - On Linux and macOS, the Python interpreter is in 'bin'. * - On Windows, it is in the installation root. */ export function getGraalPyBinaryPath(installDir: string) { const _binDir = path.join(installDir, 'bin'); return IS_WINDOWS ? installDir : _binDir; } export function findAsset( item: IGraalPyManifestRelease, architecture: string, platform: string ) { const graalpyArch = architecture === 'x64' ? 'amd64' : (architecture === 'arm64' ? 'aarch64' : architecture); const graalpyPlatform = platform == "win32" ? "windows" : platform; if (item.assets) { return item.assets.find( (file: IGraalPyManifestAsset) => { const match_data = file.name.match(".*(darwin|linux|windows)-(amd64|aarch64).tar.gz$"); return match_data && match_data[1] === graalpyPlatform && match_data[2] === graalpyArch; } ); } else { return undefined; } }