From 739154f76b6d2caca93e1942ee1258e0862bf510 Mon Sep 17 00:00:00 2001 From: Dmitry Shibanov Date: Mon, 14 Dec 2020 16:59:06 +0300 Subject: [PATCH 1/5] add support to install pypy --- dist/index.js | 340 ++++++++++++++++++++++++++++++++++++++++-- src/find-pypy.ts | 116 ++++++++++++++ src/find-python.ts | 4 +- src/install-pypy.ts | 216 +++++++++++++++++++++++++++ src/install-python.ts | 5 +- src/setup-python.ts | 18 ++- src/utils.ts | 42 ++++++ 7 files changed, 718 insertions(+), 23 deletions(-) create mode 100644 src/find-pypy.ts create mode 100644 src/install-pypy.ts create mode 100644 src/utils.ts diff --git a/dist/index.js b/dist/index.js index 41cfa78b7..c6684881c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1072,6 +1072,108 @@ function _readLinuxVersionFile() { exports._readLinuxVersionFile = _readLinuxVersionFile; //# sourceMappingURL=manifest.js.map +/***/ }), + +/***/ 50: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const path = __importStar(__webpack_require__(622)); +const pypyInstall = __importStar(__webpack_require__(369)); +const utils_1 = __webpack_require__(163); +const semver = __importStar(__webpack_require__(876)); +const core = __importStar(__webpack_require__(470)); +const tc = __importStar(__webpack_require__(533)); +function findPyPyVersion(versionSpec, architecture) { + return __awaiter(this, void 0, void 0, function* () { + let resolvedPyPyVersion = ''; + let resolvedPythonVersion = ''; + let installDir; + const pypyVersionSpec = parsePyPyVersion(versionSpec); + // PyPy only precompiles binaries for x86, but the architecture parameter defaults to x64. + if (utils_1.IS_WINDOWS && architecture === 'x64') { + architecture = 'x86'; + } + ({ installDir, resolvedPythonVersion, resolvedPyPyVersion } = findPyPyToolCache(pypyVersionSpec.pythonVersion, pypyVersionSpec.pypyVersion, architecture)); + if (!installDir) { + ({ + installDir, + resolvedPythonVersion, + resolvedPyPyVersion + } = yield pypyInstall.installPyPy(pypyVersionSpec.pypyVersion, pypyVersionSpec.pythonVersion, architecture)); + } + const pipDir = utils_1.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 }; + }); +} +exports.findPyPyVersion = findPyPyVersion; +function findPyPyToolCache(pythonVersion, pypyVersion, architecture) { + let resolvedPyPyVersion = ''; + let resolvedPythonVersion = ''; + let installDir = 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) { + 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-'. See readme for more examples."); + } + const pythonVersion = versions[1]; + let pypyVersion; + if (versions.length > 2) { + pypyVersion = pypyInstall.pypyVersionToSemantic(versions[2]); + } + else { + pypyVersion = 'x'; + } + return { + pypyVersion: pypyVersion, + pythonVersion: pythonVersion + }; +} +function getPyPyVersionFromPath(installDir) { + return path.basename(path.dirname(installDir)); +} + + /***/ }), /***/ 65: @@ -2197,6 +2299,43 @@ if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { exports.debug = debug; // for test +/***/ }), + +/***/ 163: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = __importStar(__webpack_require__(747)); +const path = __importStar(__webpack_require__(622)); +exports.IS_WINDOWS = process.platform === 'win32'; +exports.IS_LINUX = process.platform === 'linux'; +/** create Symlinks for downloaded PyPy + * It should be executed only for downloaded versions in runtime, because + * toolcache versions have this setup. + */ +function createSymlinkInFolder(folderPath, sourceName, targetName, setExecutable = false) { + const sourcePath = path.join(folderPath, sourceName); + const targetPath = path.join(folderPath, targetName); + if (fs.existsSync(targetPath)) { + return; + } + fs.symlinkSync(sourcePath, targetPath); + if (!exports.IS_WINDOWS && setExecutable) { + fs.chmodSync(targetPath, '755'); + } +} +exports.createSymlinkInFolder = createSymlinkInFolder; + + /***/ }), /***/ 164: @@ -2443,16 +2582,26 @@ var __importStar = (this && this.__importStar) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(__webpack_require__(470)); const finder = __importStar(__webpack_require__(927)); +const finderPyPy = __importStar(__webpack_require__(50)); const path = __importStar(__webpack_require__(622)); const os = __importStar(__webpack_require__(87)); +function isPyPyVersion(versionSpec) { + return versionSpec.startsWith('pypy-'); +} function run() { return __awaiter(this, void 0, void 0, function* () { try { let version = core.getInput('python-version'); if (version) { const arch = core.getInput('architecture') || os.arch(); - const installed = yield finder.findPythonVersion(version, arch); - core.info(`Successfully setup ${installed.impl} (${installed.version})`); + if (isPyPyVersion(version)) { + const installed = yield finderPyPy.findPyPyVersion(version, arch); + core.info(`Successfully setup PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})`); + } + else { + const installed = yield finder.findPythonVersion(version, arch); + core.info(`Successfully setup ${installed.impl} (${installed.version})`); + } } const matchersPath = path.join(__dirname, '..', '.github'); core.info(`##[add-matcher]${path.join(matchersPath, 'python.json')}`); @@ -2580,6 +2729,171 @@ module.exports = ltr module.exports = require("assert"); +/***/ }), + +/***/ 369: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const path = __importStar(__webpack_require__(622)); +const core = __importStar(__webpack_require__(470)); +const tc = __importStar(__webpack_require__(533)); +const semver = __importStar(__webpack_require__(876)); +const httpm = __importStar(__webpack_require__(539)); +const exec = __importStar(__webpack_require__(986)); +const fs = __importStar(__webpack_require__(747)); +const utils_1 = __webpack_require__(163); +const PYPY_VERSION_FILE = 'PYPY_VERSION'; +function installPyPy(pypyVersion, pythonVersion, architecture) { + return __awaiter(this, void 0, void 0, function* () { + let downloadDir; + const releases = yield 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 = yield tc.downloadTool(downloadUrl); + core.info('Extracting downloaded archive...'); + if (utils_1.IS_WINDOWS) { + downloadDir = yield tc.extractZip(pypyPath); + } + else { + downloadDir = yield 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 = yield tc.cacheDir(toolDir, 'PyPy', resolvedPythonVersion, architecture); + } + writeExactPyPyVersionFile(installDir, resolvedPyPyVersion); + const binaryPath = getPyPyBinaryPath(installDir); + yield createPyPySymlink(binaryPath, resolvedPythonVersion); + yield installPip(binaryPath); + return { installDir, resolvedPythonVersion, resolvedPyPyVersion }; + }); +} +exports.installPyPy = installPyPy; +function getAvailablePyPyVersions() { + return __awaiter(this, void 0, void 0, function* () { + const url = 'https://downloads.python.org/pypy/versions.json'; + const http = new httpm.HttpClient('tool-cache'); + const response = yield http.getJson(url); + if (!response.result) { + throw new Error(`Unable to retrieve the list of available PyPy versions from '${url}'`); + } + return response.result; + }); +} +function createPyPySymlink(pypyBinaryPath, pythonVersion) { + return __awaiter(this, void 0, void 0, function* () { + const version = semver.coerce(pythonVersion); + const pythonBinaryPostfix = semver.major(version); + const pypyBinaryPostfix = pythonBinaryPostfix === 2 ? '' : '3'; + let binaryExtension = utils_1.IS_WINDOWS ? '.exe' : ''; + core.info('Creating symlinks...'); + utils_1.createSymlinkInFolder(pypyBinaryPath, `pypy${pypyBinaryPostfix}${binaryExtension}`, `python${pythonBinaryPostfix}${binaryExtension}`, true); + utils_1.createSymlinkInFolder(pypyBinaryPath, `pypy${pypyBinaryPostfix}${binaryExtension}`, `python${binaryExtension}`, true); + }); +} +function installPip(pythonLocation) { + return __awaiter(this, void 0, void 0, function* () { + core.info('Installing and updating pip'); + const pythonBinary = path.join(pythonLocation, 'python'); + yield exec.exec(`${pythonBinary} -m ensurepip`); + // TO-DO should we skip updating of pip ? + yield exec.exec(`${pythonLocation}/python -m pip install --ignore-installed pip`); + }); +} +function findRelease(releases, pythonVersion, pypyVersion, architecture) { + const filterReleases = releases.filter(item => { + const isPythonVersionSatisfies = semver.satisfies(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 '/PyPy//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' + */ +function readExactPyPyVersion(installDir) { + 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; +} +exports.readExactPyPyVersion = readExactPyPyVersion; +function writeExactPyPyVersionFile(installDir, resolvedPyPyVersion) { + 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. + */ +function getPyPyBinaryPath(installDir) { + const _binDir = path.join(installDir, 'bin'); + return utils_1.IS_WINDOWS ? installDir : _binDir; +} +exports.getPyPyBinaryPath = getPyPyBinaryPath; +function isNightlyKeyword(pypyVersion) { + return pypyVersion === 'nightly'; +} +function pypyVersionToSemantic(versionSpec) { + const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc))(\d*)/g; + return versionSpec.replace(prereleaseVersion, '$1-$2.$3'); +} +exports.pypyVersionToSemantic = pypyVersionToSemantic; + + /***/ }), /***/ 413: @@ -6426,14 +6740,13 @@ const path = __importStar(__webpack_require__(622)); const core = __importStar(__webpack_require__(470)); const tc = __importStar(__webpack_require__(533)); const exec = __importStar(__webpack_require__(986)); +const utils_1 = __webpack_require__(163); const TOKEN = core.getInput('token'); const AUTH = !TOKEN || isGhes() ? undefined : `token ${TOKEN}`; const MANIFEST_REPO_OWNER = 'actions'; const MANIFEST_REPO_NAME = 'python-versions'; const MANIFEST_REPO_BRANCH = 'main'; exports.MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`; -const IS_WINDOWS = process.platform === 'win32'; -const IS_LINUX = process.platform === 'linux'; function findReleaseFromManifest(semanticVersionSpec, architecture) { return __awaiter(this, void 0, void 0, function* () { const manifest = yield tc.getManifestFromRepo(MANIFEST_REPO_OWNER, MANIFEST_REPO_NAME, AUTH, MANIFEST_REPO_BRANCH); @@ -6445,7 +6758,7 @@ function installPython(workingDirectory) { return __awaiter(this, void 0, void 0, function* () { const options = { cwd: workingDirectory, - env: Object.assign(Object.assign({}, process.env), (IS_LINUX && { LD_LIBRARY_PATH: path.join(workingDirectory, 'lib') })), + env: Object.assign(Object.assign({}, process.env), (utils_1.IS_LINUX && { LD_LIBRARY_PATH: path.join(workingDirectory, 'lib') })), silent: true, listeners: { stdout: (data) => { @@ -6456,7 +6769,7 @@ function installPython(workingDirectory) { } } }; - if (IS_WINDOWS) { + if (utils_1.IS_WINDOWS) { yield exec.exec('powershell', ['./setup.ps1'], options); } else { @@ -6471,7 +6784,7 @@ function installCpythonFromRelease(release) { const pythonPath = yield tc.downloadTool(downloadUrl, undefined, AUTH); core.info('Extract downloaded archive'); let pythonExtractedFolder; - if (IS_WINDOWS) { + if (utils_1.IS_WINDOWS) { pythonExtractedFolder = yield tc.extractZip(pythonPath); } else { @@ -6686,12 +6999,11 @@ var __importStar = (this && this.__importStar) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); const os = __importStar(__webpack_require__(87)); const path = __importStar(__webpack_require__(622)); +const utils_1 = __webpack_require__(163); const semver = __importStar(__webpack_require__(876)); const installer = __importStar(__webpack_require__(824)); const core = __importStar(__webpack_require__(470)); const tc = __importStar(__webpack_require__(533)); -const IS_WINDOWS = process.platform === 'win32'; -const IS_LINUX = process.platform === 'linux'; // Python has "scripts" or "bin" directories where command-line tools that come with packages are installed. // This is where pip is, along with anything that pip installs. // There is a seperate directory for `pip install --user`. @@ -6705,7 +7017,7 @@ const IS_LINUX = process.platform === 'linux'; // (--user) %APPDATA%\Python\PythonXY\Scripts // See https://docs.python.org/3/library/sysconfig.html function binDir(installDir) { - if (IS_WINDOWS) { + if (utils_1.IS_WINDOWS) { return path.join(installDir, 'Scripts'); } else { @@ -6720,7 +7032,7 @@ function binDir(installDir) { function usePyPy(majorVersion, architecture) { const findPyPy = tc.find.bind(undefined, 'PyPy', majorVersion); let installDir = findPyPy(architecture); - if (!installDir && IS_WINDOWS) { + if (!installDir && utils_1.IS_WINDOWS) { // PyPy only precompiles binaries for x86, but the architecture parameter defaults to x64. // On our Windows virtual environments, we only install an x86 version. // Fall back to x86. @@ -6734,7 +7046,7 @@ function usePyPy(majorVersion, architecture) { const _binDir = path.join(installDir, 'bin'); // On Linux and macOS, the Python interpreter is in 'bin'. // On Windows, it is in the installation root. - const pythonLocation = IS_WINDOWS ? installDir : _binDir; + const pythonLocation = utils_1.IS_WINDOWS ? installDir : _binDir; core.exportVariable('pythonLocation', pythonLocation); core.addPath(installDir); core.addPath(_binDir); @@ -6764,7 +7076,7 @@ function useCpythonVersion(version, architecture) { ].join(os.EOL)); } core.exportVariable('pythonLocation', installDir); - if (IS_LINUX) { + if (utils_1.IS_LINUX) { const libPath = process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''; @@ -6775,7 +7087,7 @@ function useCpythonVersion(version, architecture) { } core.addPath(installDir); core.addPath(binDir(installDir)); - if (IS_WINDOWS) { + if (utils_1.IS_WINDOWS) { // Add --user directory // `installDir` from tool cache should look like $RUNNER_TOOL_CACHE/Python//x64/ // So if `findLocalTool` succeeded above, we must have a conformant `installDir` diff --git a/src/find-pypy.ts b/src/find-pypy.ts new file mode 100644 index 000000000..ef8586f36 --- /dev/null +++ b/src/find-pypy.ts @@ -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-'. See readme for more examples." + ); + } + const pythonVersion = versions[1]; + let pypyVersion: string; + if (versions.length > 2) { + pypyVersion = pypyInstall.pypyVersionToSemantic(versions[2]); + } else { + pypyVersion = 'x'; + } + + return { + pypyVersion: pypyVersion, + pythonVersion: pythonVersion + }; +} + +function getPyPyVersionFromPath(installDir: string) { + return path.basename(path.dirname(installDir)); +} diff --git a/src/find-python.ts b/src/find-python.ts index 6702430c5..68136bd23 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -1,5 +1,6 @@ import * as os from 'os'; import * as path from 'path'; +import {IS_WINDOWS, IS_LINUX} from './utils'; import * as semver from 'semver'; @@ -8,9 +9,6 @@ import * as installer from './install-python'; import * as core from '@actions/core'; import * as tc from '@actions/tool-cache'; -const IS_WINDOWS = process.platform === 'win32'; -const IS_LINUX = process.platform === 'linux'; - // Python has "scripts" or "bin" directories where command-line tools that come with packages are installed. // This is where pip is, along with anything that pip installs. // There is a seperate directory for `pip install --user`. diff --git a/src/install-pypy.ts b/src/install-pypy.ts new file mode 100644 index 000000000..b8ef167ba --- /dev/null +++ b/src/install-pypy.ts @@ -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); + + 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(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( + `${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( + 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 '/PyPy//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) { + const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc))(\d*)/g; + return versionSpec.replace(prereleaseVersion, '$1-$2.$3'); +} diff --git a/src/install-python.ts b/src/install-python.ts index 8fcfe68ee..526e7d59d 100644 --- a/src/install-python.ts +++ b/src/install-python.ts @@ -3,7 +3,7 @@ import * as core from '@actions/core'; import * as tc from '@actions/tool-cache'; import * as exec from '@actions/exec'; import {ExecOptions} from '@actions/exec/lib/interfaces'; -import {stderr} from 'process'; +import {IS_WINDOWS, IS_LINUX} from './utils'; const TOKEN = core.getInput('token'); const AUTH = !TOKEN || isGhes() ? undefined : `token ${TOKEN}`; @@ -12,9 +12,6 @@ const MANIFEST_REPO_NAME = 'python-versions'; const MANIFEST_REPO_BRANCH = 'main'; export const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`; -const IS_WINDOWS = process.platform === 'win32'; -const IS_LINUX = process.platform === 'linux'; - export async function findReleaseFromManifest( semanticVersionSpec: string, architecture: string diff --git a/src/setup-python.ts b/src/setup-python.ts index c97f314ca..15e46956b 100644 --- a/src/setup-python.ts +++ b/src/setup-python.ts @@ -1,15 +1,29 @@ import * as core from '@actions/core'; import * as finder from './find-python'; +import * as finderPyPy from './find-pypy'; import * as path from 'path'; import * as os from 'os'; +function isPyPyVersion(versionSpec: string) { + return versionSpec.startsWith('pypy-'); +} + async function run() { try { let version = core.getInput('python-version'); if (version) { const arch: string = core.getInput('architecture') || os.arch(); - const installed = await finder.findPythonVersion(version, arch); - core.info(`Successfully setup ${installed.impl} (${installed.version})`); + if (isPyPyVersion(version)) { + const installed = await finderPyPy.findPyPyVersion(version, arch); + core.info( + `Successfully setup PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})` + ); + } else { + const installed = await finder.findPythonVersion(version, arch); + core.info( + `Successfully setup ${installed.impl} (${installed.version})` + ); + } } const matchersPath = path.join(__dirname, '..', '.github'); core.info(`##[add-matcher]${path.join(matchersPath, 'python.json')}`); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..039f19297 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,42 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export const IS_WINDOWS = process.platform === 'win32'; +export const IS_LINUX = process.platform === 'linux'; + +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[]; +} + +/** create Symlinks for downloaded PyPy + * 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'); + } +} From 3d613a97df2047e8c2abd223f9e293c4d50f0c39 Mon Sep 17 00:00:00 2001 From: Dmitry Shibanov Date: Tue, 15 Dec 2020 16:27:56 +0300 Subject: [PATCH 2/5] resolved comments, update readme, add e2e tests. --- .github/workflows/test-pypy.yml | 47 +++++++++++++++ .../workflows/{test.yml => test-python.yml} | 12 ++-- README.md | 57 +++++++++++++++++-- dist/index.js | 42 ++++++++++---- src/find-pypy.ts | 15 ++++- src/install-pypy.ts | 36 +++++++----- src/utils.ts | 9 +++ 7 files changed, 180 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/test-pypy.yml rename .github/workflows/{test.yml => test-python.yml} (95%) diff --git a/.github/workflows/test-pypy.yml b/.github/workflows/test-pypy.yml new file mode 100644 index 000000000..4f5d31dbf --- /dev/null +++ b/.github/workflows/test-pypy.yml @@ -0,0 +1,47 @@ +name: Validate PyPy e2e +on: + push: + branches: + - main + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + schedule: + - cron: 0 0 * * * + +jobs: + setup-pypy: + name: Setup PyPy ${{ matrix.pypy }} ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest, ubuntu-18.04, ubuntu-20.04] + pypy: + - 'pypy-2.7' + - 'pypy-3.6' + - 'pypy-3.7' + - 'pypy-2.7-v7.3.2' + - 'pypy-3.6-v7.3.2' + - 'pypy-3.7-v7.3.2' + - 'pypy-3.6-v7.3.x' + - 'pypy-3.7-v7.x' + - 'pypy-3.6-v7.3.3rc1' + - 'pypy-3.7-nightly' + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: setup-python ${{ matrix.pypy }} + uses: ./ + with: + python-version: ${{ matrix.pypy }} + + - name: PyPy and Python version + run: python --version + + - name: Run simple code + run: python -c 'import math; print(math.factorial(5))' \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test-python.yml similarity index 95% rename from .github/workflows/test.yml rename to .github/workflows/test-python.yml index 9e4fc7dac..2026d7c99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test-python.yml @@ -1,4 +1,4 @@ -name: Validate 'setup-python' +name: Validate Python e2e on: push: branches: @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-16.04, ubuntu-18.04] + os: [macos-latest, windows-latest, ubuntu-16.04, ubuntu-18.04, ubuntu-20.04] steps: - name: Checkout uses: actions/checkout@v2 @@ -38,7 +38,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-16.04, ubuntu-18.04] + os: [macos-latest, windows-latest, ubuntu-16.04, ubuntu-18.04, ubuntu-20.04] python: [3.5.4, 3.6.7, 3.7.5, 3.8.1] steps: - name: Checkout @@ -68,7 +68,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-16.04, ubuntu-18.04] + os: [macos-latest, windows-latest, ubuntu-16.04, ubuntu-18.04, ubuntu-20.04] steps: - name: Checkout uses: actions/checkout@v2 @@ -91,13 +91,13 @@ jobs: - name: Run simple code run: python -c 'import math; print(math.factorial(5))' - setup-pypy: + setup-pypy-legacy-way: name: Setup PyPy ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-16.04, ubuntu-18.04] + os: [macos-latest, windows-latest, ubuntu-16.04, ubuntu-18.04, ubuntu-20.04] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/README.md b/README.md index f9ee8a6b7..d169b086f 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ This action sets up a Python environment for use in actions by: - Allows for pinning to a specific patch version of Python without the worry of it ever being removed or changed. - Automatic setup and download of Python packages if using a self-hosted runner. - Support for pre-release versions of Python. +- Support for installation any version of PyPy on-flight # Usage @@ -40,7 +41,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '2.x', '3.x', 'pypy2', 'pypy3' ] + python-version: [ '2.x', '3.x', 'pypy-2.7', 'pypy-3.6', 'pypy-3.7' ] name: Python ${{ matrix.python-version }} sample steps: - uses: actions/checkout@v2 @@ -60,7 +61,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [2.7, 3.6, 3.7, 3.8, pypy2, pypy3] + python-version: [2.7, 3.6, 3.7, 3.8, pypy-2.7, pypy-3.6] exclude: - os: macos-latest python-version: 3.8 @@ -91,7 +92,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - run: python my_script.py - ``` Download and set up an accurate pre-release version of Python: @@ -114,6 +114,27 @@ steps: - run: python my_script.py ``` +Download and set up PyPy: + +```yaml +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - pypy-3.6 # the latest available version of PyPy + - pypy-3.7 # the latest available version of PyPy + - pypy-3.7-v7.3.3 # Python 3.7 and PyPy 7.3.3 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - run: python my_script.py +``` +More details on PyPy syntax and examples of using preview / nightly versions of PyPy can be found in [Available versions of PyPy](#available-versions-of-pypy) section. + # Getting started with Python + Actions Check out our detailed guide on using [Python with GitHub Actions](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/using-python-with-github-actions). @@ -129,7 +150,21 @@ Check out our detailed guide on using [Python with GitHub Actions](https://help. - If the exact patch version doesn't matter to you, specifying just the major and minor version will get you the latest preinstalled patch version. In the previous example, the version spec `3.8` will use the `3.8.2` Python version found in the cache. - Downloadable Python versions from GitHub Releases ([actions/python-versions](https://github.com/actions/python-versions/releases)). - All available versions are listed in the [version-manifest.json](https://github.com/actions/python-versions/blob/main/versions-manifest.json) file. - - If there is a specific version of Python that is not available, you can open an issue here. + - If there is a specific version of Python that is not available, you can open an issue here + + # Available versions of PyPy + + `setup-python` is able to configure PyPy from two sources: + +- Preinstalled versions of PyPy in the tools cache on GitHub-hosted runners + - For detailed information regarding the available versions of PyPy that are installed see [Supported software](https://docs.github.com/en/actions/reference/specifications-for-github-hosted-runners#supported-software). + - For the latest PyPy release, all version of Python are cached. + - Cache is updated with 1-2 weeks delay. if you specify PyPy as `pypy-3.6`, the version from cache will be used although a new version is available. If you need to start using the recently released version right after release, you should specify exact PyPy version `pypy-3.6-v7.3.3`. + +- Downloadable PyPy versions from [official PyPy site](https://downloads.python.org/pypy/). + - All available versions are listed in the [versions.json](https://downloads.python.org/pypy/versions.json) file. + - PyPy < 7.3.3 are not available to install on-flight. + - If some versions are not available, you can open an issue in https://foss.heptapod.net/pypy/pypy/ # Hosted Tool Cache @@ -155,6 +190,20 @@ You should specify only a major and minor version if you are okay with the most - There will be a single patch version already installed on each runner for every minor version of Python that is supported. - The patch version that will be preinstalled, will generally be the latest and every time there is a new patch released, the older version that is preinstalled will be replaced. - Using the most recent patch version will result in a very quick setup since no downloads will be required since a locally installed version Python on the runner will be used. + +# Specifying a PyPy version +The version of PyPy should be specified in the format `pypy-[-v]`. +Parameter `` is optional and can be skipped. The latest version will be used in this case. + +``` +pypy-3.6 # the latest available version of PyPy +pypy-3.7 # the latest available version of PyPy +pypy-2.7 # the latest available version of PyPy +pypy-3.7-v7.3.3 # Python 3.7 and PyPy 7.3.3 +pypy-3.7-v7.x # Python 3.7 and the latest available PyPy 7.x +pypy-3.7-v7.3.3rc1 # Python 3.7 and preview version of PyPy +pypy-3.7-nightly # Python 3.7 and nightly PyPy +``` # Using `setup-python` with a self hosted runner diff --git a/dist/index.js b/dist/index.js index c6684881c..16f3f88ac 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1154,7 +1154,8 @@ function findPyPyToolCache(pythonVersion, pypyVersion, architecture) { function parsePyPyVersion(versionSpec) { 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-'. See readme for more examples."); + core.setFailed("Invalid 'version' property for PyPy. PyPy version should be specified as 'pypy-'. See README for examples and documentation."); + process.exit(); } const pythonVersion = versions[1]; let pypyVersion; @@ -1164,6 +1165,10 @@ function parsePyPyVersion(versionSpec) { else { pypyVersion = 'x'; } + if (!utils_1.validateVersion(pythonVersion) || !utils_1.validateVersion(pypyVersion)) { + core.setFailed("Invalid 'version' property for PyPy. Both Python version and PyPy versions should satisfy SemVer notation. See README for examples and documentation."); + process.exit(); + } return { pypyVersion: pypyVersion, pythonVersion: pythonVersion @@ -2316,6 +2321,7 @@ var __importStar = (this && this.__importStar) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); const fs = __importStar(__webpack_require__(747)); const path = __importStar(__webpack_require__(622)); +const semver = __importStar(__webpack_require__(876)); exports.IS_WINDOWS = process.platform === 'win32'; exports.IS_LINUX = process.platform === 'linux'; /** create Symlinks for downloaded PyPy @@ -2334,6 +2340,14 @@ function createSymlinkInFolder(folderPath, sourceName, targetName, setExecutable } } exports.createSymlinkInFolder = createSymlinkInFolder; +function validateVersion(version) { + return isNightlyKeyword(version) || Boolean(semver.validRange(version)); +} +exports.validateVersion = validateVersion; +function isNightlyKeyword(pypyVersion) { + return pypyVersion === 'nightly'; +} +exports.isNightlyKeyword = isNightlyKeyword; /***/ }), @@ -2766,9 +2780,14 @@ function installPyPy(pypyVersion, pythonVersion, architecture) { return __awaiter(this, void 0, void 0, function* () { let downloadDir; const releases = yield getAvailablePyPyVersions(); + if (!releases || releases.length === 0) { + core.setFailed('No release was found in PyPy version.json'); + process.exit(); + } const releaseData = findRelease(releases, pythonVersion, pypyVersion, architecture); if (!releaseData || !releaseData.foundAsset) { - throw new Error(`PyPy version ${pythonVersion} (${pypyVersion}) with arch ${architecture} not found`); + core.setFailed(`PyPy version ${pythonVersion} (${pypyVersion}) with arch ${architecture} not found`); + process.exit(); } const { foundAsset, resolvedPythonVersion, resolvedPyPyVersion } = releaseData; let downloadUrl = `${foundAsset.download_url}`; @@ -2786,7 +2805,7 @@ function installPyPy(pypyVersion, pythonVersion, architecture) { const archiveName = fs.readdirSync(downloadDir)[0]; const toolDir = path.join(downloadDir, archiveName); let installDir = toolDir; - if (!isNightlyKeyword(resolvedPyPyVersion)) { + if (!utils_1.isNightlyKeyword(resolvedPyPyVersion)) { installDir = yield tc.cacheDir(toolDir, 'PyPy', resolvedPythonVersion, architecture); } writeExactPyPyVersionFile(installDir, resolvedPyPyVersion); @@ -2803,7 +2822,8 @@ function getAvailablePyPyVersions() { const http = new httpm.HttpClient('tool-cache'); const response = yield http.getJson(url); if (!response.result) { - throw new Error(`Unable to retrieve the list of available PyPy versions from '${url}'`); + core.setFailed(`Unable to retrieve the list of available PyPy versions from '${url}'`); + process.exit(); } return response.result; }); @@ -2830,12 +2850,13 @@ function installPip(pythonLocation) { } function findRelease(releases, pythonVersion, pypyVersion, architecture) { const filterReleases = releases.filter(item => { - const isPythonVersionSatisfies = semver.satisfies(semver.coerce(item.python_version), pythonVersion); - const isPyPyNightly = isNightlyKeyword(pypyVersion) && isNightlyKeyword(item.pypy_version); - const isPyPyVersionSatisfies = isPyPyNightly || + const isPythonVersionSatisfied = semver.satisfies(semver.coerce(item.python_version), pythonVersion); + const isPyPyNightly = utils_1.isNightlyKeyword(pypyVersion) && utils_1.isNightlyKeyword(item.pypy_version); + const isPyPyVersionSatisfied = 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; + const isArchPresent = item.files && + item.files.some(file => file.arch === architecture && file.platform === process.platform); + return isPythonVersionSatisfied && isPyPyVersionSatisfied && isArchPresent; }); if (filterReleases.length === 0) { return null; @@ -2884,9 +2905,6 @@ function getPyPyBinaryPath(installDir) { return utils_1.IS_WINDOWS ? installDir : _binDir; } exports.getPyPyBinaryPath = getPyPyBinaryPath; -function isNightlyKeyword(pypyVersion) { - return pypyVersion === 'nightly'; -} function pypyVersionToSemantic(versionSpec) { const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc))(\d*)/g; return versionSpec.replace(prereleaseVersion, '$1-$2.$3'); diff --git a/src/find-pypy.ts b/src/find-pypy.ts index ef8586f36..6cb30a8e3 100644 --- a/src/find-pypy.ts +++ b/src/find-pypy.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as pypyInstall from './install-pypy'; -import {IS_WINDOWS} from './utils'; +import {IS_WINDOWS, validateVersion} from './utils'; import * as semver from 'semver'; import * as core from '@actions/core'; @@ -93,10 +93,12 @@ 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-'. See readme for more examples." + core.setFailed( + "Invalid 'version' property for PyPy. PyPy version should be specified as 'pypy-'. See README for examples and documentation." ); + process.exit(); } + const pythonVersion = versions[1]; let pypyVersion: string; if (versions.length > 2) { @@ -105,6 +107,13 @@ function parsePyPyVersion(versionSpec: string): IPyPyVersionSpec { pypyVersion = 'x'; } + if (!validateVersion(pythonVersion) || !validateVersion(pypyVersion)) { + core.setFailed( + "Invalid 'version' property for PyPy. Both Python version and PyPy versions should satisfy SemVer notation. See README for examples and documentation." + ); + process.exit(); + } + return { pypyVersion: pypyVersion, pythonVersion: pythonVersion diff --git a/src/install-pypy.ts b/src/install-pypy.ts index b8ef167ba..8d7306bab 100644 --- a/src/install-pypy.ts +++ b/src/install-pypy.ts @@ -6,7 +6,12 @@ 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'; +import { + IS_WINDOWS, + IPyPyManifestRelease, + createSymlinkInFolder, + isNightlyKeyword +} from './utils'; const PYPY_VERSION_FILE = 'PYPY_VERSION'; @@ -18,6 +23,11 @@ export async function installPyPy( let downloadDir; const releases = await getAvailablePyPyVersions(); + if (!releases || releases.length === 0) { + core.setFailed('No release was found in PyPy version.json'); + process.exit(); + } + const releaseData = findRelease( releases, pythonVersion, @@ -26,9 +36,10 @@ export async function installPyPy( ); if (!releaseData || !releaseData.foundAsset) { - throw new Error( + core.setFailed( `PyPy version ${pythonVersion} (${pypyVersion}) with arch ${architecture} not found` ); + process.exit(); } const {foundAsset, resolvedPythonVersion, resolvedPyPyVersion} = releaseData; @@ -74,9 +85,10 @@ async function getAvailablePyPyVersions() { const response = await http.getJson(url); if (!response.result) { - throw new Error( + core.setFailed( `Unable to retrieve the list of available PyPy versions from '${url}'` ); + process.exit(); } return response.result; @@ -124,19 +136,21 @@ function findRelease( architecture: string ) { const filterReleases = releases.filter(item => { - const isPythonVersionSatisfies = semver.satisfies( + const isPythonVersionSatisfied = semver.satisfies( semver.coerce(item.python_version)!, pythonVersion ); const isPyPyNightly = isNightlyKeyword(pypyVersion) && isNightlyKeyword(item.pypy_version); - const isPyPyVersionSatisfies = + const isPyPyVersionSatisfied = 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; + const isArchPresent = + item.files && + item.files.some( + file => file.arch === architecture && file.platform === process.platform + ); + return isPythonVersionSatisfied && isPyPyVersionSatisfied && isArchPresent; }); if (filterReleases.length === 0) { @@ -206,10 +220,6 @@ export function getPyPyBinaryPath(installDir: string) { return IS_WINDOWS ? installDir : _binDir; } -function isNightlyKeyword(pypyVersion: string) { - return pypyVersion === 'nightly'; -} - export function pypyVersionToSemantic(versionSpec: string) { const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc))(\d*)/g; return versionSpec.replace(prereleaseVersion, '$1-$2.$3'); diff --git a/src/utils.ts b/src/utils.ts index 039f19297..b164268de 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; +import * as semver from 'semver'; export const IS_WINDOWS = process.platform === 'win32'; export const IS_LINUX = process.platform === 'linux'; @@ -40,3 +41,11 @@ export function createSymlinkInFolder( fs.chmodSync(targetPath, '755'); } } + +export function validateVersion(version: string) { + return isNightlyKeyword(version) || Boolean(semver.validRange(version)); +} + +export function isNightlyKeyword(pypyVersion: string) { + return pypyVersion === 'nightly'; +} From ef9020329d6af7fbcb8c62ce43b2fd58ee6feea4 Mon Sep 17 00:00:00 2001 From: Dmitry Shibanov Date: Tue, 15 Dec 2020 20:36:35 +0300 Subject: [PATCH 3/5] resolve throw error --- dist/index.js | 30 ++++++++++++++---------------- src/find-pypy.ts | 18 ++++++------------ src/install-pypy.ts | 13 +++++-------- src/utils.ts | 4 ++++ 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/dist/index.js b/dist/index.js index 16f3f88ac..5fb5226b5 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1137,7 +1137,7 @@ function findPyPyToolCache(pythonVersion, pypyVersion, 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); + resolvedPythonVersion = utils_1.getPyPyVersionFromPath(installDir); resolvedPyPyVersion = pypyInstall.readExactPyPyVersion(installDir); const isPyPyVersionSatisfies = semver.satisfies(resolvedPyPyVersion, pypyVersion); if (!isPyPyVersionSatisfies) { @@ -1151,11 +1151,11 @@ function findPyPyToolCache(pythonVersion, pypyVersion, architecture) { } return { installDir, resolvedPythonVersion, resolvedPyPyVersion }; } +exports.findPyPyToolCache = findPyPyToolCache; function parsePyPyVersion(versionSpec) { const versions = versionSpec.split('-').filter(item => !!item); - if (versions.length < 2) { - core.setFailed("Invalid 'version' property for PyPy. PyPy version should be specified as 'pypy-'. See README for examples and documentation."); - process.exit(); + if (versions.length < 2 || versions[0] != 'pypy') { + throw new Error("Invalid 'version' property for PyPy. PyPy version should be specified as 'pypy-'. See README for examples and documentation."); } const pythonVersion = versions[1]; let pypyVersion; @@ -1166,17 +1166,14 @@ function parsePyPyVersion(versionSpec) { pypyVersion = 'x'; } if (!utils_1.validateVersion(pythonVersion) || !utils_1.validateVersion(pypyVersion)) { - core.setFailed("Invalid 'version' property for PyPy. Both Python version and PyPy versions should satisfy SemVer notation. See README for examples and documentation."); - process.exit(); + throw new Error("Invalid 'version' property for PyPy. Both Python version and PyPy versions should satisfy SemVer notation. See README for examples and documentation."); } return { pypyVersion: pypyVersion, pythonVersion: pythonVersion }; } -function getPyPyVersionFromPath(installDir) { - return path.basename(path.dirname(installDir)); -} +exports.parsePyPyVersion = parsePyPyVersion; /***/ }), @@ -2348,6 +2345,10 @@ function isNightlyKeyword(pypyVersion) { return pypyVersion === 'nightly'; } exports.isNightlyKeyword = isNightlyKeyword; +function getPyPyVersionFromPath(installDir) { + return path.basename(path.dirname(installDir)); +} +exports.getPyPyVersionFromPath = getPyPyVersionFromPath; /***/ }), @@ -2781,13 +2782,11 @@ function installPyPy(pypyVersion, pythonVersion, architecture) { let downloadDir; const releases = yield getAvailablePyPyVersions(); if (!releases || releases.length === 0) { - core.setFailed('No release was found in PyPy version.json'); - process.exit(); + throw new Error('No release was found in PyPy version.json'); } const releaseData = findRelease(releases, pythonVersion, pypyVersion, architecture); if (!releaseData || !releaseData.foundAsset) { - core.setFailed(`PyPy version ${pythonVersion} (${pypyVersion}) with arch ${architecture} not found`); - process.exit(); + throw new Error(`PyPy version ${pythonVersion} (${pypyVersion}) with arch ${architecture} not found`); } const { foundAsset, resolvedPythonVersion, resolvedPyPyVersion } = releaseData; let downloadUrl = `${foundAsset.download_url}`; @@ -2822,8 +2821,7 @@ function getAvailablePyPyVersions() { const http = new httpm.HttpClient('tool-cache'); const response = yield http.getJson(url); if (!response.result) { - core.setFailed(`Unable to retrieve the list of available PyPy versions from '${url}'`); - process.exit(); + throw new Error(`Unable to retrieve the list of available PyPy versions from '${url}'`); } return response.result; }); @@ -2844,7 +2842,6 @@ function installPip(pythonLocation) { core.info('Installing and updating pip'); const pythonBinary = path.join(pythonLocation, 'python'); yield exec.exec(`${pythonBinary} -m ensurepip`); - // TO-DO should we skip updating of pip ? yield exec.exec(`${pythonLocation}/python -m pip install --ignore-installed pip`); }); } @@ -2873,6 +2870,7 @@ function findRelease(releases, pythonVersion, pypyVersion, architecture) { resolvedPyPyVersion: foundRelease.pypy_version }; } +exports.findRelease = findRelease; // helper functions /** * In tool-cache, we put PyPy to '/PyPy//x64' diff --git a/src/find-pypy.ts b/src/find-pypy.ts index 6cb30a8e3..1fc443986 100644 --- a/src/find-pypy.ts +++ b/src/find-pypy.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as pypyInstall from './install-pypy'; -import {IS_WINDOWS, validateVersion} from './utils'; +import {IS_WINDOWS, validateVersion, getPyPyVersionFromPath} from './utils'; import * as semver from 'semver'; import * as core from '@actions/core'; @@ -54,7 +54,7 @@ export async function findPyPyVersion( return {resolvedPyPyVersion, resolvedPythonVersion}; } -function findPyPyToolCache( +export function findPyPyToolCache( pythonVersion: string, pypyVersion: string, architecture: string @@ -89,14 +89,13 @@ function findPyPyToolCache( return {installDir, resolvedPythonVersion, resolvedPyPyVersion}; } -function parsePyPyVersion(versionSpec: string): IPyPyVersionSpec { +export function parsePyPyVersion(versionSpec: string): IPyPyVersionSpec { const versions = versionSpec.split('-').filter(item => !!item); - if (versions.length < 2) { - core.setFailed( + if (versions.length < 2 || versions[0] != 'pypy') { + throw new Error( "Invalid 'version' property for PyPy. PyPy version should be specified as 'pypy-'. See README for examples and documentation." ); - process.exit(); } const pythonVersion = versions[1]; @@ -108,10 +107,9 @@ function parsePyPyVersion(versionSpec: string): IPyPyVersionSpec { } if (!validateVersion(pythonVersion) || !validateVersion(pypyVersion)) { - core.setFailed( + throw new Error( "Invalid 'version' property for PyPy. Both Python version and PyPy versions should satisfy SemVer notation. See README for examples and documentation." ); - process.exit(); } return { @@ -119,7 +117,3 @@ function parsePyPyVersion(versionSpec: string): IPyPyVersionSpec { pythonVersion: pythonVersion }; } - -function getPyPyVersionFromPath(installDir: string) { - return path.basename(path.dirname(installDir)); -} diff --git a/src/install-pypy.ts b/src/install-pypy.ts index 8d7306bab..cf006e0d5 100644 --- a/src/install-pypy.ts +++ b/src/install-pypy.ts @@ -24,8 +24,7 @@ export async function installPyPy( const releases = await getAvailablePyPyVersions(); if (!releases || releases.length === 0) { - core.setFailed('No release was found in PyPy version.json'); - process.exit(); + throw new Error('No release was found in PyPy version.json'); } const releaseData = findRelease( @@ -36,10 +35,9 @@ export async function installPyPy( ); if (!releaseData || !releaseData.foundAsset) { - core.setFailed( + throw new Error( `PyPy version ${pythonVersion} (${pypyVersion}) with arch ${architecture} not found` ); - process.exit(); } const {foundAsset, resolvedPythonVersion, resolvedPyPyVersion} = releaseData; @@ -85,10 +83,9 @@ async function getAvailablePyPyVersions() { const response = await http.getJson(url); if (!response.result) { - core.setFailed( + throw new Error( `Unable to retrieve the list of available PyPy versions from '${url}'` ); - process.exit(); } return response.result; @@ -123,13 +120,13 @@ 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( `${pythonLocation}/python -m pip install --ignore-installed pip` ); } -function findRelease( +export function findRelease( releases: IPyPyManifestRelease[], pythonVersion: string, pypyVersion: string, diff --git a/src/utils.ts b/src/utils.ts index b164268de..52143b34c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -49,3 +49,7 @@ export function validateVersion(version: string) { export function isNightlyKeyword(pypyVersion: string) { return pypyVersion === 'nightly'; } + +export function getPyPyVersionFromPath(installDir: string) { + return path.basename(path.dirname(installDir)); +} From e6c917a949516d04d01bbd9eea3dd92dbcc87e43 Mon Sep 17 00:00:00 2001 From: Dmitry Shibanov Date: Wed, 16 Dec 2020 15:46:38 +0300 Subject: [PATCH 4/5] Add pypy unit tests to cover code * add tests * Update test-pypy.yml * Update test-python.yml * Update test-python.yml * Update README.md * fixing tests * change order Co-authored-by: Maxim Lobanov --- .github/workflows/test-pypy.yml | 4 +- .github/workflows/test-python.yml | 4 +- README.md | 24 +- __tests__/data/pypy.json | 494 ++++++++++++++++++++++++++++++ __tests__/find-pypy.test.ts | 248 +++++++++++++++ __tests__/install-pypy.test.ts | 230 ++++++++++++++ dist/index.js | 69 +++-- src/find-pypy.ts | 9 +- src/install-pypy.ts | 36 +-- src/utils.ts | 29 +- 10 files changed, 1063 insertions(+), 84 deletions(-) create mode 100644 __tests__/data/pypy.json create mode 100644 __tests__/find-pypy.test.ts create mode 100644 __tests__/install-pypy.test.ts diff --git a/.github/workflows/test-pypy.yml b/.github/workflows/test-pypy.yml index 4f5d31dbf..4041440d4 100644 --- a/.github/workflows/test-pypy.yml +++ b/.github/workflows/test-pypy.yml @@ -9,7 +9,7 @@ on: paths-ignore: - '**.md' schedule: - - cron: 0 0 * * * + - cron: 30 3 * * * jobs: setup-pypy: @@ -44,4 +44,4 @@ jobs: run: python --version - name: Run simple code - run: python -c 'import math; print(math.factorial(5))' \ No newline at end of file + run: python -c 'import math; print(math.factorial(5))' diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 2026d7c99..3a85ee22d 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -9,7 +9,7 @@ on: paths-ignore: - '**.md' schedule: - - cron: 0 0 * * * + - cron: 30 3 * * * jobs: default-version: @@ -91,7 +91,7 @@ jobs: - name: Run simple code run: python -c 'import math; print(math.factorial(5))' - setup-pypy-legacy-way: + setup-pypy-legacy: name: Setup PyPy ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: diff --git a/README.md b/README.md index d169b086f..d1fb79e97 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This action sets up a Python environment for use in actions by: - Allows for pinning to a specific patch version of Python without the worry of it ever being removed or changed. - Automatic setup and download of Python packages if using a self-hosted runner. - Support for pre-release versions of Python. -- Support for installation any version of PyPy on-flight +- Support for installing any version of PyPy on-flight # Usage @@ -123,8 +123,8 @@ jobs: strategy: matrix: python-version: - - pypy-3.6 # the latest available version of PyPy - - pypy-3.7 # the latest available version of PyPy + - pypy-3.6 # the latest available version of PyPy that supports Python 3.6 + - pypy-3.7 # the latest available version of PyPy that supports Python 3.7 - pypy-3.7-v7.3.3 # Python 3.7 and PyPy 7.3.3 steps: - uses: actions/checkout@v2 @@ -133,7 +133,7 @@ jobs: python-version: ${{ matrix.python-version }} - run: python my_script.py ``` -More details on PyPy syntax and examples of using preview / nightly versions of PyPy can be found in [Available versions of PyPy](#available-versions-of-pypy) section. +More details on PyPy syntax and examples of using preview / nightly versions of PyPy can be found in the [Available versions of PyPy](#available-versions-of-pypy) section. # Getting started with Python + Actions @@ -158,11 +158,11 @@ Check out our detailed guide on using [Python with GitHub Actions](https://help. - Preinstalled versions of PyPy in the tools cache on GitHub-hosted runners - For detailed information regarding the available versions of PyPy that are installed see [Supported software](https://docs.github.com/en/actions/reference/specifications-for-github-hosted-runners#supported-software). - - For the latest PyPy release, all version of Python are cached. - - Cache is updated with 1-2 weeks delay. if you specify PyPy as `pypy-3.6`, the version from cache will be used although a new version is available. If you need to start using the recently released version right after release, you should specify exact PyPy version `pypy-3.6-v7.3.3`. + - For the latest PyPy release, all versions of Python are cached. + - Cache is updated with a 1-2 week delay. If you specify the PyPy version as `pypy-3.6`, the cached version will be used although a newer version is available. If you need to start using the recently released version right after release, you should specify the exact PyPy version using `pypy-3.6-v7.3.3`. -- Downloadable PyPy versions from [official PyPy site](https://downloads.python.org/pypy/). - - All available versions are listed in the [versions.json](https://downloads.python.org/pypy/versions.json) file. +- Downloadable PyPy versions from the [official PyPy site](https://downloads.python.org/pypy/). + - All available versions that we can download are listed in [versions.json](https://downloads.python.org/pypy/versions.json) file. - PyPy < 7.3.3 are not available to install on-flight. - If some versions are not available, you can open an issue in https://foss.heptapod.net/pypy/pypy/ @@ -193,12 +193,12 @@ You should specify only a major and minor version if you are okay with the most # Specifying a PyPy version The version of PyPy should be specified in the format `pypy-[-v]`. -Parameter `` is optional and can be skipped. The latest version will be used in this case. +The `` parameter is optional and can be skipped. The latest version will be used in this case. ``` -pypy-3.6 # the latest available version of PyPy -pypy-3.7 # the latest available version of PyPy -pypy-2.7 # the latest available version of PyPy +pypy-3.6 # the latest available version of PyPy that supports Python 3.6 +pypy-3.7 # the latest available version of PyPy that supports Python 3.7 +pypy-2.7 # the latest available version of PyPy that supports Python 2.7 pypy-3.7-v7.3.3 # Python 3.7 and PyPy 7.3.3 pypy-3.7-v7.x # Python 3.7 and the latest available PyPy 7.x pypy-3.7-v7.3.3rc1 # Python 3.7 and preview version of PyPy diff --git a/__tests__/data/pypy.json b/__tests__/data/pypy.json new file mode 100644 index 000000000..95e06bbb1 --- /dev/null +++ b/__tests__/data/pypy.json @@ -0,0 +1,494 @@ +[ + { + "pypy_version": "7.3.3", + "python_version": "3.6.12", + "stable": true, + "latest_pypy": true, + "date": "2020-11-21", + "files": [ + { + "filename": "pypy3.6-v7.3.3-aarch64.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.3-aarch64.tar.bz2" + }, + { + "filename": "pypy3.6-v7.3.3-linux32.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.3-linux32.tar.bz2" + }, + { + "filename": "pypy3.6-v7.3.3-linux64.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.3-linux64.tar.bz2" + }, + { + "filename": "pypy3.6-v7.3.3-darwin64.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.3-darwin64.tar.bz2" + }, + { + "filename": "pypy3.6-v7.3.3-win32.zip", + "arch": "x86", + "platform": "win32", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.3-win32.zip" + }, + { + "filename": "pypy3.6-v7.3.3-s390x.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.3-s390x.tar.bz2" + } + ] + }, + { + "pypy_version": "7.3.3rc1", + "python_version": "3.6.12", + "stable": false, + "latest_pypy": false, + "date": "2020-11-11", + "files": [ + { + "filename": "pypy3.6-v7.3.3rc1-aarch64.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.3rc1-aarch64.tar.bz2" + }, + { + "filename": "pypy3.6-v7.3.3-linux32rc1.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.3rc1-linux32.tar.bz2" + }, + { + "filename": "pypy3.6-v7.3.3rc1-linux64.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.3rc1-linux64.tar.bz2" + }, + { + "filename": "pypy3.6-v7.3.3rc1-osx64.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.3rc1-osx64.tar.bz2" + }, + { + "filename": "pypy3.6-v7.3.3-win32rc1.zip", + "arch": "x86", + "platform": "win32", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.3rc1-win32.zip" + }, + { + "filename": "pypy3.6-v7.3.3rc1-s390x.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.3rc1-s390x.tar.bz2" + } + ] + }, + { + "pypy_version": "7.3.3rc2", + "python_version": "3.7.7", + "stable": false, + "latest_pypy": false, + "date": "2020-11-11", + "files": [ + { + "filename": "test.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "test.tar.bz2" + }, + { + "filename": "test.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "test.tar.bz2" + }, + { + "filename": "test.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "test.tar.bz2" + }, + { + "filename": "test.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "test.tar.bz2" + }, + { + "filename": "test.zip", + "arch": "x86", + "platform": "win32", + "download_url": "test.zip" + }, + { + "filename": "test.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "test.tar.bz2" + } + ] + }, + { + "pypy_version": "7.3.3", + "python_version": "3.7.9", + "stable": true, + "latest_pypy": true, + "date": "2020-11-21", + "files": [ + { + "filename": "pypy3.7-v7.3.3-aarch64.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.7-v7.3.3-aarch64.tar.bz2" + }, + { + "filename": "pypy3.7-v7.3.3-linux32.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.7-v7.3.3-linux32.tar.bz2" + }, + { + "filename": "pypy3.7-v7.3.3-linux64.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.7-v7.3.3-linux64.tar.bz2" + }, + { + "filename": "pypy3.7-v7.3.3-osx64.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "https://test.download.python.org/pypy/pypy3.7-v7.3.3-osx64.tar.bz2" + }, + { + "filename": "pypy3.7-v7.3.3-win32.zip", + "arch": "x86", + "platform": "win32", + "download_url": "https://test.download.python.org/pypy/pypy3.7-v7.3.3-win32.zip" + }, + { + "filename": "pypy3.7-v7.3.3-s390x.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.7-v7.3.3-s390x.tar.bz2" + } + ] + }, + { + "pypy_version": "7.3.3", + "python_version": "2.7.18", + "stable": true, + "latest_pypy": true, + "date": "2020-11-21", + "files": [ + { + "filename": "pypy2.7-v7.3.3-aarch64.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy2.7-v7.3.3-aarch64.tar.bz2" + }, + { + "filename": "pypy2.7-v7.3.3-linux32.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy2.7-v7.3.3-linux32.tar.bz2" + }, + { + "filename": "pypy2.7-v7.3.3-linux64.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy2.7-v7.3.3-linux64.tar.bz2" + }, + { + "filename": "pypy2.7-v7.3.3-osx64.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "https://test.download.python.org/pypy/pypy2.7-v7.3.3-osx64.tar.bz2" + }, + { + "filename": "pypy2.7-v7.3.3-win32.zip", + "arch": "x86", + "platform": "win32", + "download_url": "https://test.download.python.org/pypy/pypy2.7-v7.3.3-win32.zip" + }, + { + "filename": "pypy2.7-v7.3.3-s390x.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy2.7-v7.3.3-s390x.tar.bz2" + } + ] + }, + { + "pypy_version": "7.3.2", + "python_version": "3.6.9", + "stable": true, + "latest_pypy": true, + "date": "2020-09-25", + "files": [ + { + "filename": "pypy3.6-v7.3.2-aarch64.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.2-aarch64.tar.bz2" + }, + { + "filename": "pypy3.6-v7.3.2-linux32.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.2-linux32.tar.bz2" + }, + { + "filename": "pypy3.6-v7.3.2-linux64.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.2-linux64.tar.bz2" + }, + { + "filename": "pypy3.6-v7.3.2-osx64.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.2-osx64.tar.bz2" + }, + { + "filename": "pypy3.6-v7.3.2-win32.zip", + "arch": "x86", + "platform": "win32", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.2-win32.zip" + }, + { + "filename": "pypy3.6-v7.3.2-s390x.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.3.2-s390x.tar.bz2" + } + ] + }, + { + "pypy_version": "7.3.2", + "python_version": "3.7.9", + "stable": true, + "latest_pypy": false, + "date": "2020-09-25", + "files": [ + { + "filename": "pypy3.7-v7.3.2-aarch64.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.7-v7.3.2-aarch64.tar.bz2" + }, + { + "filename": "pypy3.7-v7.3.2-linux32.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.7-v7.3.2-linux32.tar.bz2" + }, + { + "filename": "pypy3.7-v7.3.2-linux64.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.7-v7.3.2-linux64.tar.bz2" + }, + { + "filename": "pypy3.7-v7.3.2-osx64.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "https://test.download.python.org/pypy/pypy3.7-v7.3.2-osx64.tar.bz2" + }, + { + "filename": "pypy3.7-v7.3.2-win32.zip", + "arch": "x86", + "platform": "win32", + "download_url": "https://test.download.python.org/pypy/pypy3.7-v7.3.2-win32.zip" + }, + { + "filename": "pypy3.7-v7.3.2-s390x.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.7-v7.3.2-s390x.tar.bz2" + } + ] + }, + { + "pypy_version": "7.3.2", + "python_version": "2.7.13", + "stable": true, + "latest_pypy": true, + "date": "2020-09-25", + "files": [ + { + "filename": "pypy2.7-v7.3.2-aarch64.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy2.7-v7.3.2-aarch64.tar.bz2" + }, + { + "filename": "pypy2.7-v7.3.2-linux32.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy2.7-v7.3.2-linux32.tar.bz2" + }, + { + "filename": "pypy2.7-v7.3.2-linux64.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy2.7-v7.3.2-linux64.tar.bz2" + }, + { + "filename": "pypy2.7-v7.3.2-osx64.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "https://test.download.python.org/pypy/pypy2.7-v7.3.2-osx64.tar.bz2" + }, + { + "filename": "pypy2.7-v7.3.2-win32.zip", + "arch": "x86", + "platform": "win32", + "download_url": "https://test.download.python.org/pypy/pypy2.7-v7.3.2-win32.zip" + }, + { + "filename": "pypy2.7-v7.3.2-s390x.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy2.7-v7.3.2-s390x.tar.bz2" + } + ] + }, + { + "pypy_version": "nightly", + "python_version": "2.7", + "stable": false, + "latest_pypy": false, + "files": [ + { + "filename": "filename.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + }, + { + "filename": "filename.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + }, + { + "filename": "filename.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + }, + { + "filename": "filename.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + }, + { + "filename": "filename.zip", + "arch": "x86", + "platform": "win32", + "download_url": "http://nightlyBuilds.org/filename.zip" + }, + { + "filename": "filename.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + } + ] + }, + { + "pypy_version": "nightly", + "python_version": "3.7", + "stable": false, + "latest_pypy": false, + "files": [ + { + "filename": "filename.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + }, + { + "filename": "filename.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + }, + { + "filename": "filename.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + }, + { + "filename": "filename.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + }, + { + "filename": "filename.zip", + "arch": "x86", + "platform": "win32", + "download_url": "http://nightlyBuilds.org/filename.zip" + }, + { + "filename": "filename.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + } + ] + }, + { + "pypy_version": "nightly", + "python_version": "3.6", + "stable": false, + "latest_pypy": false, + "files": [ + { + "filename": "filename.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + }, + { + "filename": "filename.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + }, + { + "filename": "filename.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + }, + { + "filename": "filename.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + }, + { + "filename": "filename.zip", + "arch": "x86", + "platform": "win32", + "download_url": "http://nightlyBuilds.org/filename.zip" + }, + { + "filename": "filename.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "http://nightlyBuilds.org/filename.tar.bz2" + } + ] + } +] \ No newline at end of file diff --git a/__tests__/find-pypy.test.ts b/__tests__/find-pypy.test.ts new file mode 100644 index 000000000..b9c6599f9 --- /dev/null +++ b/__tests__/find-pypy.test.ts @@ -0,0 +1,248 @@ +import fs from 'fs'; + +import * as utils from '../src/utils'; +import {HttpClient} from '@actions/http-client'; +import * as ifm from '@actions/http-client/interfaces'; +import * as tc from '@actions/tool-cache'; +import * as exec from '@actions/exec'; + +import * as path from 'path'; +import * as semver from 'semver'; + +import * as finder from '../src/find-pypy'; +import { + IPyPyManifestRelease, + IS_WINDOWS, + validateVersion, + getPyPyVersionFromPath +} from '../src/utils'; + +const manifestData = require('./data/pypy.json'); + +let architecture: string; + +if (IS_WINDOWS) { + architecture = 'x86'; +} else { + architecture = 'x64'; +} + +const toolDir = path.join(__dirname, 'runner', 'tools'); +const tempDir = path.join(__dirname, 'runner', 'temp'); + +describe('parsePyPyVersion', () => { + it.each([ + ['pypy-3.6-v7.3.3', {pythonVersion: '3.6', pypyVersion: 'v7.3.3'}], + ['pypy-3.6-v7.3.x', {pythonVersion: '3.6', pypyVersion: 'v7.3.x'}], + ['pypy-3.6-v7.x', {pythonVersion: '3.6', pypyVersion: 'v7.x'}], + ['pypy-3.6', {pythonVersion: '3.6', pypyVersion: 'x'}], + ['pypy-3.6-nightly', {pythonVersion: '3.6', pypyVersion: 'nightly'}], + ['pypy-3.6-v7.3.3rc1', {pythonVersion: '3.6', pypyVersion: 'v7.3.3-rc.1'}] + ])('%s -> %s', (input, expected) => { + expect(finder.parsePyPyVersion(input)).toEqual(expected); + }); + + it('throw on invalid input', () => { + expect(() => finder.parsePyPyVersion('pypy-')).toThrowError( + "Invalid 'version' property for PyPy. PyPy version should be specified as 'pypy-'. See README for examples and documentation." + ); + }); +}); + +describe('validateVersion', () => { + it.each([ + ['v7.3.3', true], + ['v7.3.x', true], + ['v7.x', true], + ['x', true], + ['v7.3.3-rc.1', true], + ['nightly', true], + ['v7.3.b', false], + ['3.6', true], + ['3.b', false], + ['3', true] + ])('%s -> %s', (input, expected) => { + expect(validateVersion(input)).toEqual(expected); + }); +}); + +describe('getPyPyVersionFromPath', () => { + it('/fake/toolcache/PyPy/3.6.5/x64 -> 3.6.5', () => { + expect(getPyPyVersionFromPath('/fake/toolcache/PyPy/3.6.5/x64')).toEqual( + '3.6.5' + ); + }); +}); + +describe('findPyPyToolCache', () => { + const actualPythonVersion = '3.6.17'; + const actualPyPyVersion = '7.5.4'; + const pypyPath = path.join('PyPy', actualPythonVersion, architecture); + let tcFind: jest.SpyInstance; + let spyReadExactPyPyVersion: jest.SpyInstance; + + beforeEach(() => { + tcFind = jest.spyOn(tc, 'find'); + tcFind.mockImplementation((toolname: string, pythonVersion: string) => { + const semverVersion = new semver.Range(pythonVersion); + return semver.satisfies(actualPythonVersion, semverVersion) + ? pypyPath + : ''; + }); + + spyReadExactPyPyVersion = jest.spyOn(utils, 'readExactPyPyVersionFile'); + spyReadExactPyPyVersion.mockImplementation(() => actualPyPyVersion); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('PyPy exists on the path and versions are satisfied', () => { + expect(finder.findPyPyToolCache('3.6.17', 'v7.5.4', architecture)).toEqual({ + installDir: pypyPath, + resolvedPythonVersion: actualPythonVersion, + resolvedPyPyVersion: actualPyPyVersion + }); + }); + + it('PyPy exists on the path and versions are satisfied with semver', () => { + expect(finder.findPyPyToolCache('3.6', 'v7.5.x', architecture)).toEqual({ + installDir: pypyPath, + resolvedPythonVersion: actualPythonVersion, + resolvedPyPyVersion: actualPyPyVersion + }); + }); + + it("PyPy exists on the path, but Python version doesn't match", () => { + expect(finder.findPyPyToolCache('3.7', 'v7.5.4', architecture)).toEqual({ + installDir: '', + resolvedPythonVersion: '', + resolvedPyPyVersion: '' + }); + }); + + it("PyPy exists on the path, but PyPy version doesn't match", () => { + expect(finder.findPyPyToolCache('3.6', 'v7.5.1', architecture)).toEqual({ + installDir: null, + resolvedPythonVersion: '', + resolvedPyPyVersion: '' + }); + }); +}); + +describe('findPyPyVersion', () => { + let tcFind: jest.SpyInstance; + let spyExtractZip: jest.SpyInstance; + let spyExtractTar: jest.SpyInstance; + let spyHttpClient: jest.SpyInstance; + let spyExistsSync: jest.SpyInstance; + let spyExec: jest.SpyInstance; + let spySymlinkSync: jest.SpyInstance; + let spyDownloadTool: jest.SpyInstance; + let spyReadExactPyPyVersion: jest.SpyInstance; + let spyFsReadDir: jest.SpyInstance; + let spyWriteExactPyPyVersionFile: jest.SpyInstance; + let spyCacheDir: jest.SpyInstance; + let spyChmodSync: jest.SpyInstance; + + beforeEach(() => { + tcFind = jest.spyOn(tc, 'find'); + tcFind.mockImplementation((tool: string, version: string) => { + const semverRange = new semver.Range(version); + let pypyPath = ''; + if (semver.satisfies('3.6.12', semverRange)) { + pypyPath = path.join(toolDir, 'PyPy', '3.6.12', architecture); + } + return pypyPath; + }); + + spyWriteExactPyPyVersionFile = jest.spyOn( + utils, + 'writeExactPyPyVersionFile' + ); + spyWriteExactPyPyVersionFile.mockImplementation(() => null); + + spyReadExactPyPyVersion = jest.spyOn(utils, 'readExactPyPyVersionFile'); + spyReadExactPyPyVersion.mockImplementation(() => '7.3.3'); + + spyDownloadTool = jest.spyOn(tc, 'downloadTool'); + spyDownloadTool.mockImplementation(() => path.join(tempDir, 'PyPy')); + + spyExtractZip = jest.spyOn(tc, 'extractZip'); + spyExtractZip.mockImplementation(() => tempDir); + + spyExtractTar = jest.spyOn(tc, 'extractTar'); + spyExtractTar.mockImplementation(() => tempDir); + + spyFsReadDir = jest.spyOn(fs, 'readdirSync'); + spyFsReadDir.mockImplementation((directory: string) => ['PyPyTest']); + + spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); + spyHttpClient.mockImplementation( + async (): Promise> => { + const result = JSON.stringify(manifestData); + return { + statusCode: 200, + headers: {}, + result: JSON.parse(result) as IPyPyManifestRelease[] + }; + } + ); + + spyExec = jest.spyOn(exec, 'exec'); + spyExec.mockImplementation(() => undefined); + + spySymlinkSync = jest.spyOn(fs, 'symlinkSync'); + spySymlinkSync.mockImplementation(() => undefined); + + spyExistsSync = jest.spyOn(fs, 'existsSync'); + spyExistsSync.mockReturnValue(true); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('found PyPy in toolcache', async () => { + await expect( + finder.findPyPyVersion('pypy-3.6-v7.3.x', architecture) + ).resolves.toEqual({ + resolvedPythonVersion: '3.6.12', + resolvedPyPyVersion: '7.3.3' + }); + }); + + it('throw on invalid input format', async () => { + await expect( + finder.findPyPyVersion('pypy3.7-v7.3.x', architecture) + ).rejects.toThrow(); + }); + + it('found and install successfully', async () => { + spyCacheDir = jest.spyOn(tc, 'cacheDir'); + spyCacheDir.mockImplementation(() => + path.join(toolDir, 'PyPy', '3.7.7', architecture) + ); + spyChmodSync = jest.spyOn(fs, 'chmodSync'); + spyChmodSync.mockImplementation(() => undefined); + await expect( + finder.findPyPyVersion('pypy-3.7-v7.3.x', architecture) + ).resolves.toEqual({ + resolvedPythonVersion: '3.7.9', + resolvedPyPyVersion: '7.3.3' + }); + }); + + it('throw if release is not found', async () => { + await expect( + finder.findPyPyVersion('pypy3.7-v7.3.x', architecture) + ).rejects.toThrowError( + "Invalid 'version' property for PyPy. PyPy version should be specified as 'pypy-'. See README for examples and documentation." + ); + }); +}); diff --git a/__tests__/install-pypy.test.ts b/__tests__/install-pypy.test.ts new file mode 100644 index 000000000..cffc90e8f --- /dev/null +++ b/__tests__/install-pypy.test.ts @@ -0,0 +1,230 @@ +import fs from 'fs'; + +import {HttpClient} from '@actions/http-client'; +import * as ifm from '@actions/http-client/interfaces'; +import * as tc from '@actions/tool-cache'; +import * as exec from '@actions/exec'; +import * as path from 'path'; + +import * as installer from '../src/install-pypy'; +import { + IPyPyManifestRelease, + IPyPyManifestAsset, + IS_WINDOWS +} from '../src/utils'; + +const manifestData = require('./data/pypy.json'); + +let architecture: string; +if (IS_WINDOWS) { + architecture = 'x86'; +} else { + architecture = 'x64'; +} + +const toolDir = path.join(__dirname, 'runner', 'tools'); +const tempDir = path.join(__dirname, 'runner', 'temp'); + +describe('pypyVersionToSemantic', () => { + it.each([ + ['7.3.3rc1', '7.3.3-rc.1'], + ['7.3.3', '7.3.3'], + ['7.3.x', '7.3.x'], + ['7.x', '7.x'], + ['nightly', 'nightly'] + ])('%s -> %s', (input, expected) => { + expect(installer.pypyVersionToSemantic(input)).toEqual(expected); + }); +}); + +describe('findRelease', () => { + const result = JSON.stringify(manifestData); + const releases = JSON.parse(result) as IPyPyManifestRelease[]; + const extension = IS_WINDOWS ? '.zip' : '.tar.bz2'; + const extensionName = IS_WINDOWS + ? `${process.platform}${extension}` + : `${process.platform}64${extension}`; + const files: IPyPyManifestAsset = { + filename: `pypy3.6-v7.3.3-${extensionName}`, + arch: architecture, + platform: process.platform, + download_url: `https://test.download.python.org/pypy/pypy3.6-v7.3.3-${extensionName}` + }; + + it("Python version is found, but PyPy version doesn't match", () => { + const pythonVersion = '3.6'; + const pypyVersion = '7.3.7'; + expect( + installer.findRelease(releases, pythonVersion, pypyVersion, architecture) + ).toEqual(null); + }); + + it('Python version is found and PyPy version matches', () => { + const pythonVersion = '3.6'; + const pypyVersion = '7.3.3'; + expect( + installer.findRelease(releases, pythonVersion, pypyVersion, architecture) + ).toEqual({ + foundAsset: files, + resolvedPythonVersion: '3.6.12', + resolvedPyPyVersion: pypyVersion + }); + }); + + it('Python version is found in toolcache and PyPy version matches semver', () => { + const pythonVersion = '3.6'; + const pypyVersion = '7.x'; + expect( + installer.findRelease(releases, pythonVersion, pypyVersion, architecture) + ).toEqual({ + foundAsset: files, + resolvedPythonVersion: '3.6.12', + resolvedPyPyVersion: '7.3.3' + }); + }); + + it('Python and preview version of PyPy are found', () => { + const pythonVersion = '3.7'; + const pypyVersion = installer.pypyVersionToSemantic('7.3.3rc2'); + expect( + installer.findRelease(releases, pythonVersion, pypyVersion, architecture) + ).toEqual({ + foundAsset: { + filename: `test${extension}`, + arch: architecture, + platform: process.platform, + download_url: `test${extension}` + }, + resolvedPythonVersion: '3.7.7', + resolvedPyPyVersion: '7.3.3rc2' + }); + }); + + it('Python version with latest PyPy is found', () => { + const pythonVersion = '3.6'; + const pypyVersion = 'x'; + expect( + installer.findRelease(releases, pythonVersion, pypyVersion, architecture) + ).toEqual({ + foundAsset: files, + resolvedPythonVersion: '3.6.12', + resolvedPyPyVersion: '7.3.3' + }); + }); + + it('Nightly release is found', () => { + const pythonVersion = '3.6'; + const pypyVersion = 'nightly'; + const filename = IS_WINDOWS ? 'filename.zip' : 'filename.tar.bz2'; + expect( + installer.findRelease(releases, pythonVersion, pypyVersion, architecture) + ).toEqual({ + foundAsset: { + filename: filename, + arch: architecture, + platform: process.platform, + download_url: `http://nightlyBuilds.org/${filename}` + }, + resolvedPythonVersion: '3.6', + resolvedPyPyVersion: pypyVersion + }); + }); +}); + +describe('installPyPy', () => { + let tcFind: jest.SpyInstance; + let spyExtractZip: jest.SpyInstance; + let spyExtractTar: jest.SpyInstance; + let spyFsReadDir: jest.SpyInstance; + let spyFsWriteFile: jest.SpyInstance; + let spyHttpClient: jest.SpyInstance; + let spyExistsSync: jest.SpyInstance; + let spyExec: jest.SpyInstance; + let spySymlinkSync: jest.SpyInstance; + let spyDownloadTool: jest.SpyInstance; + let spyCacheDir: jest.SpyInstance; + let spyChmodSync: jest.SpyInstance; + + beforeEach(() => { + tcFind = jest.spyOn(tc, 'find'); + tcFind.mockImplementation(() => path.join('PyPy', '3.6.12', architecture)); + + spyDownloadTool = jest.spyOn(tc, 'downloadTool'); + spyDownloadTool.mockImplementation(() => path.join(tempDir, 'PyPy')); + + spyExtractZip = jest.spyOn(tc, 'extractZip'); + spyExtractZip.mockImplementation(() => tempDir); + + spyExtractTar = jest.spyOn(tc, 'extractTar'); + spyExtractTar.mockImplementation(() => tempDir); + + spyFsReadDir = jest.spyOn(fs, 'readdirSync'); + spyFsReadDir.mockImplementation(() => ['PyPyTest']); + + spyFsWriteFile = jest.spyOn(fs, 'writeFileSync'); + spyFsWriteFile.mockImplementation(() => undefined); + + spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); + spyHttpClient.mockImplementation( + async (): Promise> => { + const result = JSON.stringify(manifestData); + return { + statusCode: 200, + headers: {}, + result: JSON.parse(result) as IPyPyManifestRelease[] + }; + } + ); + + spyExec = jest.spyOn(exec, 'exec'); + spyExec.mockImplementation(() => undefined); + + spySymlinkSync = jest.spyOn(fs, 'symlinkSync'); + spySymlinkSync.mockImplementation(() => undefined); + + spyExistsSync = jest.spyOn(fs, 'existsSync'); + spyExistsSync.mockImplementation(() => false); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('throw if release is not found', async () => { + await expect( + installer.installPyPy('7.3.3', '3.6.17', architecture) + ).rejects.toThrowError( + `PyPy version 3.6.17 (7.3.3) with arch ${architecture} not found` + ); + + expect(spyHttpClient).toHaveBeenCalled(); + expect(spyDownloadTool).not.toHaveBeenCalled(); + expect(spyExec).not.toHaveBeenCalled(); + }); + + it('found and install PyPy', async () => { + spyCacheDir = jest.spyOn(tc, 'cacheDir'); + spyCacheDir.mockImplementation(() => + path.join(toolDir, 'PyPy', '3.6.12', architecture) + ); + + spyChmodSync = jest.spyOn(fs, 'chmodSync'); + spyChmodSync.mockImplementation(() => undefined); + + await expect( + installer.installPyPy('7.3.x', '3.6.12', architecture) + ).resolves.toEqual({ + installDir: path.join(toolDir, 'PyPy', '3.6.12', architecture), + resolvedPythonVersion: '3.6.12', + resolvedPyPyVersion: '7.3.3' + }); + + expect(spyHttpClient).toHaveBeenCalled(); + expect(spyDownloadTool).toHaveBeenCalled(); + expect(spyExistsSync).toHaveBeenCalled(); + expect(spyCacheDir).toHaveBeenCalled(); + expect(spyExec).toHaveBeenCalled(); + }); +}); diff --git a/dist/index.js b/dist/index.js index 5fb5226b5..4992e541d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1138,7 +1138,7 @@ function findPyPyToolCache(pythonVersion, pypyVersion, architecture) { // 'tc.find' finds tool based on Python version but we also need to check // whether PyPy version satisfies requested version. resolvedPythonVersion = utils_1.getPyPyVersionFromPath(installDir); - resolvedPyPyVersion = pypyInstall.readExactPyPyVersion(installDir); + resolvedPyPyVersion = utils_1.readExactPyPyVersionFile(installDir); const isPyPyVersionSatisfies = semver.satisfies(resolvedPyPyVersion, pypyVersion); if (!isPyPyVersionSatisfies) { installDir = null; @@ -2308,6 +2308,9 @@ exports.debug = debug; // for test "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; @@ -2316,11 +2319,12 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __importStar(__webpack_require__(747)); +const fs_1 = __importDefault(__webpack_require__(747)); const path = __importStar(__webpack_require__(622)); const semver = __importStar(__webpack_require__(876)); exports.IS_WINDOWS = process.platform === 'win32'; exports.IS_LINUX = process.platform === 'linux'; +const PYPY_VERSION_FILE = 'PYPY_VERSION'; /** create Symlinks for downloaded PyPy * It should be executed only for downloaded versions in runtime, because * toolcache versions have this setup. @@ -2328,12 +2332,12 @@ exports.IS_LINUX = process.platform === 'linux'; function createSymlinkInFolder(folderPath, sourceName, targetName, setExecutable = false) { const sourcePath = path.join(folderPath, sourceName); const targetPath = path.join(folderPath, targetName); - if (fs.existsSync(targetPath)) { + if (fs_1.default.existsSync(targetPath)) { return; } - fs.symlinkSync(sourcePath, targetPath); + fs_1.default.symlinkSync(sourcePath, targetPath); if (!exports.IS_WINDOWS && setExecutable) { - fs.chmodSync(targetPath, '755'); + fs_1.default.chmodSync(targetPath, '755'); } } exports.createSymlinkInFolder = createSymlinkInFolder; @@ -2349,6 +2353,28 @@ function getPyPyVersionFromPath(installDir) { return path.basename(path.dirname(installDir)); } exports.getPyPyVersionFromPath = getPyPyVersionFromPath; +/** + * In tool-cache, we put PyPy to '/PyPy//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' + */ +function readExactPyPyVersionFile(installDir) { + let pypyVersion = ''; + let fileVersion = path.join(installDir, PYPY_VERSION_FILE); + if (fs_1.default.existsSync(fileVersion)) { + pypyVersion = fs_1.default.readFileSync(fileVersion).toString(); + } + return pypyVersion; +} +exports.readExactPyPyVersionFile = readExactPyPyVersionFile; +function writeExactPyPyVersionFile(installDir, resolvedPyPyVersion) { + const pypyFilePath = path.join(installDir, PYPY_VERSION_FILE); + fs_1.default.writeFileSync(pypyFilePath, resolvedPyPyVersion); +} +exports.writeExactPyPyVersionFile = writeExactPyPyVersionFile; /***/ }), @@ -2767,6 +2793,9 @@ var __importStar = (this && this.__importStar) || function (mod) { result["default"] = mod; return result; }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); const path = __importStar(__webpack_require__(622)); const core = __importStar(__webpack_require__(470)); @@ -2774,9 +2803,8 @@ const tc = __importStar(__webpack_require__(533)); const semver = __importStar(__webpack_require__(876)); const httpm = __importStar(__webpack_require__(539)); const exec = __importStar(__webpack_require__(986)); -const fs = __importStar(__webpack_require__(747)); +const fs_1 = __importDefault(__webpack_require__(747)); const utils_1 = __webpack_require__(163); -const PYPY_VERSION_FILE = 'PYPY_VERSION'; function installPyPy(pypyVersion, pythonVersion, architecture) { return __awaiter(this, void 0, void 0, function* () { let downloadDir; @@ -2801,13 +2829,13 @@ function installPyPy(pypyVersion, pythonVersion, architecture) { } // 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 archiveName = fs_1.default.readdirSync(downloadDir)[0]; const toolDir = path.join(downloadDir, archiveName); let installDir = toolDir; if (!utils_1.isNightlyKeyword(resolvedPyPyVersion)) { installDir = yield tc.cacheDir(toolDir, 'PyPy', resolvedPythonVersion, architecture); } - writeExactPyPyVersionFile(installDir, resolvedPyPyVersion); + utils_1.writeExactPyPyVersionFile(installDir, resolvedPyPyVersion); const binaryPath = getPyPyBinaryPath(installDir); yield createPyPySymlink(binaryPath, resolvedPythonVersion); yield installPip(binaryPath); @@ -2871,29 +2899,6 @@ function findRelease(releases, pythonVersion, pypyVersion, architecture) { }; } exports.findRelease = findRelease; -// helper functions -/** - * In tool-cache, we put PyPy to '/PyPy//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' - */ -function readExactPyPyVersion(installDir) { - 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; -} -exports.readExactPyPyVersion = readExactPyPyVersion; -function writeExactPyPyVersionFile(installDir, resolvedPyPyVersion) { - 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. diff --git a/src/find-pypy.ts b/src/find-pypy.ts index 1fc443986..b45f48f84 100644 --- a/src/find-pypy.ts +++ b/src/find-pypy.ts @@ -1,6 +1,11 @@ import * as path from 'path'; import * as pypyInstall from './install-pypy'; -import {IS_WINDOWS, validateVersion, getPyPyVersionFromPath} from './utils'; +import { + IS_WINDOWS, + validateVersion, + getPyPyVersionFromPath, + readExactPyPyVersionFile +} from './utils'; import * as semver from 'semver'; import * as core from '@actions/core'; @@ -67,7 +72,7 @@ export function findPyPyToolCache( // '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); + resolvedPyPyVersion = readExactPyPyVersionFile(installDir); const isPyPyVersionSatisfies = semver.satisfies( resolvedPyPyVersion, diff --git a/src/install-pypy.ts b/src/install-pypy.ts index cf006e0d5..99d603000 100644 --- a/src/install-pypy.ts +++ b/src/install-pypy.ts @@ -4,17 +4,16 @@ 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 fs from 'fs'; import { IS_WINDOWS, IPyPyManifestRelease, createSymlinkInFolder, - isNightlyKeyword + isNightlyKeyword, + writeExactPyPyVersionFile } from './utils'; -const PYPY_VERSION_FILE = 'PYPY_VERSION'; - export async function installPyPy( pypyVersion: string, pythonVersion: string, @@ -179,35 +178,6 @@ export function findRelease( }; } -// helper functions - -/** - * In tool-cache, we put PyPy to '/PyPy//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. diff --git a/src/utils.ts b/src/utils.ts index 52143b34c..845fe0973 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,10 @@ -import * as fs from 'fs'; +import fs from 'fs'; import * as path from 'path'; import * as semver from 'semver'; export const IS_WINDOWS = process.platform === 'win32'; export const IS_LINUX = process.platform === 'linux'; +const PYPY_VERSION_FILE = 'PYPY_VERSION'; export interface IPyPyManifestAsset { filename: string; @@ -53,3 +54,29 @@ export function isNightlyKeyword(pypyVersion: string) { export function getPyPyVersionFromPath(installDir: string) { return path.basename(path.dirname(installDir)); } + +/** + * In tool-cache, we put PyPy to '/PyPy//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 = ''; + let fileVersion = path.join(installDir, PYPY_VERSION_FILE); + if (fs.existsSync(fileVersion)) { + pypyVersion = fs.readFileSync(fileVersion).toString(); + } + + return pypyVersion; +} + +export function writeExactPyPyVersionFile( + installDir: string, + resolvedPyPyVersion: string +) { + const pypyFilePath = path.join(installDir, PYPY_VERSION_FILE); + fs.writeFileSync(pypyFilePath, resolvedPyPyVersion); +} From 0c8eaf819ea8152306b5e6aa2e218db32b3f488f Mon Sep 17 00:00:00 2001 From: Dmitry Shibanov Date: Thu, 17 Dec 2020 12:37:02 +0300 Subject: [PATCH 5/5] add pypy tests and fix issue with pypy-3-nightly --- __tests__/find-pypy.test.ts | 27 ++++++++------------------- __tests__/utils.test.ts | 34 ++++++++++++++++++++++++++++++++++ dist/index.js | 13 +++++++++++++ src/find-pypy.ts | 9 ++++++++- src/utils.ts | 10 ++++++++++ 5 files changed, 73 insertions(+), 20 deletions(-) create mode 100644 __tests__/utils.test.ts diff --git a/__tests__/find-pypy.test.ts b/__tests__/find-pypy.test.ts index b9c6599f9..ddf7ebcf4 100644 --- a/__tests__/find-pypy.test.ts +++ b/__tests__/find-pypy.test.ts @@ -49,23 +49,6 @@ describe('parsePyPyVersion', () => { }); }); -describe('validateVersion', () => { - it.each([ - ['v7.3.3', true], - ['v7.3.x', true], - ['v7.x', true], - ['x', true], - ['v7.3.3-rc.1', true], - ['nightly', true], - ['v7.3.b', false], - ['3.6', true], - ['3.b', false], - ['3', true] - ])('%s -> %s', (input, expected) => { - expect(validateVersion(input)).toEqual(expected); - }); -}); - describe('getPyPyVersionFromPath', () => { it('/fake/toolcache/PyPy/3.6.5/x64 -> 3.6.5', () => { expect(getPyPyVersionFromPath('/fake/toolcache/PyPy/3.6.5/x64')).toEqual( @@ -223,6 +206,12 @@ describe('findPyPyVersion', () => { ).rejects.toThrow(); }); + it('throw on invalid input format pypy3.7-7.3.x', async () => { + await expect( + finder.findPyPyVersion('pypy3.7-v7.3.x', architecture) + ).rejects.toThrow(); + }); + it('found and install successfully', async () => { spyCacheDir = jest.spyOn(tc, 'cacheDir'); spyCacheDir.mockImplementation(() => @@ -240,9 +229,9 @@ describe('findPyPyVersion', () => { it('throw if release is not found', async () => { await expect( - finder.findPyPyVersion('pypy3.7-v7.3.x', architecture) + finder.findPyPyVersion('pypy-3.7-v7.5.x', architecture) ).rejects.toThrowError( - "Invalid 'version' property for PyPy. PyPy version should be specified as 'pypy-'. See README for examples and documentation." + `PyPy version 3.7 (v7.5.x) with arch ${architecture} not found` ); }); }); diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts new file mode 100644 index 000000000..9463849f9 --- /dev/null +++ b/__tests__/utils.test.ts @@ -0,0 +1,34 @@ +import { + validateVersion, + validatePythonVersionFormatForPyPy +} from '../src/utils'; + +describe('validatePythonVersionFormatForPyPy', () => { + it.each([ + ['3.6', true], + ['3.7', true], + ['3.6.x', false], + ['3.7.x', false], + ['3.x', false], + ['3', false] + ])('%s -> %s', (input, expected) => { + expect(validatePythonVersionFormatForPyPy(input)).toEqual(expected); + }); +}); + +describe('validateVersion', () => { + it.each([ + ['v7.3.3', true], + ['v7.3.x', true], + ['v7.x', true], + ['x', true], + ['v7.3.3-rc.1', true], + ['nightly', true], + ['v7.3.b', false], + ['3.6', true], + ['3.b', false], + ['3', true] + ])('%s -> %s', (input, expected) => { + expect(validateVersion(input)).toEqual(expected); + }); +}); diff --git a/dist/index.js b/dist/index.js index 4992e541d..3fe97152d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1168,6 +1168,9 @@ function parsePyPyVersion(versionSpec) { if (!utils_1.validateVersion(pythonVersion) || !utils_1.validateVersion(pypyVersion)) { throw new Error("Invalid 'version' property for PyPy. Both Python version and PyPy versions should satisfy SemVer notation. See README for examples and documentation."); } + if (!utils_1.validatePythonVersionFormatForPyPy(pythonVersion)) { + throw new Error("Invalid format of Python version for PyPy. Python version should be specified in format 'x.y'. See README for examples and documentation."); + } return { pypyVersion: pypyVersion, pythonVersion: pythonVersion @@ -2375,6 +2378,16 @@ function writeExactPyPyVersionFile(installDir, resolvedPyPyVersion) { fs_1.default.writeFileSync(pypyFilePath, resolvedPyPyVersion); } exports.writeExactPyPyVersionFile = writeExactPyPyVersionFile; +/** + * Python version should be specified explicitly like "x.y" (2.7, 3.6, 3.7) + * "3.x" or "3" are not supported + * because it could cause ambiguity when both PyPy version and Python version are not precise + */ +function validatePythonVersionFormatForPyPy(version) { + const re = /^\d+\.\d+$/; + return re.test(version); +} +exports.validatePythonVersionFormatForPyPy = validatePythonVersionFormatForPyPy; /***/ }), diff --git a/src/find-pypy.ts b/src/find-pypy.ts index b45f48f84..700ce9ee5 100644 --- a/src/find-pypy.ts +++ b/src/find-pypy.ts @@ -4,7 +4,8 @@ import { IS_WINDOWS, validateVersion, getPyPyVersionFromPath, - readExactPyPyVersionFile + readExactPyPyVersionFile, + validatePythonVersionFormatForPyPy } from './utils'; import * as semver from 'semver'; @@ -117,6 +118,12 @@ export function parsePyPyVersion(versionSpec: string): IPyPyVersionSpec { ); } + if (!validatePythonVersionFormatForPyPy(pythonVersion)) { + throw new Error( + "Invalid format of Python version for PyPy. Python version should be specified in format 'x.y'. See README for examples and documentation." + ); + } + return { pypyVersion: pypyVersion, pythonVersion: pythonVersion diff --git a/src/utils.ts b/src/utils.ts index 845fe0973..e96d5b230 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -80,3 +80,13 @@ export function writeExactPyPyVersionFile( const pypyFilePath = path.join(installDir, PYPY_VERSION_FILE); fs.writeFileSync(pypyFilePath, resolvedPyPyVersion); } + +/** + * Python version should be specified explicitly like "x.y" (2.7, 3.6, 3.7) + * "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); +}