-
Notifications
You must be signed in to change notification settings - Fork 657
Adding support for more PyPy versions and installing them on-flight #168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
konradpabjan
merged 7 commits into
actions:main
from
dmitry-shibanov:v-dmshib/add-pypy-installation
Dec 17, 2020
Merged
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
739154f
add support to install pypy
dmitry-shibanov 3d613a9
resolved comments, update readme, add e2e tests.
dmitry-shibanov ef90203
resolve throw error
dmitry-shibanov fed9c7c
Resolve remain comments
e6c917a
Add pypy unit tests to cover code
dmitry-shibanov 0c8eaf8
add pypy tests and fix issue with pypy-3-nightly
dmitry-shibanov ac952b1
Fix PyPy nightly resolving
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next
Next commit
add support to install pypy
- Loading branch information
commit 739154f76b6d2caca93e1942ee1258e0862bf510
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import * as path from 'path'; | ||
import * as pypyInstall from './install-pypy'; | ||
import {IS_WINDOWS} from './utils'; | ||
|
||
import * as semver from 'semver'; | ||
import * as core from '@actions/core'; | ||
import * as tc from '@actions/tool-cache'; | ||
|
||
interface IPyPyVersionSpec { | ||
pypyVersion: string; | ||
pythonVersion: string; | ||
} | ||
|
||
export async function findPyPyVersion( | ||
versionSpec: string, | ||
architecture: string | ||
): Promise<{resolvedPyPyVersion: string; resolvedPythonVersion: string}> { | ||
let resolvedPyPyVersion = ''; | ||
let resolvedPythonVersion = ''; | ||
let installDir: string | null; | ||
|
||
const pypyVersionSpec = parsePyPyVersion(versionSpec); | ||
|
||
// PyPy only precompiles binaries for x86, but the architecture parameter defaults to x64. | ||
if (IS_WINDOWS && architecture === 'x64') { | ||
architecture = 'x86'; | ||
} | ||
|
||
({installDir, resolvedPythonVersion, resolvedPyPyVersion} = findPyPyToolCache( | ||
pypyVersionSpec.pythonVersion, | ||
pypyVersionSpec.pypyVersion, | ||
architecture | ||
)); | ||
|
||
if (!installDir) { | ||
({ | ||
installDir, | ||
resolvedPythonVersion, | ||
resolvedPyPyVersion | ||
} = await pypyInstall.installPyPy( | ||
pypyVersionSpec.pypyVersion, | ||
pypyVersionSpec.pythonVersion, | ||
architecture | ||
)); | ||
} | ||
|
||
const pipDir = IS_WINDOWS ? 'Scripts' : 'bin'; | ||
const _binDir = path.join(installDir, pipDir); | ||
const pythonLocation = pypyInstall.getPyPyBinaryPath(installDir); | ||
core.exportVariable('pythonLocation', pythonLocation); | ||
core.addPath(pythonLocation); | ||
core.addPath(_binDir); | ||
|
||
return {resolvedPyPyVersion, resolvedPythonVersion}; | ||
} | ||
|
||
function findPyPyToolCache( | ||
pythonVersion: string, | ||
pypyVersion: string, | ||
architecture: string | ||
) { | ||
let resolvedPyPyVersion = ''; | ||
let resolvedPythonVersion = ''; | ||
let installDir: string | null = tc.find('PyPy', pythonVersion, architecture); | ||
|
||
if (installDir) { | ||
// 'tc.find' finds tool based on Python version but we also need to check | ||
// whether PyPy version satisfies requested version. | ||
resolvedPythonVersion = getPyPyVersionFromPath(installDir); | ||
resolvedPyPyVersion = pypyInstall.readExactPyPyVersion(installDir); | ||
|
||
const isPyPyVersionSatisfies = semver.satisfies( | ||
resolvedPyPyVersion, | ||
pypyVersion | ||
); | ||
if (!isPyPyVersionSatisfies) { | ||
installDir = null; | ||
resolvedPyPyVersion = ''; | ||
resolvedPythonVersion = ''; | ||
} | ||
} | ||
|
||
if (!installDir) { | ||
core.info( | ||
`PyPy version ${pythonVersion} (${pypyVersion}) was not found in the local cache` | ||
); | ||
} | ||
|
||
return {installDir, resolvedPythonVersion, resolvedPyPyVersion}; | ||
} | ||
|
||
function parsePyPyVersion(versionSpec: string): IPyPyVersionSpec { | ||
const versions = versionSpec.split('-').filter(item => !!item); | ||
|
||
if (versions.length < 2) { | ||
throw new Error( | ||
"Invalid 'version' property for PyPy. PyPy version should be specified as 'pypy-<python-version>'. See readme for more examples." | ||
); | ||
konradpabjan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
const pythonVersion = versions[1]; | ||
dmitry-shibanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let pypyVersion: string; | ||
if (versions.length > 2) { | ||
pypyVersion = pypyInstall.pypyVersionToSemantic(versions[2]); | ||
} else { | ||
pypyVersion = 'x'; | ||
} | ||
|
||
return { | ||
pypyVersion: pypyVersion, | ||
pythonVersion: pythonVersion | ||
}; | ||
} | ||
|
||
function getPyPyVersionFromPath(installDir: string) { | ||
dmitry-shibanov marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
return path.basename(path.dirname(installDir)); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
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 * as fs from 'fs'; | ||
|
||
import {IS_WINDOWS, IPyPyManifestRelease, createSymlinkInFolder} from './utils'; | ||
|
||
const PYPY_VERSION_FILE = 'PYPY_VERSION'; | ||
|
||
export async function installPyPy( | ||
pypyVersion: string, | ||
pythonVersion: string, | ||
architecture: string | ||
) { | ||
let downloadDir; | ||
|
||
const releases = await getAvailablePyPyVersions(); | ||
const releaseData = findRelease( | ||
releases, | ||
pythonVersion, | ||
pypyVersion, | ||
architecture | ||
); | ||
|
||
if (!releaseData || !releaseData.foundAsset) { | ||
throw new Error( | ||
`PyPy version ${pythonVersion} (${pypyVersion}) with arch ${architecture} not found` | ||
); | ||
} | ||
|
||
const {foundAsset, resolvedPythonVersion, resolvedPyPyVersion} = releaseData; | ||
let downloadUrl = `${foundAsset.download_url}`; | ||
|
||
core.info(`Downloading PyPy from "${downloadUrl}" ...`); | ||
const pypyPath = await tc.downloadTool(downloadUrl); | ||
dmitry-shibanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
core.info('Extracting downloaded archive...'); | ||
if (IS_WINDOWS) { | ||
downloadDir = await tc.extractZip(pypyPath); | ||
} else { | ||
downloadDir = await tc.extractTar(pypyPath, undefined, 'x'); | ||
} | ||
|
||
// 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(resolvedPyPyVersion)) { | ||
installDir = await tc.cacheDir( | ||
toolDir, | ||
'PyPy', | ||
resolvedPythonVersion, | ||
architecture | ||
); | ||
} | ||
|
||
writeExactPyPyVersionFile(installDir, resolvedPyPyVersion); | ||
|
||
const binaryPath = getPyPyBinaryPath(installDir); | ||
await createPyPySymlink(binaryPath, resolvedPythonVersion); | ||
await installPip(binaryPath); | ||
|
||
return {installDir, resolvedPythonVersion, resolvedPyPyVersion}; | ||
} | ||
|
||
async function getAvailablePyPyVersions() { | ||
const url = 'https://downloads.python.org/pypy/versions.json'; | ||
const http: httpm.HttpClient = new httpm.HttpClient('tool-cache'); | ||
|
||
const response = await http.getJson<IPyPyManifestRelease[]>(url); | ||
if (!response.result) { | ||
throw new Error( | ||
`Unable to retrieve the list of available PyPy versions from '${url}'` | ||
); | ||
} | ||
|
||
return response.result; | ||
} | ||
|
||
async function createPyPySymlink( | ||
pypyBinaryPath: string, | ||
pythonVersion: string | ||
) { | ||
const version = semver.coerce(pythonVersion)!; | ||
const pythonBinaryPostfix = semver.major(version); | ||
const pypyBinaryPostfix = pythonBinaryPostfix === 2 ? '' : '3'; | ||
let binaryExtension = IS_WINDOWS ? '.exe' : ''; | ||
|
||
core.info('Creating symlinks...'); | ||
createSymlinkInFolder( | ||
pypyBinaryPath, | ||
`pypy${pypyBinaryPostfix}${binaryExtension}`, | ||
`python${pythonBinaryPostfix}${binaryExtension}`, | ||
true | ||
); | ||
|
||
createSymlinkInFolder( | ||
pypyBinaryPath, | ||
`pypy${pypyBinaryPostfix}${binaryExtension}`, | ||
`python${binaryExtension}`, | ||
true | ||
); | ||
} | ||
|
||
async function installPip(pythonLocation: string) { | ||
core.info('Installing and updating pip'); | ||
const pythonBinary = path.join(pythonLocation, 'python'); | ||
await exec.exec(`${pythonBinary} -m ensurepip`); | ||
// TO-DO should we skip updating of pip ? | ||
await exec.exec( | ||
dmitry-shibanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`${pythonLocation}/python -m pip install --ignore-installed pip` | ||
); | ||
} | ||
|
||
function findRelease( | ||
releases: IPyPyManifestRelease[], | ||
pythonVersion: string, | ||
pypyVersion: string, | ||
architecture: string | ||
) { | ||
const filterReleases = releases.filter(item => { | ||
const isPythonVersionSatisfies = semver.satisfies( | ||
dmitry-shibanov marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
semver.coerce(item.python_version)!, | ||
pythonVersion | ||
); | ||
const isPyPyNightly = | ||
isNightlyKeyword(pypyVersion) && isNightlyKeyword(item.pypy_version); | ||
const isPyPyVersionSatisfies = | ||
isPyPyNightly || | ||
semver.satisfies(pypyVersionToSemantic(item.pypy_version), pypyVersion); | ||
const isArchExists = item.files.some( | ||
file => file.arch === architecture && file.platform === process.platform | ||
); | ||
return isPythonVersionSatisfies && isPyPyVersionSatisfies && isArchExists; | ||
}); | ||
|
||
if (filterReleases.length === 0) { | ||
return null; | ||
} | ||
|
||
const sortedReleases = filterReleases.sort((previous, current) => { | ||
return ( | ||
semver.compare( | ||
semver.coerce(pypyVersionToSemantic(current.pypy_version))!, | ||
semver.coerce(pypyVersionToSemantic(previous.pypy_version))! | ||
) || | ||
semver.compare( | ||
semver.coerce(current.python_version)!, | ||
semver.coerce(previous.python_version)! | ||
) | ||
); | ||
}); | ||
|
||
const foundRelease = sortedReleases[0]; | ||
const foundAsset = foundRelease.files.find( | ||
item => item.arch === architecture && item.platform === process.platform | ||
); | ||
|
||
return { | ||
foundAsset, | ||
resolvedPythonVersion: foundRelease.python_version, | ||
resolvedPyPyVersion: foundRelease.pypy_version | ||
}; | ||
} | ||
|
||
// helper functions | ||
|
||
/** | ||
* 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 readExactPyPyVersion(installDir: string) { | ||
let pypyVersion = ''; | ||
let fileVersion = path.join(installDir, PYPY_VERSION_FILE); | ||
if (fs.existsSync(fileVersion)) { | ||
pypyVersion = fs.readFileSync(fileVersion).toString(); | ||
core.debug(`Version from ${PYPY_VERSION_FILE} file is ${pypyVersion}`); | ||
} | ||
|
||
return pypyVersion; | ||
} | ||
|
||
function writeExactPyPyVersionFile( | ||
installDir: string, | ||
resolvedPyPyVersion: string | ||
) { | ||
const pypyFilePath = path.join(installDir, PYPY_VERSION_FILE); | ||
fs.writeFileSync(pypyFilePath, resolvedPyPyVersion); | ||
} | ||
|
||
/** Get PyPy 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 getPyPyBinaryPath(installDir: string) { | ||
const _binDir = path.join(installDir, 'bin'); | ||
return IS_WINDOWS ? installDir : _binDir; | ||
} | ||
|
||
function isNightlyKeyword(pypyVersion: string) { | ||
return pypyVersion === 'nightly'; | ||
} | ||
|
||
export function pypyVersionToSemantic(versionSpec: string) { | ||
dmitry-shibanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc))(\d*)/g; | ||
return versionSpec.replace(prereleaseVersion, '$1-$2.$3'); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.