diff --git a/__tests__/data/pypy.json b/__tests__/data/pypy.json index c8889a3b5..66d153e07 100644 --- a/__tests__/data/pypy.json +++ b/__tests__/data/pypy.json @@ -1,4 +1,88 @@ [ + { + "pypy_version": "7.3.8rc2", + "python_version": "3.8.12", + "stable": false, + "latest_pypy": false, + "date": "2022-02-11", + "files": [ + { + "filename": "pypy3.8-v7.3.8rc2-linux32.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.8-v7.3.8rc2-linux32.tar.bz2" + }, + { + "filename": "pypy3.8-v7.3.8rc2-linux64.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.8-v7.3.8rc2-linux64.tar.bz2" + }, + { + "filename": "pypy3.8-v7.3.8rc2-darwin64.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "https://test.download.python.org/pypy/pypy3.8-v7.3.8rc2-darwin64.tar.bz2" + }, + { + "filename": "pypy3.8-v7.3.8rc2-s390x.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.8-v7.3.8rc2-s390x.tar.bz2" + }, + { + "filename": "pypy3.8-v7.3.8rc2-win64.zip", + "arch": "x64", + "platform": "win64", + "download_url": "https://test.download.python.org/pypy/pypy3.8-v7.3.8rc2-win64.zip" + } + ] + }, + { + "pypy_version": "7.4.0rc1", + "python_version": "3.6.12", + "stable": false, + "latest_pypy": false, + "date": "2021-11-11", + "files": [ + { + "filename": "pypy3.6-v7.4.0rc1-aarch64.tar.bz2", + "arch": "aarch64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.4.0rc1-aarch64.tar.bz2" + }, + { + "filename": "pypy3.6-v7.4.0rc1-linux32.tar.bz2", + "arch": "i686", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.4.0rc1-linux32.tar.bz2" + }, + { + "filename": "pypy3.6-v7.4.0rc1-linux64.tar.bz2", + "arch": "x64", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.4.0rc1-linux64.tar.bz2" + }, + { + "filename": "pypy3.6-v7.4.0rc1-darwin64.tar.bz2", + "arch": "x64", + "platform": "darwin", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.4.0rc1-darwin64.tar.bz2" + }, + { + "filename": "pypy3.6-v7.4.0rc1-win32.zip", + "arch": "x86", + "platform": "win32", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.4.0rc1-win32.zip" + }, + { + "filename": "pypy3.6-v7.4.0rc1-s390x.tar.bz2", + "arch": "s390x", + "platform": "linux", + "download_url": "https://test.download.python.org/pypy/pypy3.6-v7.4.0rc1-s390x.tar.bz2" + } + ] + }, { "pypy_version": "7.3.3", "python_version": "3.6.12", @@ -530,4 +614,4 @@ } ] } -] \ No newline at end of file +] diff --git a/__tests__/data/versions-manifest.json b/__tests__/data/versions-manifest.json index 2d23f98e3..083160f50 100644 --- a/__tests__/data/versions-manifest.json +++ b/__tests__/data/versions-manifest.json @@ -1,4 +1,29 @@ [ + { + "version": "1.2.4-beta.2", + "stable": false, + "release_url": "https://github.com/actions/sometool/releases/tag/1.2.4-beta.2-20200402.5", + "files": [ + { + "filename": "sometool-1.2.4-linux-x64.tar.gz", + "arch": "x64", + "platform": "linux", + "download_url": "https://github.com/actions/sometool/releases/tag/1.2.4-beta.2-20200402.5/sometool-1.2.4-linux-x64.tar.gz" + }, + { + "filename": "sometool-1.2.4-darwin-x64.tar.gz", + "arch": "x64", + "platform": "darwin", + "download_url": "https://github.com/actions/sometool/releases/tag/1.2.4-beta.2-20200402.5/sometool-1.2.4-darwin-x64.tar.gz" + }, + { + "filename": "sometool-1.2.4-win32-x64.tar.gz", + "arch": "x64", + "platform": "win32", + "download_url": "https://github.com/actions/sometool/releases/tag/1.2.4-beta.2-20200402.5/sometool-1.2.4-win32-x64.tar.gz" + } + ] + }, { "version": "1.2.3", "stable": true, @@ -25,28 +50,28 @@ ] }, { - "version": "1.2.3-beta.2", + "version": "1.1.0-beta.2", "stable": false, - "release_url": "https://github.com/actions/sometool/releases/tag/1.2.3-beta.2-20200402.5", + "release_url": "https://github.com/actions/sometool/releases/tag/1.1.0-beta.2-20200402.5", "files": [ { - "filename": "sometool-1.2.3-linux-x64.tar.gz", + "filename": "sometool-1.1.0-linux-x64.tar.gz", "arch": "x64", "platform": "linux", - "download_url": "https://github.com/actions/sometool/releases/tag/1.2.3-beta.2-20200402.5/sometool-1.2.3-linux-x64.tar.gz" + "download_url": "https://github.com/actions/sometool/releases/tag/1.1.0-beta.2-20200402.5/sometool-1.1.0-linux-x64.tar.gz" }, { - "filename": "sometool-1.2.3-darwin-x64.tar.gz", + "filename": "sometool-1.1.0-darwin-x64.tar.gz", "arch": "x64", "platform": "darwin", - "download_url": "https://github.com/actions/sometool/releases/tag/1.2.3-20200402.5/sometool-1.2.3-darwin-x64.tar.gz" + "download_url": "https://github.com/actions/sometool/releases/tag/1.1.0-beta.2-20200402.5/sometool-1.1.0-darwin-x64.tar.gz" }, { - "filename": "sometool-1.2.3-win32-x64.tar.gz", + "filename": "sometool-1.1.0-win32-x64.tar.gz", "arch": "x64", "platform": "win32", - "download_url": "https://github.com/actions/sometool/releases/tag/1.2.3-20200402.5/sometool-1.2.3-win32-x64.tar.gz" + "download_url": "https://github.com/actions/sometool/releases/tag/1.1.0-beta.2-20200402.5/sometool-1.1.0-win32-x64.tar.gz" } ] } -] \ No newline at end of file +] diff --git a/__tests__/find-pypy.test.ts b/__tests__/find-pypy.test.ts index 8cb32509f..825396253 100644 --- a/__tests__/find-pypy.test.ts +++ b/__tests__/find-pypy.test.ts @@ -252,4 +252,19 @@ describe('findPyPyVersion', () => { `PyPy version 3.7 (v7.5.x) with arch ${architecture} not found` ); }); + + it('found and install successfully, pre-release fallback', async () => { + spyCacheDir = jest.spyOn(tc, 'cacheDir'); + spyCacheDir.mockImplementation(() => + path.join(toolDir, 'PyPy', '3.8.12', architecture) + ); + spyChmodSync = jest.spyOn(fs, 'chmodSync'); + spyChmodSync.mockImplementation(() => undefined); + await expect( + finder.findPyPyVersion('pypy3.8', architecture) + ).resolves.toEqual({ + resolvedPythonVersion: '3.8.12', + resolvedPyPyVersion: '7.3.8rc2' + }); + }); }); diff --git a/__tests__/finder.test.ts b/__tests__/finder.test.ts index d48056964..2e1031df9 100644 --- a/__tests__/finder.test.ts +++ b/__tests__/finder.test.ts @@ -52,7 +52,10 @@ 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 expect(finder.useCpythonVersion('1.2.3', 'x64')).resolves.toEqual({ + impl: 'CPython', + version: '1.2.3' + }); }); it('Finds pre-release Python version in the manifest', async () => { @@ -67,14 +70,64 @@ describe('Finder tests', () => { const pythonDir: string = path.join( toolDir, 'Python', - '1.2.3-beta.2', + '1.2.4-beta.2', 'x64' ); await io.mkdirP(pythonDir); 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 expect( + finder.useCpythonVersion('1.2.4-beta.2', 'x64') + ).resolves.toEqual({ + impl: 'CPython', + version: '1.2.4-beta.2' + }); + }); + + it('Finds stable Python version if it is not installed, but exists in the manifest, skipping newer pre-release', async () => { + const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo'); + findSpy.mockImplementation(() => manifestData); + + const installSpy: jest.SpyInstance = jest.spyOn( + installer, + 'installCpythonFromRelease' + ); + installSpy.mockImplementation(async () => { + const pythonDir: string = path.join(toolDir, 'Python', '1.2.3', 'x64'); + 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 expect(finder.useCpythonVersion('1.2', 'x64')).resolves.toEqual({ + impl: 'CPython', + version: '1.2.3' + }); + }); + + it('Finds Python version if it is not installed, but exists in the manifest, pre-release fallback', async () => { + const findSpy: jest.SpyInstance = jest.spyOn(tc, 'getManifestFromRepo'); + findSpy.mockImplementation(() => manifestData); + + const installSpy: jest.SpyInstance = jest.spyOn( + installer, + 'installCpythonFromRelease' + ); + installSpy.mockImplementation(async () => { + const pythonDir: string = path.join( + toolDir, + 'Python', + '1.1.0-beta.2', + 'x64' + ); + 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 expect(finder.useCpythonVersion('1.1', 'x64')).resolves.toEqual({ + impl: 'CPython', + version: '1.1.0-beta.2' + }); }); it('Errors if Python is not installed', async () => { diff --git a/__tests__/install-pypy.test.ts b/__tests__/install-pypy.test.ts index cffc90e8f..e5a857229 100644 --- a/__tests__/install-pypy.test.ts +++ b/__tests__/install-pypy.test.ts @@ -50,12 +50,24 @@ describe('findRelease', () => { platform: process.platform, download_url: `https://test.download.python.org/pypy/pypy3.6-v7.3.3-${extensionName}` }; + const filesRC1: IPyPyManifestAsset = { + filename: `pypy3.6-v7.4.0rc1-${extensionName}`, + arch: architecture, + platform: process.platform, + download_url: `https://test.download.python.org/pypy/pypy3.6-v7.4.0rc1-${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) + installer.findRelease( + releases, + pythonVersion, + pypyVersion, + architecture, + false + ) ).toEqual(null); }); @@ -63,7 +75,13 @@ describe('findRelease', () => { const pythonVersion = '3.6'; const pypyVersion = '7.3.3'; expect( - installer.findRelease(releases, pythonVersion, pypyVersion, architecture) + installer.findRelease( + releases, + pythonVersion, + pypyVersion, + architecture, + false + ) ).toEqual({ foundAsset: files, resolvedPythonVersion: '3.6.12', @@ -75,7 +93,13 @@ describe('findRelease', () => { const pythonVersion = '3.6'; const pypyVersion = '7.x'; expect( - installer.findRelease(releases, pythonVersion, pypyVersion, architecture) + installer.findRelease( + releases, + pythonVersion, + pypyVersion, + architecture, + false + ) ).toEqual({ foundAsset: files, resolvedPythonVersion: '3.6.12', @@ -87,7 +111,13 @@ describe('findRelease', () => { const pythonVersion = '3.7'; const pypyVersion = installer.pypyVersionToSemantic('7.3.3rc2'); expect( - installer.findRelease(releases, pythonVersion, pypyVersion, architecture) + installer.findRelease( + releases, + pythonVersion, + pypyVersion, + architecture, + false + ) ).toEqual({ foundAsset: { filename: `test${extension}`, @@ -104,7 +134,13 @@ describe('findRelease', () => { const pythonVersion = '3.6'; const pypyVersion = 'x'; expect( - installer.findRelease(releases, pythonVersion, pypyVersion, architecture) + installer.findRelease( + releases, + pythonVersion, + pypyVersion, + architecture, + false + ) ).toEqual({ foundAsset: files, resolvedPythonVersion: '3.6.12', @@ -112,12 +148,45 @@ describe('findRelease', () => { }); }); + it('Python version and PyPy version matches semver (pre-release)', () => { + const pythonVersion = '3.6'; + const pypyVersion = '7.4.x'; + expect( + installer.findRelease( + releases, + pythonVersion, + pypyVersion, + architecture, + false + ) + ).toBeNull(); + expect( + installer.findRelease( + releases, + pythonVersion, + pypyVersion, + architecture, + true + ) + ).toEqual({ + foundAsset: filesRC1, + resolvedPythonVersion: '3.6.12', + resolvedPyPyVersion: '7.4.0rc1' + }); + }); + 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) + installer.findRelease( + releases, + pythonVersion, + pypyVersion, + architecture, + false + ) ).toEqual({ foundAsset: { filename: filename, @@ -214,7 +283,7 @@ describe('installPyPy', () => { spyChmodSync.mockImplementation(() => undefined); await expect( - installer.installPyPy('7.3.x', '3.6.12', architecture) + installer.installPyPy('7.x', '3.6.12', architecture) ).resolves.toEqual({ installDir: path.join(toolDir, 'PyPy', '3.6.12', architecture), resolvedPythonVersion: '3.6.12', @@ -227,4 +296,28 @@ describe('installPyPy', () => { expect(spyCacheDir).toHaveBeenCalled(); expect(spyExec).toHaveBeenCalled(); }); + + it('found and install PyPy, pre-release fallback', 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.4.x', '3.6.12', architecture) + ).resolves.toEqual({ + installDir: path.join(toolDir, 'PyPy', '3.6.12', architecture), + resolvedPythonVersion: '3.6.12', + resolvedPyPyVersion: '7.4.0rc1' + }); + + expect(spyHttpClient).toHaveBeenCalled(); + expect(spyDownloadTool).toHaveBeenCalled(); + expect(spyExistsSync).toHaveBeenCalled(); + expect(spyCacheDir).toHaveBeenCalled(); + expect(spyExec).toHaveBeenCalled(); + }); }); diff --git a/dist/setup/index.js b/dist/setup/index.js index 0d98b66f4..d8bf18a16 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -64198,10 +64198,15 @@ function versionFromPath(installDir) { * Python's prelease versions look like `3.7.0b2`. * This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-b2`. * If the version spec contains prerelease versions, we need to convert them to the semantic version equivalent. + * + * For easier use of the action, we also map 'x.y' to allow pre-release before 'x.y.0' release */ function pythonVersionToSemantic(versionSpec) { const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)/g; - return versionSpec.replace(prereleaseVersion, '$1-$2'); + const majorMinor = /^(\d+)\.(\d+)$/; + return versionSpec + .replace(prereleaseVersion, '$1-$2') + .replace(majorMinor, '~$1.$2.0-0'); } exports.pythonVersionToSemantic = pythonVersionToSemantic; @@ -64246,6 +64251,7 @@ var __importDefault = (this && this.__importDefault) || function (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; +const os = __importStar(__nccwpck_require__(2037)); const path = __importStar(__nccwpck_require__(1017)); const core = __importStar(__nccwpck_require__(2186)); const tc = __importStar(__nccwpck_require__(7784)); @@ -64261,7 +64267,15 @@ function installPyPy(pypyVersion, pythonVersion, architecture) { if (!releases || releases.length === 0) { throw new Error('No release was found in PyPy version.json'); } - const releaseData = findRelease(releases, pythonVersion, pypyVersion, architecture); + let releaseData = findRelease(releases, pythonVersion, pypyVersion, architecture, false); + if (!releaseData || !releaseData.foundAsset) { + // check for pre-release + core.info([ + `Stable PyPy version ${pythonVersion} (${pypyVersion}) with arch ${architecture} not found`, + `Trying pre-release versions` + ].join(os.EOL)); + releaseData = findRelease(releases, pythonVersion, pypyVersion, architecture, true); + } if (!releaseData || !releaseData.foundAsset) { throw new Error(`PyPy version ${pythonVersion} (${pypyVersion}) with arch ${architecture} not found`); } @@ -64325,12 +64339,13 @@ function installPip(pythonLocation) { yield exec.exec(`${pythonLocation}/python -m pip install --ignore-installed pip`); }); } -function findRelease(releases, pythonVersion, pypyVersion, architecture) { +function findRelease(releases, pythonVersion, pypyVersion, architecture, includePrerelease) { + const options = { includePrerelease: includePrerelease }; 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); + semver.satisfies(pypyVersionToSemantic(item.pypy_version), pypyVersion, options); const isArchPresent = item.files && (utils_1.IS_WINDOWS ? isArchPresentForWindows(item) diff --git a/package-lock.json b/package-lock.json index 5354897b3..d009e435c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "setup-python", - "version": "3.1.1", + "version": "4.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "setup-python", - "version": "3.1.1", + "version": "4.0.0", "license": "MIT", "dependencies": { "@actions/cache": "^2.0.2", diff --git a/src/find-python.ts b/src/find-python.ts index f1dad3a9c..c48a03e1f 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -140,8 +140,13 @@ interface InstalledVersion { * Python's prelease versions look like `3.7.0b2`. * This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-b2`. * If the version spec contains prerelease versions, we need to convert them to the semantic version equivalent. + * + * For easier use of the action, we also map 'x.y' to allow pre-release before 'x.y.0' release */ export function pythonVersionToSemantic(versionSpec: string) { const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)/g; - return versionSpec.replace(prereleaseVersion, '$1-$2'); + const majorMinor = /^(\d+)\.(\d+)$/; + return versionSpec + .replace(prereleaseVersion, '$1-$2') + .replace(majorMinor, '~$1.$2.0-0'); } diff --git a/src/install-pypy.ts b/src/install-pypy.ts index c3718b792..40eb8dc18 100644 --- a/src/install-pypy.ts +++ b/src/install-pypy.ts @@ -1,3 +1,4 @@ +import * as os from 'os'; import * as path from 'path'; import * as core from '@actions/core'; import * as tc from '@actions/tool-cache'; @@ -28,13 +29,31 @@ export async function installPyPy( throw new Error('No release was found in PyPy version.json'); } - const releaseData = findRelease( + let releaseData = findRelease( releases, pythonVersion, pypyVersion, - architecture + architecture, + false ); + if (!releaseData || !releaseData.foundAsset) { + // check for pre-release + core.info( + [ + `Stable PyPy version ${pythonVersion} (${pypyVersion}) with arch ${architecture} not found`, + `Trying pre-release versions` + ].join(os.EOL) + ); + releaseData = findRelease( + releases, + pythonVersion, + pypyVersion, + architecture, + true + ); + } + if (!releaseData || !releaseData.foundAsset) { throw new Error( `PyPy version ${pythonVersion} (${pypyVersion}) with arch ${architecture} not found` @@ -140,8 +159,10 @@ export function findRelease( releases: IPyPyManifestRelease[], pythonVersion: string, pypyVersion: string, - architecture: string + architecture: string, + includePrerelease: boolean ) { + const options = {includePrerelease: includePrerelease}; const filterReleases = releases.filter(item => { const isPythonVersionSatisfied = semver.satisfies( semver.coerce(item.python_version)!, @@ -151,7 +172,11 @@ export function findRelease( isNightlyKeyword(pypyVersion) && isNightlyKeyword(item.pypy_version); const isPyPyVersionSatisfied = isPyPyNightly || - semver.satisfies(pypyVersionToSemantic(item.pypy_version), pypyVersion); + semver.satisfies( + pypyVersionToSemantic(item.pypy_version), + pypyVersion, + options + ); const isArchPresent = item.files && (IS_WINDOWS