2023-03-09 10:44:56 +00:00
/* eslint no-unsafe-finally: "off" */
2022-03-31 19:11:27 +00:00
import * as cache from '@actions/cache' ;
import * as core from '@actions/core' ;
2020-12-17 15:03:54 +00:00
import fs from 'fs' ;
import * as path from 'path' ;
import * as semver from 'semver' ;
2023-06-28 20:02:44 +00:00
import * as toml from '@iarna/toml' ;
2022-07-19 12:20:19 +00:00
import * as exec from '@actions/exec' ;
2023-12-05 13:52:09 +00:00
import * as ifm from '@actions/http-client/lib/interfaces' ;
import * as http from 'http' ;
2020-12-17 15:03:54 +00:00
export const IS_WINDOWS = process . platform === 'win32' ;
export const IS_LINUX = process . platform === 'linux' ;
2022-07-22 08:02:07 +00:00
export const IS_MAC = process . platform === 'darwin' ;
2021-04-12 17:59:38 +00:00
export const WINDOWS_ARCHS = [ 'x86' , 'x64' ] ;
export const WINDOWS_PLATFORMS = [ 'win32' , 'win64' ] ;
2020-12-17 15:03:54 +00:00
const PYPY_VERSION_FILE = 'PYPY_VERSION' ;
export interface IPyPyManifestAsset {
filename : string ;
arch : string ;
platform : string ;
download_url : string ;
}
export interface IPyPyManifestRelease {
pypy_version : string ;
python_version : string ;
stable : boolean ;
latest_pypy : boolean ;
files : IPyPyManifestAsset [ ] ;
}
2023-10-10 12:59:54 +00:00
export interface IGraalPyManifestAsset {
name : string ;
browser_download_url : string ;
}
export interface IGraalPyManifestRelease {
tag_name : string ;
assets : IGraalPyManifestAsset [ ] ;
}
2020-12-17 15:03:54 +00:00
/ * * c r e a t e S y m l i n k s f o r d o w n l o a d e d P y P y
* It should be executed only for downloaded versions in runtime , because
* toolcache versions have this setup .
* /
export function createSymlinkInFolder (
folderPath : string ,
sourceName : string ,
targetName : string ,
setExecutable = false
) {
const sourcePath = path . join ( folderPath , sourceName ) ;
const targetPath = path . join ( folderPath , targetName ) ;
if ( fs . existsSync ( targetPath ) ) {
return ;
}
fs . symlinkSync ( sourcePath , targetPath ) ;
if ( ! IS_WINDOWS && setExecutable ) {
fs . chmodSync ( targetPath , '755' ) ;
}
}
export function validateVersion ( version : string ) {
return isNightlyKeyword ( version ) || Boolean ( semver . validRange ( version ) ) ;
}
export function isNightlyKeyword ( pypyVersion : string ) {
return pypyVersion === 'nightly' ;
}
export function getPyPyVersionFromPath ( installDir : string ) {
return path . basename ( path . dirname ( installDir ) ) ;
}
/ * *
* In tool - cache , we put PyPy to '<toolcache_root>/PyPy/<python_version>/x64'
* There is no easy way to determine what PyPy version is located in specific folder
* 'pypy --version' is not reliable enough since it is not set properly for preview versions
* "7.3.3rc1" is marked as '7.3.3' in 'pypy --version'
* so we put PYPY_VERSION file to PyPy directory when install it to VM and read it when we need to know version
* PYPY_VERSION contains exact version from 'versions.json'
* /
export function readExactPyPyVersionFile ( installDir : string ) {
let pypyVersion = '' ;
2023-03-09 10:44:56 +00:00
const fileVersion = path . join ( installDir , PYPY_VERSION_FILE ) ;
2020-12-17 15:03:54 +00:00
if ( fs . existsSync ( fileVersion ) ) {
2023-02-20 10:28:16 +00:00
pypyVersion = fs . readFileSync ( fileVersion ) . toString ( ) . trim ( ) ;
2020-12-17 15:03:54 +00:00
}
return pypyVersion ;
}
export function writeExactPyPyVersionFile (
installDir : string ,
resolvedPyPyVersion : string
) {
const pypyFilePath = path . join ( installDir , PYPY_VERSION_FILE ) ;
fs . writeFileSync ( pypyFilePath , resolvedPyPyVersion ) ;
}
/ * *
2023-10-16 09:01:43 +00:00
* Python version should be specified explicitly like "x.y" ( 3.10 , 3.11 , etc )
2020-12-17 15:03:54 +00:00
* "3.x" or "3" are not supported
* because it could cause ambiguity when both PyPy version and Python version are not precise
* /
export function validatePythonVersionFormatForPyPy ( version : string ) {
const re = /^\d+\.\d+$/ ;
return re . test ( version ) ;
}
2021-11-17 10:31:22 +00:00
export function isGhes ( ) : boolean {
const ghUrl = new URL (
process . env [ 'GITHUB_SERVER_URL' ] || 'https://github.com'
) ;
return ghUrl . hostname . toUpperCase ( ) !== 'GITHUB.COM' ;
}
2022-03-31 19:11:27 +00:00
export function isCacheFeatureAvailable ( ) : boolean {
2022-12-19 13:00:46 +00:00
if ( cache . isFeatureAvailable ( ) ) {
return true ;
}
2022-03-31 19:11:27 +00:00
2022-12-19 13:00:46 +00:00
if ( isGhes ( ) ) {
core . warning (
'Caching is only supported on GHES version >= 3.5. If you are on a version >= 3.5, please check with your GHES admin if the Actions cache service is enabled or not.'
) ;
2022-03-31 19:11:27 +00:00
return false ;
}
2022-12-19 13:00:46 +00:00
core . warning (
'The runner was not able to contact the cache service. Caching will be skipped'
) ;
return false ;
2022-03-31 19:11:27 +00:00
}
2022-07-19 12:20:19 +00:00
2022-12-07 17:12:42 +00:00
export function logWarning ( message : string ) : void {
const warningPrefix = '[warning]' ;
core . info ( ` ${ warningPrefix } ${ message } ` ) ;
}
async function getWindowsInfo() {
const { stdout } = await exec . getExecOutput (
'powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Caption"' ,
undefined ,
2022-07-19 12:20:19 +00:00
{
silent : true
}
) ;
2022-12-07 17:12:42 +00:00
const windowsVersion = stdout . trim ( ) . split ( ' ' ) [ 3 ] ;
2022-07-19 12:20:19 +00:00
2022-12-07 17:12:42 +00:00
return { osName : 'Windows' , osVersion : windowsVersion } ;
}
async function getMacOSInfo() {
const { stdout } = await exec . getExecOutput ( 'sw_vers' , [ '-productVersion' ] , {
silent : true
} ) ;
2022-07-19 12:20:19 +00:00
2022-12-07 17:12:42 +00:00
const macOSVersion = stdout . trim ( ) ;
return { osName : 'macOS' , osVersion : macOSVersion } ;
2022-07-19 12:20:19 +00:00
}
2022-07-25 13:02:06 +00:00
2022-12-07 17:12:42 +00:00
export async function getLinuxInfo() {
const { stdout } = await exec . getExecOutput ( 'lsb_release' , [ '-i' , '-r' , '-s' ] , {
silent : true
} ) ;
const [ osName , osVersion ] = stdout . trim ( ) . split ( '\n' ) ;
core . debug ( ` OS Name: ${ osName } , Version: ${ osVersion } ` ) ;
return { osName : osName , osVersion : osVersion } ;
}
export async function getOSInfo() {
let osInfo ;
try {
if ( IS_WINDOWS ) {
osInfo = await getWindowsInfo ( ) ;
} else if ( IS_LINUX ) {
osInfo = await getLinuxInfo ( ) ;
} else if ( IS_MAC ) {
osInfo = await getMacOSInfo ( ) ;
}
} catch ( err ) {
const error = err as Error ;
core . debug ( error . message ) ;
} finally {
return osInfo ;
}
2022-07-25 13:02:06 +00:00
}
2023-06-28 20:02:44 +00:00
/ * *
* Extract a value from an object by following the keys path provided .
* If the value is present , it is returned . Otherwise undefined is returned .
* /
function extractValue ( obj : any , keys : string [ ] ) : string | undefined {
if ( keys . length > 0 ) {
const value = obj [ keys [ 0 ] ] ;
if ( keys . length > 1 && value !== undefined ) {
return extractValue ( value , keys . slice ( 1 ) ) ;
} else {
return value ;
}
} else {
return ;
}
}
/ * *
* Python version extracted from the TOML file .
* If the ` project ` key is present at the root level , the version is assumed to
* be specified according to PEP 621 in ` project.requires-python ` .
* Otherwise , if the ` tool ` key is present at the root level , the version is
* assumed to be specified using poetry under ` tool.poetry.dependencies.python ` .
* If none is present , returns an empty list .
* /
export function getVersionInputFromTomlFile ( versionFile : string ) : string [ ] {
core . debug ( ` Trying to resolve version form ${ versionFile } ` ) ;
2024-09-06 16:40:29 +00:00
let pyprojectFile = fs . readFileSync ( versionFile , 'utf8' ) ;
// Normalize the line endings in the pyprojectFile
pyprojectFile = pyprojectFile . replace ( /\r\n/g , '\n' ) ;
2023-06-28 20:02:44 +00:00
const pyprojectConfig = toml . parse ( pyprojectFile ) ;
let keys = [ ] ;
if ( 'project' in pyprojectConfig ) {
// standard project metadata (PEP 621)
keys = [ 'project' , 'requires-python' ] ;
} else {
// python poetry
keys = [ 'tool' , 'poetry' , 'dependencies' , 'python' ] ;
}
const versions = [ ] ;
const version = extractValue ( pyprojectConfig , keys ) ;
if ( version !== undefined ) {
versions . push ( version ) ;
}
core . info ( ` Extracted ${ versions } from ${ versionFile } ` ) ;
2023-09-07 13:45:09 +00:00
const rawVersions = Array . from ( versions , version = >
version . split ( ',' ) . join ( ' ' )
) ;
const validatedVersions = rawVersions
. map ( item = > semver . validRange ( item , true ) )
. filter ( ( versionRange , index ) = > {
if ( ! versionRange ) {
core . debug (
` The version ${ rawVersions [ index ] } is not valid SemVer range `
) ;
}
return ! ! versionRange ;
} ) as string [ ] ;
return validatedVersions ;
2023-06-28 20:02:44 +00:00
}
/ * *
* Python version extracted from a plain text file .
* /
export function getVersionInputFromPlainFile ( versionFile : string ) : string [ ] {
core . debug ( ` Trying to resolve version form ${ versionFile } ` ) ;
2023-10-06 10:34:33 +00:00
const version = fs . readFileSync ( versionFile , 'utf8' ) . trim ( ) ;
2023-06-28 20:02:44 +00:00
core . info ( ` Resolved ${ versionFile } as ${ version } ` ) ;
return [ version ] ;
}
/ * *
* Python version extracted from a plain or TOML file .
* /
export function getVersionInputFromFile ( versionFile : string ) : string [ ] {
if ( versionFile . endsWith ( '.toml' ) ) {
return getVersionInputFromTomlFile ( versionFile ) ;
} else {
return getVersionInputFromPlainFile ( versionFile ) ;
}
}
2023-10-10 12:59:54 +00:00
/ * *
* Get the directory containing interpreter binary from installation directory of PyPy or GraalPy
* - On Linux and macOS , the Python interpreter is in 'bin' .
* - On Windows , it is in the installation root .
* /
export function getBinaryDirectory ( installDir : string ) {
return IS_WINDOWS ? installDir : path.join ( installDir , 'bin' ) ;
}
/ * *
* Extract next page URL from a HTTP response "link" header . Such headers are used in GitHub APIs .
* /
2023-12-05 13:52:09 +00:00
export function getNextPageUrl < T > ( response : ifm.TypedResponse < T > ) {
const responseHeaders = < http.OutgoingHttpHeaders > response . headers ;
2023-10-10 12:59:54 +00:00
const linkHeader = responseHeaders . link ;
if ( typeof linkHeader === 'string' ) {
for ( const link of linkHeader . split ( /\s*,\s*/ ) ) {
const match = link . match ( /<([^>]+)>(.*)/ ) ;
if ( match ) {
const url = match [ 1 ] ;
for ( const param of match [ 2 ] . split ( /\s*;\s*/ ) ) {
if ( param . match ( /rel="?next"?/ ) ) {
return url ;
}
}
}
}
}
return null ;
}
2024-08-05 17:23:34 +00:00
/ * *
* Add temporary fix for Windows
* On Windows , it is necessary to retain the . zip extension for proper extraction .
* because the tc . extractZip ( ) failure due to tc . downloadTool ( ) not adding . zip extension .
* Related issue : https : //github.com/actions/toolkit/issues/1179
* Related issue : https : //github.com/actions/setup-python/issues/819
* /
export function getDownloadFileName ( downloadUrl : string ) : string | undefined {
const tempDir = process . env . RUNNER_TEMP || '.' ;
return IS_WINDOWS
? path . join ( tempDir , path . basename ( downloadUrl ) )
: undefined ;
}