From 8723b8aa5a471fe1d57b08ca8432946698b1f176 Mon Sep 17 00:00:00 2001 From: Dmitry Shibanov Date: Fri, 6 May 2022 09:50:12 +0200 Subject: [PATCH] add check-latest for pypy --- .github/workflows/test-pypy.yml | 33 +++++++++++++++ __tests__/find-pypy.test.ts | 72 ++++++++++++++++++++++++++++++--- __tests__/finder.test.ts | 10 ++--- dist/setup/index.js | 34 +++++++++++----- src/find-pypy.ts | 30 ++++++++++++-- src/find-python.ts | 5 +-- src/install-pypy.ts | 7 ++-- src/setup-python.ts | 5 ++- 8 files changed, 165 insertions(+), 31 deletions(-) diff --git a/.github/workflows/test-pypy.yml b/.github/workflows/test-pypy.yml index f6362069..8896ca09 100644 --- a/.github/workflows/test-pypy.yml +++ b/.github/workflows/test-pypy.yml @@ -58,3 +58,36 @@ jobs: EXECUTABLE=${EXECUTABLE%%-*} # remove any -* suffixe ${EXECUTABLE} --version shell: bash + + check-latest: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v3 + - name: Setup PyPy and check latest + uses: ./ + with: + python-version: 'pypy-3.7-v7.3.x' + check-latest: true + - name: PyPy and Python version + run: python --version + + - name: Run simple code + run: python -c 'import math; print(math.factorial(5))' + + - name: Assert PyPy is running + run: | + import platform + assert platform.python_implementation().lower() == "pypy" + shell: python + + - name: Assert expected binaries (or symlinks) are present + run: | + EXECUTABLE=${{ matrix.pypy }} + EXECUTABLE=${EXECUTABLE/-/} # remove the first '-' in "pypy-X.Y" -> "pypyX.Y" to match executable name + EXECUTABLE=${EXECUTABLE%%-*} # remove any -* suffixe + ${EXECUTABLE} --version + shell: bash \ No newline at end of file diff --git a/__tests__/find-pypy.test.ts b/__tests__/find-pypy.test.ts index ddf7ebcf..a72b4ba2 100644 --- a/__tests__/find-pypy.test.ts +++ b/__tests__/find-pypy.test.ts @@ -5,6 +5,7 @@ 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 core from '@actions/core'; import * as path from 'path'; import * as semver from 'semver'; @@ -13,7 +14,6 @@ import * as finder from '../src/find-pypy'; import { IPyPyManifestRelease, IS_WINDOWS, - validateVersion, getPyPyVersionFromPath } from '../src/utils'; @@ -117,6 +117,8 @@ describe('findPyPyToolCache', () => { }); describe('findPyPyVersion', () => { + let getBooleanInputSpy: jest.SpyInstance; + let infoSpy: jest.SpyInstance; let tcFind: jest.SpyInstance; let spyExtractZip: jest.SpyInstance; let spyExtractTar: jest.SpyInstance; @@ -132,6 +134,12 @@ describe('findPyPyVersion', () => { let spyChmodSync: jest.SpyInstance; beforeEach(() => { + getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput'); + getBooleanInputSpy.mockImplementation(() => false); + + infoSpy = jest.spyOn(core, 'info'); + infoSpy.mockImplementation(() => {}); + tcFind = jest.spyOn(tc, 'find'); tcFind.mockImplementation((tool: string, version: string) => { const semverRange = new semver.Range(version); @@ -193,7 +201,7 @@ describe('findPyPyVersion', () => { it('found PyPy in toolcache', async () => { await expect( - finder.findPyPyVersion('pypy-3.6-v7.3.x', architecture) + finder.findPyPyVersion('pypy-3.6-v7.3.x', architecture, false) ).resolves.toEqual({ resolvedPythonVersion: '3.6.12', resolvedPyPyVersion: '7.3.3' @@ -202,13 +210,13 @@ describe('findPyPyVersion', () => { it('throw on invalid input format', async () => { await expect( - finder.findPyPyVersion('pypy3.7-v7.3.x', architecture) + finder.findPyPyVersion('pypy3.7-v7.3.x', architecture, false) ).rejects.toThrow(); }); it('throw on invalid input format pypy3.7-7.3.x', async () => { await expect( - finder.findPyPyVersion('pypy3.7-v7.3.x', architecture) + finder.findPyPyVersion('pypy3.7-v7.3.x', architecture, false) ).rejects.toThrow(); }); @@ -220,7 +228,7 @@ describe('findPyPyVersion', () => { spyChmodSync = jest.spyOn(fs, 'chmodSync'); spyChmodSync.mockImplementation(() => undefined); await expect( - finder.findPyPyVersion('pypy-3.7-v7.3.x', architecture) + finder.findPyPyVersion('pypy-3.7-v7.3.x', architecture, false) ).resolves.toEqual({ resolvedPythonVersion: '3.7.9', resolvedPyPyVersion: '7.3.3' @@ -229,9 +237,61 @@ describe('findPyPyVersion', () => { it('throw if release is not found', async () => { await expect( - finder.findPyPyVersion('pypy-3.7-v7.5.x', architecture) + finder.findPyPyVersion('pypy-3.7-v7.5.x', architecture, false) ).rejects.toThrowError( `PyPy version 3.7 (v7.5.x) with arch ${architecture} not found` ); }); + + it('check-latest enabled version found and used from toolcache', async () => { + await expect( + finder.findPyPyVersion('pypy-3.6-v7.3.x', architecture, true) + ).resolves.toEqual({ + resolvedPythonVersion: '3.6.12', + resolvedPyPyVersion: '7.3.3' + }); + + expect(infoSpy).toHaveBeenCalledWith( + 'Resolved as PyPy 7.3.3 with Python (3.6.12)' + ); + }); + + it('check-latest enabled version 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, true) + ).resolves.toEqual({ + resolvedPythonVersion: '3.7.9', + resolvedPyPyVersion: '7.3.3' + }); + expect(infoSpy).toHaveBeenCalledWith( + 'Resolved as PyPy 7.3.3 with Python (3.7.9)' + ); + }); + + it('check-latest enabled version is not found and used from toolcache', async () => { + tcFind.mockImplementationOnce((tool: string, version: string) => { + const semverRange = new semver.Range(version); + let pypyPath = ''; + if (semver.satisfies('3.8.8', semverRange)) { + pypyPath = path.join(toolDir, 'PyPy', '3.8.8', architecture); + } + return pypyPath; + }); + await expect( + finder.findPyPyVersion('pypy-3.8-v7.3.x', architecture, true) + ).resolves.toEqual({ + resolvedPythonVersion: '3.8.8', + resolvedPyPyVersion: '7.3.3' + }); + + expect(infoSpy).toHaveBeenCalledWith( + 'Failed to resolve PyPy v7.3.x with Python (3.8) from manifest' + ); + }); }); diff --git a/__tests__/finder.test.ts b/__tests__/finder.test.ts index 10421a38..ada16c51 100644 --- a/__tests__/finder.test.ts +++ b/__tests__/finder.test.ts @@ -40,7 +40,7 @@ describe('Finder tests', () => { await io.mkdirP(pythonDir); fs.writeFileSync(`${pythonDir}.complete`, 'hello'); // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists) - await finder.useCpythonVersion('3.x', 'x64'); + await finder.useCpythonVersion('3.x', 'x64', false); }); it('Finds stable Python version if it is not installed, but exists in the manifest', async () => { @@ -60,7 +60,7 @@ describe('Finder tests', () => { fs.writeFileSync(`${pythonDir}.complete`, 'hello'); }); // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists) - await finder.useCpythonVersion('1.2.3', 'x64'); + await finder.useCpythonVersion('1.2.3', 'x64', false); }); it('Finds pre-release Python version in the manifest', async () => { @@ -85,7 +85,7 @@ describe('Finder tests', () => { fs.writeFileSync(`${pythonDir}.complete`, 'hello'); }); // This will throw if it doesn't find it in the manifest (because no such version exists) - await finder.useCpythonVersion('1.2.3-beta.2', 'x64'); + await finder.useCpythonVersion('1.2.3-beta.2', 'x64', false); }); it('Check-latest true, finds the latest version in the manifest', async () => { @@ -128,7 +128,7 @@ describe('Finder tests', () => { fs.writeFileSync(`${pythonDir}.complete`, 'hello'); // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists) - await finder.useCpythonVersion('1.2', 'x64'); + await finder.useCpythonVersion('1.2', 'x64', true); expect(infoSpy).toHaveBeenCalledWith("Resolved as '1.2.3'"); expect(infoSpy).toHaveBeenCalledWith('Version 1.2.3 was not found in the local cache'); @@ -139,7 +139,7 @@ describe('Finder tests', () => { // This will throw if it doesn't find it in the cache and in the manifest (because no such version exists) let thrown = false; try { - await finder.useCpythonVersion('3.300000', 'x64'); + await finder.useCpythonVersion('3.300000', 'x64', false); } catch { thrown = true; } diff --git a/dist/setup/index.js b/dist/setup/index.js index 2ad4c044..e961f7e7 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -5612,16 +5612,17 @@ function run() { } try { const version = core.getInput('python-version'); + const checkLatest = core.getBooleanInput('check-latest'); if (version) { let pythonVersion; const arch = core.getInput('architecture') || os.arch(); if (isPyPyVersion(version)) { - const installed = yield finderPyPy.findPyPyVersion(version, arch); + const installed = yield finderPyPy.findPyPyVersion(version, arch, checkLatest); pythonVersion = `${installed.resolvedPyPyVersion}-${installed.resolvedPythonVersion}`; core.info(`Successfully set up PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})`); } else { - const installed = yield finder.useCpythonVersion(version, arch); + const installed = yield finder.useCpythonVersion(version, arch, checkLatest); pythonVersion = installed.version; core.info(`Successfully set up ${installed.impl} (${pythonVersion})`); } @@ -9739,7 +9740,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.findAssetForMacOrLinux = exports.findAssetForWindows = exports.isArchPresentForMacOrLinux = exports.isArchPresentForWindows = exports.pypyVersionToSemantic = exports.getPyPyBinaryPath = exports.findRelease = exports.installPyPy = void 0; +exports.findAssetForMacOrLinux = exports.findAssetForWindows = exports.isArchPresentForMacOrLinux = exports.isArchPresentForWindows = exports.pypyVersionToSemantic = exports.getPyPyBinaryPath = exports.findRelease = exports.getAvailablePyPyVersions = exports.installPyPy = void 0; const path = __importStar(__webpack_require__(622)); const core = __importStar(__webpack_require__(470)); const tc = __importStar(__webpack_require__(533)); @@ -9748,10 +9749,10 @@ 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) { +function installPyPy(pypyVersion, pythonVersion, architecture, releases) { return __awaiter(this, void 0, void 0, function* () { let downloadDir; - const releases = yield getAvailablePyPyVersions(); + releases !== null && releases !== void 0 ? releases : (releases = yield getAvailablePyPyVersions()); if (!releases || releases.length === 0) { throw new Error('No release was found in PyPy version.json'); } @@ -9797,6 +9798,7 @@ function getAvailablePyPyVersions() { return response.result; }); } +exports.getAvailablePyPyVersions = getAvailablePyPyVersions; function createPyPySymlink(pypyBinaryPath, pythonVersion) { return __awaiter(this, void 0, void 0, function* () { const version = semver.coerce(pythonVersion); @@ -52248,19 +52250,34 @@ 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) { +function findPyPyVersion(versionSpec, architecture, checkLatest) { return __awaiter(this, void 0, void 0, function* () { let resolvedPyPyVersion = ''; let resolvedPythonVersion = ''; let installDir; + let releases; const pypyVersionSpec = parsePyPyVersion(versionSpec); + if (checkLatest) { + releases = yield pypyInstall.getAvailablePyPyVersions(); + if (releases && releases.length > 0) { + const releaseData = pypyInstall.findRelease(releases, pypyVersionSpec.pythonVersion, pypyVersionSpec.pypyVersion, architecture); + if (releaseData) { + core.info(`Resolved as PyPy ${releaseData.resolvedPyPyVersion} with Python (${releaseData.resolvedPythonVersion})`); + pypyVersionSpec.pythonVersion = releaseData.resolvedPythonVersion; + pypyVersionSpec.pypyVersion = releaseData.resolvedPyPyVersion; + } + else { + core.info(`Failed to resolve PyPy ${pypyVersionSpec.pypyVersion} with Python (${pypyVersionSpec.pythonVersion}) from manifest`); + } + } + } ({ installDir, resolvedPythonVersion, resolvedPyPyVersion } = findPyPyToolCache(pypyVersionSpec.pythonVersion, pypyVersionSpec.pypyVersion, architecture)); if (!installDir) { ({ installDir, resolvedPythonVersion, resolvedPyPyVersion - } = yield pypyInstall.installPyPy(pypyVersionSpec.pypyVersion, pypyVersionSpec.pythonVersion, architecture)); + } = yield pypyInstall.installPyPy(pypyVersionSpec.pypyVersion, pypyVersionSpec.pythonVersion, architecture, releases)); } const pipDir = utils_1.IS_WINDOWS ? 'Scripts' : 'bin'; const _binDir = path.join(installDir, pipDir); @@ -56880,14 +56897,13 @@ function binDir(installDir) { return path.join(installDir, 'bin'); } } -function useCpythonVersion(version, architecture) { +function useCpythonVersion(version, architecture, checkLatest) { var _a; return __awaiter(this, void 0, void 0, function* () { let manifest = null; const desugaredVersionSpec = desugarDevVersion(version); let semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec); core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`); - const checkLatest = core.getBooleanInput('check-latest'); if (checkLatest) { manifest = yield installer.getManifest(); const resolvedVersion = (_a = (yield installer.findReleaseFromManifest(semanticVersionSpec, architecture, manifest))) === null || _a === void 0 ? void 0 : _a.version; diff --git a/src/find-pypy.ts b/src/find-pypy.ts index fd273825..a4124cc8 100644 --- a/src/find-pypy.ts +++ b/src/find-pypy.ts @@ -6,7 +6,8 @@ import { validateVersion, getPyPyVersionFromPath, readExactPyPyVersionFile, - validatePythonVersionFormatForPyPy + validatePythonVersionFormatForPyPy, + IPyPyManifestRelease } from './utils'; import * as semver from 'semver'; @@ -20,14 +21,36 @@ interface IPyPyVersionSpec { export async function findPyPyVersion( versionSpec: string, - architecture: string + architecture: string, + checkLatest: boolean ): Promise<{resolvedPyPyVersion: string; resolvedPythonVersion: string}> { let resolvedPyPyVersion = ''; let resolvedPythonVersion = ''; let installDir: string | null; + let releases: IPyPyManifestRelease[] | undefined; const pypyVersionSpec = parsePyPyVersion(versionSpec); + if (checkLatest) { + releases = await pypyInstall.getAvailablePyPyVersions(); + if (releases && releases.length > 0) { + const releaseData = pypyInstall.findRelease( + releases, + pypyVersionSpec.pythonVersion, + pypyVersionSpec.pypyVersion, + architecture + ); + + if(releaseData) { + core.info(`Resolved as PyPy ${releaseData.resolvedPyPyVersion} with Python (${releaseData.resolvedPythonVersion})`); + pypyVersionSpec.pythonVersion = releaseData.resolvedPythonVersion; + pypyVersionSpec.pypyVersion = releaseData.resolvedPyPyVersion; + } else { + core.info(`Failed to resolve PyPy ${pypyVersionSpec.pypyVersion} with Python (${pypyVersionSpec.pythonVersion}) from manifest`); + } + } + } + ({installDir, resolvedPythonVersion, resolvedPyPyVersion} = findPyPyToolCache( pypyVersionSpec.pythonVersion, pypyVersionSpec.pypyVersion, @@ -42,7 +65,8 @@ export async function findPyPyVersion( } = await pypyInstall.installPyPy( pypyVersionSpec.pypyVersion, pypyVersionSpec.pythonVersion, - architecture + architecture, + releases )); } diff --git a/src/find-python.ts b/src/find-python.ts index afa49a20..76bd6a6f 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -32,15 +32,14 @@ function binDir(installDir: string): string { export async function useCpythonVersion( version: string, - architecture: string + architecture: string, + checkLatest: boolean ): Promise { let manifest: tc.IToolRelease[] | null = null; const desugaredVersionSpec = desugarDevVersion(version); let semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec); core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`); - const checkLatest = core.getBooleanInput('check-latest'); - if (checkLatest) { manifest = await installer.getManifest(); const resolvedVersion = (await installer.findReleaseFromManifest(semanticVersionSpec, architecture, manifest))?.version; diff --git a/src/install-pypy.ts b/src/install-pypy.ts index c3718b79..37e27a17 100644 --- a/src/install-pypy.ts +++ b/src/install-pypy.ts @@ -19,11 +19,12 @@ import { export async function installPyPy( pypyVersion: string, pythonVersion: string, - architecture: string + architecture: string, + releases: IPyPyManifestRelease[] | undefined ) { let downloadDir; - const releases = await getAvailablePyPyVersions(); + releases ??= await getAvailablePyPyVersions(); if (!releases || releases.length === 0) { throw new Error('No release was found in PyPy version.json'); } @@ -78,7 +79,7 @@ export async function installPyPy( return {installDir, resolvedPythonVersion, resolvedPyPyVersion}; } -async function getAvailablePyPyVersions() { +export async function getAvailablePyPyVersions() { const url = 'https://downloads.python.org/pypy/versions.json'; const http: httpm.HttpClient = new httpm.HttpClient('tool-cache'); diff --git a/src/setup-python.ts b/src/setup-python.ts index 37229c59..a68b10d8 100644 --- a/src/setup-python.ts +++ b/src/setup-python.ts @@ -34,17 +34,18 @@ async function run() { } try { const version = core.getInput('python-version'); + const checkLatest = core.getBooleanInput('check-latest'); if (version) { let pythonVersion: string; const arch: string = core.getInput('architecture') || os.arch(); if (isPyPyVersion(version)) { - const installed = await finderPyPy.findPyPyVersion(version, arch); + const installed = await finderPyPy.findPyPyVersion(version, arch, checkLatest); pythonVersion = `${installed.resolvedPyPyVersion}-${installed.resolvedPythonVersion}`; core.info( `Successfully set up PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})` ); } else { - const installed = await finder.useCpythonVersion(version, arch); + const installed = await finder.useCpythonVersion(version, arch, checkLatest); pythonVersion = installed.version; core.info(`Successfully set up ${installed.impl} (${pythonVersion})`); }