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 ] ;
}