From 878156f1deda8836af6aca53d02b078285bdf154 Mon Sep 17 00:00:00 2001 From: Peter Mescalchin Date: Mon, 5 Oct 2020 23:51:21 +1100 Subject: [PATCH 01/11] Inject LD_LIBRARY_PATH library path into Python manifest install and setup (#144) * Adding LD_LIBRARY_PATH env var to both setup and install tasks * Rebuild dist/index.js * Fixed some typos in contributors.md Markdown --- dist/index.js | 11 +++++++++++ docs/contributors.md | 6 ++++-- src/find-python.ts | 13 +++++++++++++ src/install-python.ts | 5 +++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/dist/index.js b/dist/index.js index 7ba2ab55f..7fa762a1e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6422,6 +6422,7 @@ var __importStar = (this && this.__importStar) || function (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 exec = __importStar(__webpack_require__(986)); @@ -6432,6 +6433,7 @@ 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); @@ -6443,6 +6445,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') }), silent: true, listeners: { stdout: (data) => { @@ -6688,6 +6691,7 @@ 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`. @@ -6760,6 +6764,13 @@ function useCpythonVersion(version, architecture) { ].join(os.EOL)); } core.exportVariable('pythonLocation', installDir); + if (IS_LINUX) { + const libPath = (process.env.LD_LIBRARY_PATH) ? `:${process.env.LD_LIBRARY_PATH}` : ''; + const pyLibPath = path.join(installDir, 'lib'); + if (!libPath.split(':').includes(pyLibPath)) { + core.exportVariable('LD_LIBRARY_PATH', pyLibPath + libPath); + } + } core.addPath(installDir); core.addPath(binDir(installDir)); if (IS_WINDOWS) { diff --git a/docs/contributors.md b/docs/contributors.md index 92e6d4ded..2833bb167 100644 --- a/docs/contributors.md +++ b/docs/contributors.md @@ -13,12 +13,14 @@ In order to avoid uploading `node_modules/` to the repository, we use [vercel/nc ### Developing If you're developing locally, you can run -``` + +```sh npm install tsc ncc build src/setup-python.ts ``` -Any files generated using `tsc` will be added to `lib/`, however those files also are not uploaded to the repository and are exluded using `.gitignore`. + +Any files generated using `tsc` will be added to `lib/`, however those files also are not uploaded to the repository and are excluded using `.gitignore`. During the commit step, Husky will take care of formatting all files with [Prettier](https://github.com/prettier/prettier) (to run manually, use `npm run format`). diff --git a/src/find-python.ts b/src/find-python.ts index db06230d4..8bc8d5bc4 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -9,6 +9,7 @@ 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. @@ -109,6 +110,18 @@ async function useCpythonVersion( } core.exportVariable('pythonLocation', installDir); + + if (IS_LINUX) { + const libPath = process.env.LD_LIBRARY_PATH + ? `:${process.env.LD_LIBRARY_PATH}` + : ''; + const pyLibPath = path.join(installDir, 'lib'); + + if (!libPath.split(':').includes(pyLibPath)) { + core.exportVariable('LD_LIBRARY_PATH', pyLibPath + libPath); + } + } + core.addPath(installDir); core.addPath(binDir(installDir)); diff --git a/src/install-python.ts b/src/install-python.ts index 36d88781c..8fcfe68ee 100644 --- a/src/install-python.ts +++ b/src/install-python.ts @@ -13,6 +13,7 @@ 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, @@ -35,6 +36,10 @@ export async function findReleaseFromManifest( async function installPython(workingDirectory: string) { const options: ExecOptions = { cwd: workingDirectory, + env: { + ...process.env, + ...(IS_LINUX && {LD_LIBRARY_PATH: path.join(workingDirectory, 'lib')}) + }, silent: true, listeners: { stdout: (data: Buffer) => { From 41b7212b1668f5de9d65e9c82aa777e6bbedb3a8 Mon Sep 17 00:00:00 2001 From: Prince <65214391+er-royalprince@users.noreply.github.com> Date: Wed, 14 Oct 2020 15:29:10 +0530 Subject: [PATCH 02/11] Update README.md (#145) --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 36d2f7f43..f9ee8a6b7 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,17 @@ This action sets up a Python environment for use in actions by: -- optionally installing and adding to PATH a version of Python that is already installed in the tools cache -- downloading, installing and adding to PATH an available version of Python from GitHub Releases ([actions/python-versions](https://github.com/actions/python-versions/releases)) if a specific version is not available in the tools cache -- failing if a specific version of Python is not preinstalled or available for download -- registering problem matchers for error output +- optionally installing and adding to PATH a version of Python that is already installed in the tools cache. +- downloading, installing and adding to PATH an available version of Python from GitHub Releases ([actions/python-versions](https://github.com/actions/python-versions/releases)) if a specific version is not available in the tools cache. +- failing if a specific version of Python is not preinstalled or available for download. +- registering problem matchers for error output. # What's new -- Ability to download, install and set up Python packages from `actions/python-versions` that do not come preinstalled on runners - - 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 +- Ability to download, install and set up Python packages from `actions/python-versions` that do not come preinstalled on runners. + - 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. # Usage @@ -122,14 +122,14 @@ Check out our detailed guide on using [Python with GitHub Actions](https://help. `setup-python` is able to configure Python from two sources: -- Preinstalled versions of Python in the tools cache on GitHub-hosted runners +- Preinstalled versions of Python in the tools cache on GitHub-hosted runners. - For detailed information regarding the available versions of Python that are installed see [Supported software](https://docs.github.com/en/actions/reference/specifications-for-github-hosted-runners#supported-software). - For every minor version of Python, expect only the latest patch to be preinstalled. - If `3.8.1` is installed for example, and `3.8.2` is released, expect `3.8.1` to be removed and replaced by `3.8.2` in the tools cache. - 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)) +- 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. # Hosted Tool Cache @@ -174,22 +174,22 @@ If you are experiencing problems while configuring Python on your self-hosted ru - The Python packages that are downloaded from `actions/python-versions` are originally compiled from source in `/opt/hostedtoolcache/` with the [--enable-shared](https://github.com/actions/python-versions/blob/94f04ae6806c6633c82db94c6406a16e17decd5c/builders/ubuntu-python-builder.psm1#L35) flag, which makes them non-relocatable. - Create an environment variable called `AGENT_TOOLSDIRECTORY` and set it to `/opt/hostedtoolcache`. This controls where the runner downloads and installs tools. - - In the same shell that your runner is using, type `export AGENT_TOOLSDIRECTORY=/opt/hostedtoolcache` + - In the same shell that your runner is using, type `export AGENT_TOOLSDIRECTORY=/opt/hostedtoolcache`. - A more permanent way of setting the environment variable is to create a `.env` file in the same directory as your runner and to add `AGENT_TOOLSDIRECTORY=/opt/hostedtoolcache`. This ensures the variable is always set if your runner is configured as a service. - Create a directory called `hostedtoolcache` inside `/opt`. - The user starting the runner must have write permission to the `/opt/hostedtoolcache` directory. It is not possible to start the Linux runner with `sudo` and the `/opt` directory usually requires root privileges to write to. Check the current user and group that the runner belongs to by typing `ls -l` inside the runners root directory. - The runner can be granted write access to the `/opt/hostedtoolcache` directory using a few techniques: - - The user starting the runner is the owner, and the owner has write permission - - The user starting the runner is in the owning group, and the owning group has write permission - - All users have write permission -- One quick way to grant access is to change the user and group of `/opt/hostedtoolcache` to be the same as the runners using `chown` - - `sudo chown runner-user:runner-group opt/hostedtoolcache/` + - The user starting the runner is the owner, and the owner has write permission. + - The user starting the runner is in the owning group, and the owning group has write permission. + - All users have write permission. +- One quick way to grant access is to change the user and group of `/opt/hostedtoolcache` to be the same as the runners using `chown`. + - `sudo chown runner-user:runner-group opt/hostedtoolcache/`. - If your runner is configured as a service and you run into problems, make sure the user that the service is running as is correct. For more information, you can [check the status of your self-hosted runner](https://help.github.com/en/actions/hosting-your-own-runners/configuring-the-self-hosted-runner-application-as-a-service#checking-the-status-of-the-service). ### Mac - The same setup that applies to `Linux` also applies to `Mac`, just with a different tools cache directory. -- Create a directory called `/Users/runner/hostedtoolcache` +- Create a directory called `/Users/runner/hostedtoolcache`. - Set the `AGENT_TOOLSDIRECTORY` environment variable to `/Users/runner/hostedtoolcache`. - Change the permissions of `/Users/runner/hostedtoolcache` so that the runner has write access. @@ -200,8 +200,8 @@ If you are experiencing problems while configuring Python on your self-hosted ru # License -The scripts and documentation in this project are released under the [MIT License](LICENSE) +The scripts and documentation in this project are released under the [MIT License](LICENSE). # Contributions -Contributions are welcome! See our [Contributor's Guide](docs/contributors.md) +Contributions are welcome! See our [Contributor's Guide](docs/contributors.md). From 195f5c388bc8d0f1c6a942ce7ce156a3124a50a5 Mon Sep 17 00:00:00 2001 From: Brian Cristante <33549821+brcrista@users.noreply.github.com> Date: Wed, 25 Nov 2020 16:04:11 -0500 Subject: [PATCH 03/11] Create CODEOWNERS --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..0155f0506 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @actions-service From 723e46dad7a73e33ab89ea319f9e3e831cd23e62 Mon Sep 17 00:00:00 2001 From: Brian Cristante <33549821+brcrista@users.noreply.github.com> Date: Mon, 7 Dec 2020 15:56:31 -0500 Subject: [PATCH 04/11] CODEOWNERS needs the org name for teams --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0155f0506..9ec45a506 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @actions-service +* @actions/actions-service From 3b3f2de1b1f7c1270fc5b9c37a904fa785e9ae94 Mon Sep 17 00:00:00 2001 From: Maxim Lobanov Date: Tue, 8 Dec 2020 00:59:14 +0300 Subject: [PATCH 05/11] update pypy3 to point to 3.6 (#164) --- .github/workflows/test.yml | 21 +++++++++++++++++++++ dist/index.js | 13 ++++++++----- src/find-python.ts | 12 ++++++++---- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cc23018a9..9e4fc7dac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,3 +90,24 @@ jobs: - name: Run simple code run: python -c 'import math; print(math.factorial(5))' + + setup-pypy: + 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] + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: setup-python pypy3 + uses: ./ + with: + python-version: 'pypy3' + + - name: setup-python pypy2 + uses: ./ + with: + python-version: 'pypy2' diff --git a/dist/index.js b/dist/index.js index 7fa762a1e..41cfa78b7 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6445,7 +6445,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), (IS_LINUX && { LD_LIBRARY_PATH: path.join(workingDirectory, 'lib') })), silent: true, listeners: { stdout: (data) => { @@ -6718,7 +6718,7 @@ function binDir(installDir) { // For example, PyPy 7.0 contains Python 2.7, 3.5, and 3.6-alpha. // We only care about the Python version, so we don't use the PyPy version for the tool cache. function usePyPy(majorVersion, architecture) { - const findPyPy = tc.find.bind(undefined, 'PyPy', majorVersion.toString()); + const findPyPy = tc.find.bind(undefined, 'PyPy', majorVersion); let installDir = findPyPy(architecture); if (!installDir && IS_WINDOWS) { // PyPy only precompiles binaries for x86, but the architecture parameter defaults to x64. @@ -6765,7 +6765,9 @@ function useCpythonVersion(version, architecture) { } core.exportVariable('pythonLocation', installDir); if (IS_LINUX) { - const libPath = (process.env.LD_LIBRARY_PATH) ? `:${process.env.LD_LIBRARY_PATH}` : ''; + const libPath = process.env.LD_LIBRARY_PATH + ? `:${process.env.LD_LIBRARY_PATH}` + : ''; const pyLibPath = path.join(installDir, 'lib'); if (!libPath.split(':').includes(pyLibPath)) { core.exportVariable('LD_LIBRARY_PATH', pyLibPath + libPath); @@ -6819,9 +6821,10 @@ function findPythonVersion(version, architecture) { return __awaiter(this, void 0, void 0, function* () { switch (version.toUpperCase()) { case 'PYPY2': - return usePyPy(2, architecture); + return usePyPy('2', architecture); case 'PYPY3': - return usePyPy(3, architecture); + // keep pypy3 pointing to 3.6 for backward compatibility + return usePyPy('3.6', architecture); default: return yield useCpythonVersion(version, architecture); } diff --git a/src/find-python.ts b/src/find-python.ts index 8bc8d5bc4..6702430c5 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -37,8 +37,11 @@ function binDir(installDir: string): string { // A particular version of PyPy may contain one or more versions of the Python interpreter. // For example, PyPy 7.0 contains Python 2.7, 3.5, and 3.6-alpha. // We only care about the Python version, so we don't use the PyPy version for the tool cache. -function usePyPy(majorVersion: 2 | 3, architecture: string): InstalledVersion { - const findPyPy = tc.find.bind(undefined, 'PyPy', majorVersion.toString()); +function usePyPy( + majorVersion: '2' | '3.6', + architecture: string +): InstalledVersion { + const findPyPy = tc.find.bind(undefined, 'PyPy', majorVersion); let installDir: string | null = findPyPy(architecture); if (!installDir && IS_WINDOWS) { @@ -188,9 +191,10 @@ export async function findPythonVersion( ): Promise { switch (version.toUpperCase()) { case 'PYPY2': - return usePyPy(2, architecture); + return usePyPy('2', architecture); case 'PYPY3': - return usePyPy(3, architecture); + // keep pypy3 pointing to 3.6 for backward compatibility + return usePyPy('3.6', architecture); default: return await useCpythonVersion(version, architecture); } From 2831efe49a72a829fb30b0b3e668563482fd8b9a Mon Sep 17 00:00:00 2001 From: Nikita Bykov <49442273+nikita-bykov@users.noreply.github.com> Date: Thu, 17 Dec 2020 18:02:13 +0300 Subject: [PATCH 06/11] Improve find-python to add "Scripts" folder to PATH on Windows machines (#169) * added 'Scripts' folder to PATH on Windows * add release code * update index.js * rebuild index.js * remove duplicate block Co-authored-by: Nikita Bykov Co-authored-by: Dmitry Shibanov --- dist/index.js | 4 ++++ src/find-python.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/dist/index.js b/dist/index.js index 41cfa78b7..5362e846b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6738,6 +6738,10 @@ function usePyPy(majorVersion, architecture) { core.exportVariable('pythonLocation', pythonLocation); core.addPath(installDir); core.addPath(_binDir); + // Starting from PyPy 7.3.1, the folder that is used for pip and anything that pip installs should be "Scripts" on Windows. + if (IS_WINDOWS) { + core.addPath(path.join(installDir, 'Scripts')); + } const impl = 'pypy' + majorVersion.toString(); core.setOutput('python-version', impl); return { impl: impl, version: versionFromPath(installDir) }; diff --git a/src/find-python.ts b/src/find-python.ts index 6702430c5..6cc21d484 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -66,6 +66,10 @@ function usePyPy( core.addPath(installDir); core.addPath(_binDir); + // Starting from PyPy 7.3.1, the folder that is used for pip and anything that pip installs should be "Scripts" on Windows. + if (IS_WINDOWS) { + core.addPath(path.join(installDir, 'Scripts')); + } const impl = 'pypy' + majorVersion.toString(); core.setOutput('python-version', impl); From 8c5ea631b2b2d5d8840cf4a2b183a8a0edc1e40d Mon Sep 17 00:00:00 2001 From: Dmitry Shibanov Date: Thu, 17 Dec 2020 18:03:54 +0300 Subject: [PATCH 07/11] Adding support for more PyPy versions and installing them on-flight (#168) * add support to install pypy * resolved comments, update readme, add e2e tests. * resolve throw error * 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 * add pypy tests and fix issue with pypy-3-nightly Co-authored-by: Maxim Lobanov --- .github/workflows/test-pypy.yml | 47 ++ .../workflows/{test.yml => test-python.yml} | 14 +- README.md | 57 +- __tests__/data/pypy.json | 494 ++++++++++++++++++ __tests__/find-pypy.test.ts | 237 +++++++++ __tests__/install-pypy.test.ts | 230 ++++++++ __tests__/utils.test.ts | 34 ++ dist/index.js | 374 ++++++++++++- src/find-pypy.ts | 131 +++++ src/find-python.ts | 4 +- src/install-pypy.ts | 193 +++++++ src/install-python.ts | 5 +- src/setup-python.ts | 18 +- src/utils.ts | 92 ++++ 14 files changed, 1896 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/test-pypy.yml rename .github/workflows/{test.yml => test-python.yml} (94%) create mode 100644 __tests__/data/pypy.json create mode 100644 __tests__/find-pypy.test.ts create mode 100644 __tests__/install-pypy.test.ts create mode 100644 __tests__/utils.test.ts create mode 100644 src/find-pypy.ts create mode 100644 src/install-pypy.ts create mode 100644 src/utils.ts diff --git a/.github/workflows/test-pypy.yml b/.github/workflows/test-pypy.yml new file mode 100644 index 000000000..4041440d4 --- /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: 30 3 * * * + +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))' diff --git a/.github/workflows/test.yml b/.github/workflows/test-python.yml similarity index 94% rename from .github/workflows/test.yml rename to .github/workflows/test-python.yml index 9e4fc7dac..3a85ee22d 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: @@ -9,7 +9,7 @@ on: paths-ignore: - '**.md' schedule: - - cron: 0 0 * * * + - cron: 30 3 * * * jobs: default-version: @@ -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: 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..d1fb79e97 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 installing 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 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 + - 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 the [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 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 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/ # 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]`. +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 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 +pypy-3.7-nightly # Python 3.7 and nightly PyPy +``` # Using `setup-python` with a self hosted runner 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..ddf7ebcf4 --- /dev/null +++ b/__tests__/find-pypy.test.ts @@ -0,0 +1,237 @@ +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('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('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(() => + 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('pypy-3.7-v7.5.x', architecture) + ).rejects.toThrowError( + `PyPy version 3.7 (v7.5.x) with arch ${architecture} not found` + ); + }); +}); 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/__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 5362e846b..7cf0e8f43 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1072,6 +1072,113 @@ 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 = utils_1.getPyPyVersionFromPath(installDir); + resolvedPyPyVersion = utils_1.readExactPyPyVersionFile(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 }; +} +exports.findPyPyToolCache = findPyPyToolCache; +function parsePyPyVersion(versionSpec) { + const versions = versionSpec.split('-').filter(item => !!item); + 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; + if (versions.length > 2) { + pypyVersion = pypyInstall.pypyVersionToSemantic(versions[2]); + } + else { + pypyVersion = 'x'; + } + 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 + }; +} +exports.parsePyPyVersion = parsePyPyVersion; + + /***/ }), /***/ 65: @@ -2197,6 +2304,92 @@ 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 __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 = {}; + 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_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. + */ +function createSymlinkInFolder(folderPath, sourceName, targetName, setExecutable = false) { + const sourcePath = path.join(folderPath, sourceName); + const targetPath = path.join(folderPath, targetName); + if (fs_1.default.existsSync(targetPath)) { + return; + } + fs_1.default.symlinkSync(sourcePath, targetPath); + if (!exports.IS_WINDOWS && setExecutable) { + fs_1.default.chmodSync(targetPath, '755'); + } +} +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; +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; +/** + * 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; + + /***/ }), /***/ 164: @@ -2443,16 +2636,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 +2783,151 @@ 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; +}; +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)); +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_1 = __importDefault(__webpack_require__(747)); +const utils_1 = __webpack_require__(163); +function installPyPy(pypyVersion, pythonVersion, architecture) { + return __awaiter(this, void 0, void 0, function* () { + let downloadDir; + const releases = yield getAvailablePyPyVersions(); + if (!releases || releases.length === 0) { + throw new Error('No release was found in PyPy version.json'); + } + 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_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); + } + utils_1.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`); + yield exec.exec(`${pythonLocation}/python -m pip install --ignore-installed pip`); + }); +} +function findRelease(releases, pythonVersion, pypyVersion, architecture) { + const filterReleases = releases.filter(item => { + 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 isArchPresent = item.files && + item.files.some(file => file.arch === architecture && file.platform === process.platform); + return isPythonVersionSatisfied && isPyPyVersionSatisfied && isArchPresent; + }); + 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 + }; +} +exports.findRelease = findRelease; +/** 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 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 +6774,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 +6792,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 +6803,7 @@ function installPython(workingDirectory) { } } }; - if (IS_WINDOWS) { + if (utils_1.IS_WINDOWS) { yield exec.exec('powershell', ['./setup.ps1'], options); } else { @@ -6471,7 +6818,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 +7033,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 +7051,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 +7066,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 +7080,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); @@ -6768,7 +7114,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}` : ''; @@ -6779,7 +7125,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..700ce9ee5 --- /dev/null +++ b/src/find-pypy.ts @@ -0,0 +1,131 @@ +import * as path from 'path'; +import * as pypyInstall from './install-pypy'; +import { + IS_WINDOWS, + validateVersion, + getPyPyVersionFromPath, + readExactPyPyVersionFile, + validatePythonVersionFormatForPyPy +} 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}; +} + +export 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 = readExactPyPyVersionFile(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}; +} + +export function parsePyPyVersion(versionSpec: string): IPyPyVersionSpec { + const versions = versionSpec.split('-').filter(item => !!item); + + 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: string; + if (versions.length > 2) { + pypyVersion = pypyInstall.pypyVersionToSemantic(versions[2]); + } else { + pypyVersion = 'x'; + } + + if (!validateVersion(pythonVersion) || !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 (!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/find-python.ts b/src/find-python.ts index 6cc21d484..ff2a20d8d 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..99d603000 --- /dev/null +++ b/src/install-pypy.ts @@ -0,0 +1,193 @@ +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 fs from 'fs'; + +import { + IS_WINDOWS, + IPyPyManifestRelease, + createSymlinkInFolder, + isNightlyKeyword, + writeExactPyPyVersionFile +} from './utils'; + +export async function installPyPy( + pypyVersion: string, + pythonVersion: string, + architecture: string +) { + let downloadDir; + + const releases = await getAvailablePyPyVersions(); + if (!releases || releases.length === 0) { + throw new Error('No release was found in PyPy version.json'); + } + + 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`); + + await exec.exec( + `${pythonLocation}/python -m pip install --ignore-installed pip` + ); +} + +export function findRelease( + releases: IPyPyManifestRelease[], + pythonVersion: string, + pypyVersion: string, + architecture: string +) { + const filterReleases = releases.filter(item => { + const isPythonVersionSatisfied = semver.satisfies( + semver.coerce(item.python_version)!, + pythonVersion + ); + const isPyPyNightly = + isNightlyKeyword(pypyVersion) && isNightlyKeyword(item.pypy_version); + const isPyPyVersionSatisfied = + isPyPyNightly || + semver.satisfies(pypyVersionToSemantic(item.pypy_version), pypyVersion); + 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; + } + + 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 + }; +} + +/** 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; +} + +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..e96d5b230 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,92 @@ +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; + 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'); + } +} + +export function validateVersion(version: string) { + return isNightlyKeyword(version) || Boolean(semver.validRange(version)); +} + +export function isNightlyKeyword(pypyVersion: string) { + return pypyVersion === 'nightly'; +} + +export function getPyPyVersionFromPath(installDir: string) { + return path.basename(path.dirname(installDir)); +} + +/** + * In tool-cache, we put PyPy to '/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); +} + +/** + * 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); +} From 3105fb18c05ddd93efea5f9e0bef7a03a6e9e7df Mon Sep 17 00:00:00 2001 From: Dmitry Shibanov Date: Fri, 18 Dec 2020 17:05:24 +0300 Subject: [PATCH 08/11] fix is_windows (#172) --- dist/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/index.js b/dist/index.js index 7cf0e8f43..f0939428b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -7085,7 +7085,7 @@ function usePyPy(majorVersion, architecture) { core.addPath(installDir); core.addPath(_binDir); // Starting from PyPy 7.3.1, the folder that is used for pip and anything that pip installs should be "Scripts" on Windows. - if (IS_WINDOWS) { + if (utils_1.IS_WINDOWS) { core.addPath(path.join(installDir, 'Scripts')); } const impl = 'pypy' + majorVersion.toString(); From 66319ca9fa6c19287a320c27bd980953e731b827 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Mon, 4 Jan 2021 04:14:24 -0600 Subject: [PATCH 09/11] Use quotes around Python versions in README (#175) --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d1fb79e97..461ee2ea6 100644 --- a/README.md +++ b/README.md @@ -61,12 +61,12 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [2.7, 3.6, 3.7, 3.8, pypy-2.7, pypy-3.6] + python-version: ['2.7', '3.6', '3.7', '3.8', 'pypy-2.7', 'pypy-3.6'] exclude: - os: macos-latest - python-version: 3.8 + python-version: '3.8' - os: windows-latest - python-version: 3.6 + python-version: '3.6' steps: - uses: actions/checkout@v2 - name: Set up Python @@ -85,7 +85,7 @@ jobs: strategy: matrix: # in this example, there is a newer version already installed, 3.7.7, so the older version will be downloaded - python-version: [3.5, 3.6, 3.7.4, 3.8] + python-version: ['3.5', '3.6', '3.7.4', '3.8'] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 @@ -114,7 +114,7 @@ steps: - run: python my_script.py ``` -Download and set up PyPy: +Download and set up PyPy: ```yaml jobs: @@ -123,9 +123,9 @@ jobs: strategy: matrix: python-version: - - 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 + - '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 - uses: actions/setup-python@v2 @@ -151,11 +151,11 @@ Check out our detailed guide on using [Python with GitHub Actions](https://help. - 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 - + # 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 versions of Python are cached. @@ -190,9 +190,9 @@ 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]`. +The version of PyPy should be specified in the format `pypy-[-v]`. The `` parameter is optional and can be skipped. The latest version will be used in this case. ``` From a1121449a217580167611672a6c61b4cf314e0f2 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Fri, 15 Jan 2021 11:20:02 +0000 Subject: [PATCH 10/11] Add on: pull_request trigger to CodeQL workflow (#180) From February 2021, in order to provide feedback on pull requests, Code Scanning workflows must be configured with both `push` and `pull_request` triggers. This is because Code Scanning compares the results from a pull request against the results for the base branch to tell you only what has changed between the two. Early in the beta period we supported displaying results on pull requests for workflows with only `push` triggers, but have discontinued support as this proved to be less robust. See https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#scanning-pull-requests for more information on how best to configure your Code Scanning workflows. --- .github/workflows/codeql-analysis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f12088901..d26264db1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,6 +2,7 @@ name: "Code scanning - action" on: push: + pull_request: schedule: - cron: '25 3 * * 5' From dc73133d4da04e56a135ae2246682783cc7c7cb6 Mon Sep 17 00:00:00 2001 From: Alena Sviridenko Date: Mon, 12 Apr 2021 20:59:38 +0300 Subject: [PATCH 11/11] Fix PyPy installation on Windows to adopt new parameters format (#201) * test for pypy new version notation * formatting * uncommented condition * test * added pypy to test matrix * test * test * restored all tests * removed logs, added multiarch support for toolcache * reduced test matrix * removed extra condition about arch --- .github/workflows/test-pypy.yml | 4 +-- __tests__/data/pypy.json | 39 +++++++++++++++++++++++++ dist/index.js | 42 ++++++++++++++++++++++----- src/find-pypy.ts | 21 ++++++++++---- src/install-pypy.ts | 50 +++++++++++++++++++++++++++++---- src/utils.ts | 2 ++ 6 files changed, 137 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test-pypy.yml b/.github/workflows/test-pypy.yml index 4041440d4..547d03419 100644 --- a/.github/workflows/test-pypy.yml +++ b/.github/workflows/test-pypy.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-18.04, ubuntu-20.04] + os: [macos-latest, windows-latest, ubuntu-18.04, ubuntu-latest] pypy: - 'pypy-2.7' - 'pypy-3.6' @@ -28,7 +28,7 @@ jobs: - 'pypy-3.7-v7.3.2' - 'pypy-3.6-v7.3.x' - 'pypy-3.7-v7.x' - - 'pypy-3.6-v7.3.3rc1' + - 'pypy-2.7-v7.3.4rc1' - 'pypy-3.7-nightly' steps: diff --git a/__tests__/data/pypy.json b/__tests__/data/pypy.json index 95e06bbb1..c8889a3b5 100644 --- a/__tests__/data/pypy.json +++ b/__tests__/data/pypy.json @@ -89,6 +89,45 @@ } ] }, + { + "pypy_version": "7.3.4rc1", + "python_version": "2.7.18", + "stable": false, + "latest_pypy": false, + "date": "2021-03-19", + "files": [ + { + "filename": "pypy2.7-v7.3.4rc1-aarch64.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "https://test.downloads.python.org/pypy/pypy2.7-v7.3.4rc1-aarch64.tar.bz2" + }, + { + "filename": "pypy2.7-v7.3.4rc1-linux32.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "https://test.downloads.python.org/pypy/pypy2.7-v7.3.4rc1-linux32.tar.bz2" + }, + { + "filename": "pypy2.7-v7.3.4rc1-linux64.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "https://test.downloads.python.org/pypy/pypy2.7-v7.3.4rc1-linux64.tar.bz2" + }, + { + "filename": "pypy2.7-v7.3.4rc1-osx64.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "https://test.downloads.python.org/pypy/pypy2.7-v7.3.4rc1-osx64.tar.bz2" + }, + { + "filename": "pypy2.7-v7.3.4rc1-win64.zip", + "arch": "x64", + "platform": "win64", + "download_url": "https://test.downloads.python.org/pypy/pypy2.7-v7.3.4rc1-win64.zip" + } + ] + }, { "pypy_version": "7.3.3rc2", "python_version": "3.7.7", diff --git a/dist/index.js b/dist/index.js index f0939428b..16cab2276 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1108,10 +1108,6 @@ function findPyPyVersion(versionSpec, architecture) { 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) { ({ @@ -1133,7 +1129,9 @@ exports.findPyPyVersion = findPyPyVersion; function findPyPyToolCache(pythonVersion, pypyVersion, architecture) { let resolvedPyPyVersion = ''; let resolvedPythonVersion = ''; - let installDir = tc.find('PyPy', pythonVersion, architecture); + let installDir = utils_1.IS_WINDOWS + ? findPyPyInstallDirForWindows(pythonVersion) + : 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. @@ -1177,6 +1175,12 @@ function parsePyPyVersion(versionSpec) { }; } exports.parsePyPyVersion = parsePyPyVersion; +function findPyPyInstallDirForWindows(pythonVersion) { + let installDir = ''; + utils_1.WINDOWS_ARCHS.forEach(architecture => (installDir = installDir || tc.find('PyPy', pythonVersion, architecture))); + return installDir; +} +exports.findPyPyInstallDirForWindows = findPyPyInstallDirForWindows; /***/ }), @@ -2327,6 +2331,8 @@ const path = __importStar(__webpack_require__(622)); const semver = __importStar(__webpack_require__(876)); exports.IS_WINDOWS = process.platform === 'win32'; exports.IS_LINUX = process.platform === 'linux'; +exports.WINDOWS_ARCHS = ['x86', 'x64']; +exports.WINDOWS_PLATFORMS = ['win32', 'win64']; const PYPY_VERSION_FILE = 'PYPY_VERSION'; /** create Symlinks for downloaded PyPy * It should be executed only for downloaded versions in runtime, because @@ -2893,7 +2899,9 @@ function findRelease(releases, pythonVersion, pypyVersion, architecture) { const isPyPyVersionSatisfied = isPyPyNightly || semver.satisfies(pypyVersionToSemantic(item.pypy_version), pypyVersion); const isArchPresent = item.files && - item.files.some(file => file.arch === architecture && file.platform === process.platform); + (utils_1.IS_WINDOWS + ? isArchPresentForWindows(item) + : isArchPresentForMacOrLinux(item, architecture, process.platform)); return isPythonVersionSatisfied && isPyPyVersionSatisfied && isArchPresent; }); if (filterReleases.length === 0) { @@ -2904,7 +2912,9 @@ function findRelease(releases, pythonVersion, pypyVersion, architecture) { 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); + const foundAsset = utils_1.IS_WINDOWS + ? findAssetForWindows(foundRelease) + : findAssetForMacOrLinux(foundRelease, architecture, process.platform); return { foundAsset, resolvedPythonVersion: foundRelease.python_version, @@ -2926,6 +2936,24 @@ function pypyVersionToSemantic(versionSpec) { return versionSpec.replace(prereleaseVersion, '$1-$2.$3'); } exports.pypyVersionToSemantic = pypyVersionToSemantic; +function isArchPresentForWindows(item) { + return item.files.some((file) => utils_1.WINDOWS_ARCHS.includes(file.arch) && + utils_1.WINDOWS_PLATFORMS.includes(file.platform)); +} +exports.isArchPresentForWindows = isArchPresentForWindows; +function isArchPresentForMacOrLinux(item, architecture, platform) { + return item.files.some((file) => file.arch === architecture && file.platform === platform); +} +exports.isArchPresentForMacOrLinux = isArchPresentForMacOrLinux; +function findAssetForWindows(releases) { + return releases.files.find((item) => utils_1.WINDOWS_ARCHS.includes(item.arch) && + utils_1.WINDOWS_PLATFORMS.includes(item.platform)); +} +exports.findAssetForWindows = findAssetForWindows; +function findAssetForMacOrLinux(releases, architecture, platform) { + return releases.files.find((item) => item.arch === architecture && item.platform === platform); +} +exports.findAssetForMacOrLinux = findAssetForMacOrLinux; /***/ }), diff --git a/src/find-pypy.ts b/src/find-pypy.ts index 700ce9ee5..eb8dfac65 100644 --- a/src/find-pypy.ts +++ b/src/find-pypy.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import * as pypyInstall from './install-pypy'; import { IS_WINDOWS, + WINDOWS_ARCHS, validateVersion, getPyPyVersionFromPath, readExactPyPyVersionFile, @@ -27,11 +28,6 @@ export async function findPyPyVersion( 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, @@ -67,7 +63,9 @@ export function findPyPyToolCache( ) { let resolvedPyPyVersion = ''; let resolvedPythonVersion = ''; - let installDir: string | null = tc.find('PyPy', pythonVersion, architecture); + let installDir: string | null = IS_WINDOWS + ? findPyPyInstallDirForWindows(pythonVersion) + : tc.find('PyPy', pythonVersion, architecture); if (installDir) { // 'tc.find' finds tool based on Python version but we also need to check @@ -129,3 +127,14 @@ export function parsePyPyVersion(versionSpec: string): IPyPyVersionSpec { pythonVersion: pythonVersion }; } + +export function findPyPyInstallDirForWindows(pythonVersion: string): string { + let installDir = ''; + + WINDOWS_ARCHS.forEach( + architecture => + (installDir = installDir || tc.find('PyPy', pythonVersion, architecture)) + ); + + return installDir; +} diff --git a/src/install-pypy.ts b/src/install-pypy.ts index 99d603000..402525ab3 100644 --- a/src/install-pypy.ts +++ b/src/install-pypy.ts @@ -8,6 +8,8 @@ import fs from 'fs'; import { IS_WINDOWS, + WINDOWS_ARCHS, + WINDOWS_PLATFORMS, IPyPyManifestRelease, createSymlinkInFolder, isNightlyKeyword, @@ -143,9 +145,9 @@ export function findRelease( semver.satisfies(pypyVersionToSemantic(item.pypy_version), pypyVersion); const isArchPresent = item.files && - item.files.some( - file => file.arch === architecture && file.platform === process.platform - ); + (IS_WINDOWS + ? isArchPresentForWindows(item) + : isArchPresentForMacOrLinux(item, architecture, process.platform)); return isPythonVersionSatisfied && isPyPyVersionSatisfied && isArchPresent; }); @@ -167,9 +169,9 @@ export function findRelease( }); const foundRelease = sortedReleases[0]; - const foundAsset = foundRelease.files.find( - item => item.arch === architecture && item.platform === process.platform - ); + const foundAsset = IS_WINDOWS + ? findAssetForWindows(foundRelease) + : findAssetForMacOrLinux(foundRelease, architecture, process.platform); return { foundAsset, @@ -191,3 +193,39 @@ export function pypyVersionToSemantic(versionSpec: string) { const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc))(\d*)/g; return versionSpec.replace(prereleaseVersion, '$1-$2.$3'); } + +export function isArchPresentForWindows(item: any) { + return item.files.some( + (file: any) => + WINDOWS_ARCHS.includes(file.arch) && + WINDOWS_PLATFORMS.includes(file.platform) + ); +} + +export function isArchPresentForMacOrLinux( + item: any, + architecture: string, + platform: string +) { + return item.files.some( + (file: any) => file.arch === architecture && file.platform === platform + ); +} + +export function findAssetForWindows(releases: any) { + return releases.files.find( + (item: any) => + WINDOWS_ARCHS.includes(item.arch) && + WINDOWS_PLATFORMS.includes(item.platform) + ); +} + +export function findAssetForMacOrLinux( + releases: any, + architecture: string, + platform: string +) { + return releases.files.find( + (item: any) => item.arch === architecture && item.platform === platform + ); +} diff --git a/src/utils.ts b/src/utils.ts index e96d5b230..c15fe3d05 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,6 +4,8 @@ import * as semver from 'semver'; export const IS_WINDOWS = process.platform === 'win32'; export const IS_LINUX = process.platform === 'linux'; +export const WINDOWS_ARCHS = ['x86', 'x64']; +export const WINDOWS_PLATFORMS = ['win32', 'win64']; const PYPY_VERSION_FILE = 'PYPY_VERSION'; export interface IPyPyManifestAsset {