2023-10-10 14:59:54 +02: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 14:52:09 +01:00
import * as ifm from '@actions/http-client/lib/interfaces' ;
2023-10-10 14:59:54 +02:00
import * as exec from '@actions/exec' ;
2023-12-05 14:52:09 +01:00
2023-10-10 14:59:54 +02:00
import fs from 'fs' ;
2023-12-05 14:52:09 +01:00
import * as http from 'http' ;
2023-10-10 14:59:54 +02: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 14:52:09 +01:00
const headers : http.OutgoingHttpHeaders = {};
2023-10-10 14:59:54 +02: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 14:52:09 +01:00
const response : ifm.TypedResponse < IGraalPyManifestRelease [] > =
2023-10-10 14:59:54 +02: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 ];
}