From 3d613a97df2047e8c2abd223f9e293c4d50f0c39 Mon Sep 17 00:00:00 2001 From: Dmitry Shibanov Date: Tue, 15 Dec 2020 16:27:56 +0300 Subject: [PATCH] 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 00000000..4f5d31db --- /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 9e4fc7da..2026d7c9 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 f9ee8a6b..d169b086 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 c6684881..16f3f88a 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 ef8586f3..6cb30a8e 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 b8ef167b..8d7306ba 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 039f1929..b164268d 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'; +}