Compare commits

..

12 Commits

Author SHA1 Message Date
0ae58361cd Add example of GraalPy to docs (#773) 2023-12-05 11:40:46 +01:00
b64ffcaf5b update actions/checkout to v4 (#761) 2023-11-08 11:16:06 +01:00
8d2896179a Examples now use checkout@v4 (#738) 2023-11-08 10:25:26 +01:00
7bc6abb01e advanced-usage.md: Encourage the use actions/checkout@v4 (#729) 2023-11-08 09:52:55 +01:00
e8111cec9d Bump @babel/traverse from 7.9.0 to 7.23.2 (#743) 2023-10-26 13:33:26 +02:00
a00ea43da6 add fix for graalpy ci (#741) 2023-10-16 11:57:32 +02:00
8635b1ccc5 Change deprecation comment to past tense (#723) 2023-10-16 11:19:29 +02:00
f6cc428f53 Use non-deprecated versions in examples (#724) 2023-10-16 11:01:43 +02:00
5f2af211d6 Add GraalPy support (#694)
* Add support for graalpy

* add graalpy test workflow

* format, lint and build

* symlink graalpy binaries names

* fix macos names for graalpy

* Don't attempt to update pip for graalpy

* Remove test schedule

* Extract common getBinaryDirectory function for PyPy and GraalPy

* Clean up and format

* Pass GitHub token to GraalPy queries

* Utilize pagination when querying GraalPy GitHub releases

* Build

* Fix lint errors

* Deal with possible multiple artifacts for a single releases

* Skip few GraalPy tests on windows - we don't have a windows release yet

* Fix GraalPy test on Mac OS

* Build

* Skip one more GraalPy test on windows

---------

Co-authored-by: Michael Simacek <michael.simacek@oracle.com>
2023-10-10 14:59:54 +02:00
3467d92d48 Trim python version to get rid of '\n' (#628) 2023-10-06 12:34:33 +02:00
65d7f2d534 Add range validation for toml files (#726) 2023-09-07 15:45:09 +02:00
f97b83114c Bump word-wrap from 1.2.3 to 1.2.4 (#702)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-04 09:55:40 +02:00
22 changed files with 10204 additions and 5023 deletions

View File

@ -23,7 +23,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', 'pypy-3.7-v7.x']
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Python
uses: ./
with:
@ -41,7 +41,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', 'pypy-3.9-v7.x']
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Python
id: cache-pipenv
uses: ./
@ -77,7 +77,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', 'pypy-3.8']
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install poetry
run: pipx install poetry
- name: Init pyproject.toml
@ -99,7 +99,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', 'pypy-3.7-v7.x']
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Python
uses: ./
with:
@ -118,7 +118,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', 'pypy-3.9-v7.x']
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Python
id: cache-pipenv
uses: ./

View File

@ -19,7 +19,7 @@ jobs:
operating-system: [ubuntu-20.04, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run with setup-python 3.5
uses: ./
@ -93,10 +93,3 @@ jobs:
python-version: '<3.11'
- name: Verify <3.11
run: python __tests__/verify-python.py 3.10
- name: Run with setup-python >3.8
uses: ./
with:
python-version: '>3.8'
- name: Verify >3.8
run: python __tests__/verify-python.py 3.11

116
.github/workflows/test-graalpy.yml vendored Normal file
View File

@ -0,0 +1,116 @@
name: Validate GraalPy e2e
on:
push:
branches:
- main
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
jobs:
setup-graalpy:
name: Setup GraalPy ${{ matrix.graalpy }} ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-20.04, ubuntu-latest]
graalpy:
- 'graalpy-23.0'
- 'graalpy-22.3'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: setup-python ${{ matrix.graalpy }}
id: setup-python
uses: ./
with:
python-version: ${{ matrix.graalpy }}
- name: Check python-path
run: ./__tests__/check-python-path.sh '${{ steps.setup-python.outputs.python-path }}'
shell: bash
- name: GraalPy and Python version
run: python --version
- name: Run simple code
run: python -c 'import math; print(math.factorial(5))'
- name: Assert GraalPy is running
run: |
import platform
assert platform.python_implementation().lower() == "graalvm"
shell: python
- name: Assert expected binaries (or symlinks) are present
run: |
EXECUTABLE=${{ matrix.graalpy }}
EXECUTABLE=${EXECUTABLE/graalpy-/graalpy} # remove the first '-' in "graalpy-X.Y" -> "graalpyX.Y" to match executable name
EXECUTABLE=${EXECUTABLE%%-*} # remove any -* suffixe
${EXECUTABLE} --version
shell: bash
setup-graalpy-noenv:
name: Setup GraalPy ${{ matrix.graalpy }} ${{ matrix.os }} (noenv)
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-20.04, ubuntu-latest]
graalpy: ['graalpy23.0', 'graalpy22.3']
steps:
- name: Checkout
uses: actions/checkout@v4
- name: setup-python ${{ matrix.graalpy }}
id: setup-python
uses: ./
with:
python-version: ${{ matrix.graalpy }}
update-environment: false
- name: GraalPy and Python version
run: ${{ steps.setup-python.outputs.python-path }} --version
- name: Run simple code
run: ${{ steps.setup-python.outputs.python-path }} -c 'import math; print(math.factorial(5))'
check-latest:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- name: Setup GraalPy and check latest
uses: ./
id: graalpy
with:
python-version: 'graalpy-23.x'
check-latest: true
- name: GraalPy and Python version
run: python --version
- name: Run simple code
run: python -c 'import math; print(math.factorial(5))'
- name: Assert GraalPy is running
run: |
import platform
assert platform.python_implementation().lower() == "graalvm"
shell: python
- name: Assert expected binaries (or symlinks) are present
run: |
EXECUTABLE='${{ steps.graalpy.outputs.python-version }}'
EXECUTABLE="${EXECUTABLE%.*}"
${EXECUTABLE} --version
shell: bash

View File

@ -35,7 +35,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: setup-python ${{ matrix.pypy }}
id: setup-python
@ -78,7 +78,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: setup-python ${{ matrix.pypy }}
id: setup-python
@ -100,7 +100,7 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup PyPy and check latest
uses: ./
with:
@ -133,7 +133,7 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup PyPy and check latest
uses: ./
with:

View File

@ -33,7 +33,7 @@ jobs:
python: 3.8.15
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: setup-python ${{ matrix.python }}
id: setup-python
@ -77,7 +77,7 @@ jobs:
python: 3.8.15
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: build-version-file ${{ matrix.python }}
run: echo ${{ matrix.python }} > .python-version
@ -124,7 +124,7 @@ jobs:
python: 3.8.15
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: build-version-file ${{ matrix.python }}
run: echo ${{ matrix.python }} > .python-version
@ -157,7 +157,7 @@ jobs:
fail-fast: false
matrix:
os: [macos-latest, windows-latest, ubuntu-20.04, ubuntu-22.04]
python: [3.5.4, 3.6.7, 3.7.5, 3.8.15, 3.9.13]
python: [3.5.4, 3.6.7, 3.7.5, 3.8.15, 3.9.13, '==3.10.10']
exclude:
- os: ubuntu-22.04
python: 3.5.4
@ -169,7 +169,7 @@ jobs:
python: 3.8.15
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: build-version-file ${{ matrix.python }}
run: |
@ -190,7 +190,7 @@ jobs:
- name: Validate version
run: |
$pythonVersion = (python --version)
if ("Python ${{ matrix.python }}" -ne "$pythonVersion"){
if ("Python ${{ matrix.python }}".replace("==", "") -ne "$pythonVersion"){
Write-Host "The current version is $pythonVersion; expected version is ${{ matrix.python }}"
exit 1
}
@ -219,7 +219,7 @@ jobs:
python: 3.8.15
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: build-version-file ${{ matrix.python }}
run: |
@ -259,7 +259,7 @@ jobs:
os: [macos-latest, windows-latest, ubuntu-20.04]
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: setup-python 3.9.0-beta.4
id: setup-python
@ -293,7 +293,7 @@ jobs:
os: [macos-latest, windows-latest, ubuntu-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: setup-python 3.9-dev
id: setup-python
@ -321,7 +321,7 @@ jobs:
os: [macos-latest, windows-latest, ubuntu-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: setup-python 3.12
id: setup-python
@ -351,7 +351,7 @@ jobs:
python: ['3.7', '3.8', '3.9', '3.10']
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: setup-python ${{ matrix.python }}
id: setup-python
@ -374,7 +374,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Python and check latest
uses: ./
with:
@ -397,7 +397,7 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Python and check latest
uses: ./
with:

View File

@ -1,9 +1,9 @@
---
name: semver
version: 6.3.0
version: 6.3.1
type: npm
summary: The semantic version parser used by npm.
homepage: https://github.com/npm/node-semver#readme
homepage:
license: isc
licenses:
- sources: LICENSE

View File

@ -18,7 +18,7 @@ See [action.yml](action.yml)
**Python**
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.10'
@ -28,12 +28,23 @@ steps:
**PyPy**
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 'pypy3.9'
- run: python my_script.py
```
**GraalPy**
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 'graalpy-22.3'
- run: python my_script.py
```
The `python-version` input is optional. If not supplied, the action will try to resolve the version from the default `.python-version` file. If the `.python-version` file doesn't exist Python or PyPy version from the PATH will be used. The default version of Python or PyPy in PATH varies between runners and can be changed unexpectedly so we recommend always setting Python version explicitly using the `python-version` or `python-version-file` inputs.
The action will first check the local [tool cache](docs/advanced-usage.md#hosted-tool-cache) for a [semver](https://github.com/npm/node-semver#versions) match. If unable to find a specific version in the tool cache, the action will attempt to download a version of Python from [GitHub Releases](https://github.com/actions/python-versions/releases) and for PyPy from the official [PyPy's dist](https://downloads.python.org/pypy/).
@ -62,7 +73,7 @@ The action defaults to searching for a dependency file (`requirements.txt` or `p
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.9'
@ -82,7 +93,7 @@ See examples of using `cache` and `cache-dependency-path` for `pipenv` and `poet
- [Check latest version](docs/advanced-usage.md#check-latest-version)
- [Caching packages](docs/advanced-usage.md#caching-packages)
- [Outputs and environment variables](docs/advanced-usage.md#outputs-and-environment-variables)
- [Available versions of Python and PyPy](docs/advanced-usage.md#available-versions-of-python-and-pypy)
- [Available versions of Python and PyPy](advanced-usage.md#available-versions-of-python-pypy-and-graalpy)
- [Hosted tool cache](docs/advanced-usage.md#hosted-tool-cache)
- [Using `setup-python` with a self-hosted runner](docs/advanced-usage.md#using-setup-python-with-a-self-hosted-runner)
- [Using `setup-python` on GHES](docs/advanced-usage.md#using-setup-python-on-ghes)

5798
__tests__/data/graalpy.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,378 @@
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 core from '@actions/core';
import * as path from 'path';
import * as semver from 'semver';
import * as finder from '../src/find-graalpy';
import {IGraalPyManifestRelease, IS_WINDOWS} from '../src/utils';
import manifestData from './data/graalpy.json';
const architecture = 'x64';
const toolDir = path.join(__dirname, 'runner', 'tools');
const tempDir = path.join(__dirname, 'runner', 'temp');
/* GraalPy doesn't have a windows release yet */
const describeSkipOnWindows = IS_WINDOWS ? describe.skip : describe;
describe('parseGraalPyVersion', () => {
it.each([
['graalpy-23', '23'],
['graalpy-23.0', '23.0'],
['graalpy23.0', '23.0']
])('%s -> %s', (input, expected) => {
expect(finder.parseGraalPyVersion(input)).toEqual(expected);
});
it.each(['', 'graalpy-', 'graalpy', 'p', 'notgraalpy-'])(
'throw on invalid input "%s"',
input => {
expect(() => finder.parseGraalPyVersion(input)).toThrow(
"Invalid 'version' property for GraalPy. GraalPy version should be specified as 'graalpy<python-version>' or 'graalpy-<python-version>'. See README for examples and documentation."
);
}
);
});
describe('findGraalPyToolCache', () => {
const actualGraalPyVersion = '23.0.0';
const graalpyPath = path.join('GraalPy', actualGraalPyVersion, architecture);
let tcFind: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let addPathSpy: jest.SpyInstance;
let exportVariableSpy: jest.SpyInstance;
let setOutputSpy: jest.SpyInstance;
beforeEach(() => {
tcFind = jest.spyOn(tc, 'find');
tcFind.mockImplementation((toolname: string, pythonVersion: string) => {
const semverVersion = new semver.Range(pythonVersion);
return semver.satisfies(actualGraalPyVersion, semverVersion)
? graalpyPath
: '';
});
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => null);
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
addPathSpy = jest.spyOn(core, 'addPath');
addPathSpy.mockImplementation(() => null);
exportVariableSpy = jest.spyOn(core, 'exportVariable');
exportVariableSpy.mockImplementation(() => null);
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy.mockImplementation(() => null);
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
jest.restoreAllMocks();
});
it('GraalPy exists on the path and versions are satisfied', () => {
expect(finder.findGraalPyToolCache('23.0.0', architecture)).toEqual({
installDir: graalpyPath,
resolvedGraalPyVersion: actualGraalPyVersion
});
});
it('GraalPy exists on the path and versions are satisfied with semver', () => {
expect(finder.findGraalPyToolCache('23.0', architecture)).toEqual({
installDir: graalpyPath,
resolvedGraalPyVersion: actualGraalPyVersion
});
});
it("GraalPy exists on the path, but version doesn't match", () => {
expect(finder.findGraalPyToolCache('22.3', architecture)).toEqual({
installDir: '',
resolvedGraalPyVersion: ''
});
});
});
describeSkipOnWindows('findGraalPyVersion', () => {
let getBooleanInputSpy: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
let addPathSpy: jest.SpyInstance;
let exportVariableSpy: jest.SpyInstance;
let setOutputSpy: jest.SpyInstance;
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 spyFsReadDir: jest.SpyInstance;
let spyCacheDir: jest.SpyInstance;
let spyChmodSync: jest.SpyInstance;
let spyCoreAddPath: jest.SpyInstance;
let spyCoreExportVariable: jest.SpyInstance;
const env = process.env;
beforeEach(() => {
getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
getBooleanInputSpy.mockImplementation(() => false);
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
addPathSpy = jest.spyOn(core, 'addPath');
addPathSpy.mockImplementation(() => null);
exportVariableSpy = jest.spyOn(core, 'exportVariable');
exportVariableSpy.mockImplementation(() => null);
setOutputSpy = jest.spyOn(core, 'setOutput');
setOutputSpy.mockImplementation(() => null);
jest.resetModules();
process.env = {...env};
tcFind = jest.spyOn(tc, 'find');
tcFind.mockImplementation((tool: string, version: string) => {
const semverRange = new semver.Range(version);
let graalpyPath = '';
if (semver.satisfies('23.0.0', semverRange)) {
graalpyPath = path.join(toolDir, 'GraalPy', '23.0.0', architecture);
}
return graalpyPath;
});
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool.mockImplementation(() => path.join(tempDir, 'GraalPy'));
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) => ['GraalPyTest']);
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient.mockImplementation(
async (): Promise<ifm.ITypedResponse<IGraalPyManifestRelease[]>> => {
const result = JSON.stringify(manifestData);
return {
statusCode: 200,
headers: {},
result: JSON.parse(result) as IGraalPyManifestRelease[]
};
}
);
spyExec = jest.spyOn(exec, 'exec');
spyExec.mockImplementation(() => undefined);
spySymlinkSync = jest.spyOn(fs, 'symlinkSync');
spySymlinkSync.mockImplementation(() => undefined);
spyExistsSync = jest.spyOn(fs, 'existsSync');
spyExistsSync.mockReturnValue(true);
spyCoreAddPath = jest.spyOn(core, 'addPath');
spyCoreExportVariable = jest.spyOn(core, 'exportVariable');
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
jest.restoreAllMocks();
process.env = env;
});
it('found GraalPy in toolcache', async () => {
await expect(
finder.findGraalPyVersion(
'graalpy-23.0',
architecture,
true,
false,
false
)
).resolves.toEqual('23.0.0');
expect(spyCoreAddPath).toHaveBeenCalled();
expect(spyCoreExportVariable).toHaveBeenCalledWith(
'pythonLocation',
expect.anything()
);
expect(spyCoreExportVariable).toHaveBeenCalledWith(
'PKG_CONFIG_PATH',
expect.anything()
);
});
it('throw on invalid input format', async () => {
await expect(
finder.findGraalPyVersion('graalpy-x23', architecture, true, false, false)
).rejects.toThrow();
});
it('found and install successfully', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.0.0', architecture)
);
spyChmodSync = jest.spyOn(fs, 'chmodSync');
spyChmodSync.mockImplementation(() => undefined);
await expect(
finder.findGraalPyVersion(
'graalpy-23.0.0',
architecture,
true,
false,
false
)
).resolves.toEqual('23.0.0');
expect(spyCoreAddPath).toHaveBeenCalled();
expect(spyCoreExportVariable).toHaveBeenCalledWith(
'pythonLocation',
expect.anything()
);
expect(spyCoreExportVariable).toHaveBeenCalledWith(
'PKG_CONFIG_PATH',
expect.anything()
);
});
it('found and install successfully without environment update', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.0.0', architecture)
);
spyChmodSync = jest.spyOn(fs, 'chmodSync');
spyChmodSync.mockImplementation(() => undefined);
await expect(
finder.findGraalPyVersion(
'graalpy-23.0.0',
architecture,
false,
false,
false
)
).resolves.toEqual('23.0.0');
expect(spyCoreAddPath).not.toHaveBeenCalled();
expect(spyCoreExportVariable).not.toHaveBeenCalled();
});
it('throw if release is not found', async () => {
await expect(
finder.findGraalPyVersion(
'graalpy-19.0.0',
architecture,
true,
false,
false
)
).rejects.toThrow(
`GraalPy version 19.0.0 with arch ${architecture} not found`
);
});
it('check-latest enabled version found and used from toolcache', async () => {
await expect(
finder.findGraalPyVersion(
'graalpy-23.0.0',
architecture,
false,
true,
false
)
).resolves.toEqual('23.0.0');
expect(infoSpy).toHaveBeenCalledWith('Resolved as GraalPy 23.0.0');
});
it('check-latest enabled version found and install successfully', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.0.0', architecture)
);
spyChmodSync = jest.spyOn(fs, 'chmodSync');
spyChmodSync.mockImplementation(() => undefined);
await expect(
finder.findGraalPyVersion(
'graalpy-23.0.0',
architecture,
false,
true,
false
)
).resolves.toEqual('23.0.0');
expect(infoSpy).toHaveBeenCalledWith('Resolved as GraalPy 23.0.0');
});
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 graalpyPath = '';
if (semver.satisfies('22.3.4', semverRange)) {
graalpyPath = path.join(toolDir, 'GraalPy', '22.3.4', architecture);
}
return graalpyPath;
});
await expect(
finder.findGraalPyVersion(
'graalpy-22.3.4',
architecture,
false,
true,
false
)
).resolves.toEqual('22.3.4');
expect(infoSpy).toHaveBeenCalledWith(
'Failed to resolve GraalPy 22.3.4 from manifest'
);
});
it('found and install successfully, pre-release fallback', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.1', architecture)
);
spyChmodSync = jest.spyOn(fs, 'chmodSync');
spyChmodSync.mockImplementation(() => undefined);
await expect(
finder.findGraalPyVersion(
'graalpy23.1',
architecture,
false,
false,
false
)
).rejects.toThrow();
await expect(
finder.findGraalPyVersion('graalpy23.1', architecture, false, false, true)
).resolves.toEqual('23.1.0-a.1');
});
});

View File

@ -0,0 +1,256 @@
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 core from '@actions/core';
import * as path from 'path';
import * as installer from '../src/install-graalpy';
import {
IGraalPyManifestRelease,
IGraalPyManifestAsset,
IS_WINDOWS
} from '../src/utils';
import manifestData from './data/graalpy.json';
const architecture = 'x64';
const toolDir = path.join(__dirname, 'runner', 'tools');
const tempDir = path.join(__dirname, 'runner', 'temp');
/* GraalPy doesn't have a windows release yet */
const describeSkipOnWindows = IS_WINDOWS ? describe.skip : describe;
describe('graalpyVersionToSemantic', () => {
it.each([
['23.0.0a1', '23.0.0a1'],
['23.0.0', '23.0.0'],
['23.0.x', '23.0.x'],
['23.x', '23.x']
])('%s -> %s', (input, expected) => {
expect(installer.graalPyTagToVersion(input)).toEqual(expected);
});
});
describeSkipOnWindows('findRelease', () => {
const result = JSON.stringify(manifestData);
const releases = JSON.parse(result) as IGraalPyManifestRelease[];
const extension = 'tar.gz';
const arch = installer.toGraalPyArchitecture(architecture);
const platform = installer.toGraalPyPlatform(process.platform);
const extensionName = `${platform}-${arch}.${extension}`;
const files: IGraalPyManifestAsset = {
name: `graalpython-23.0.0-${extensionName}`,
browser_download_url: `https://github.com/oracle/graalpython/releases/download/graal-23.0.0/graalpython-23.0.0-${extensionName}`
};
const filesRC1: IGraalPyManifestAsset = {
name: `graalpython-23.1.0a1-${extensionName}`,
browser_download_url: `https://github.com/oracle/graalpython/releases/download/graal-23.1.0a1/graalpython-23.1.0a1-${extensionName}`
};
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: jest.SpyInstance;
beforeEach(() => {
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
});
it("GraalPy version doesn't match", () => {
const graalpyVersion = '12.0.0';
expect(
installer.findRelease(releases, graalpyVersion, architecture, false)
).toEqual(null);
});
it('GraalPy version matches', () => {
const graalpyVersion = '23.0.0';
expect(
installer.findRelease(releases, graalpyVersion, architecture, false)
).toMatchObject({
foundAsset: files,
resolvedGraalPyVersion: graalpyVersion
});
});
it('Preview version of GraalPy is found', () => {
const graalpyVersion = installer.graalPyTagToVersion('vm-23.1.0a1');
expect(
installer.findRelease(releases, graalpyVersion, architecture, false)
).toMatchObject({
foundAsset: {
name: `graalpython-23.1.0a1-${extensionName}`,
browser_download_url: `https://github.com/oracle/graalpython/releases/download/graal-23.1.0a1/graalpython-23.1.0a1-${extensionName}`
},
resolvedGraalPyVersion: '23.1.0-a.1'
});
});
it('Latest GraalPy is found', () => {
const graalpyVersion = 'x';
expect(
installer.findRelease(releases, graalpyVersion, architecture, false)
).toMatchObject({
foundAsset: files,
resolvedGraalPyVersion: '23.0.0'
});
});
it('GraalPy version matches semver (pre-release)', () => {
const graalpyVersion = '23.1.x';
expect(
installer.findRelease(releases, graalpyVersion, architecture, false)
).toBeNull();
expect(
installer.findRelease(releases, graalpyVersion, architecture, true)
).toMatchObject({
foundAsset: filesRC1,
resolvedGraalPyVersion: '23.1.0-a.1'
});
});
});
describeSkipOnWindows('installGraalPy', () => {
let tcFind: jest.SpyInstance;
let warningSpy: jest.SpyInstance;
let debugSpy: jest.SpyInstance;
let infoSpy: 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('GraalPy', '3.6.12', architecture)
);
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool.mockImplementation(() => path.join(tempDir, 'GraalPy'));
spyExtractZip = jest.spyOn(tc, 'extractZip');
spyExtractZip.mockImplementation(() => tempDir);
spyExtractTar = jest.spyOn(tc, 'extractTar');
spyExtractTar.mockImplementation(() => tempDir);
infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(() => {});
warningSpy = jest.spyOn(core, 'warning');
warningSpy.mockImplementation(() => null);
debugSpy = jest.spyOn(core, 'debug');
debugSpy.mockImplementation(() => null);
spyFsReadDir = jest.spyOn(fs, 'readdirSync');
spyFsReadDir.mockImplementation(() => ['GraalPyTest']);
spyFsWriteFile = jest.spyOn(fs, 'writeFileSync');
spyFsWriteFile.mockImplementation(() => undefined);
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
spyHttpClient.mockImplementation(
async (): Promise<ifm.ITypedResponse<IGraalPyManifestRelease[]>> => {
const result = JSON.stringify(manifestData);
return {
statusCode: 200,
headers: {},
result: JSON.parse(result) as IGraalPyManifestRelease[]
};
}
);
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.installGraalPy('7.3.3', architecture, false, undefined)
).rejects.toThrow(
`GraalPy version 7.3.3 with arch ${architecture} not found`
);
expect(spyHttpClient).toHaveBeenCalled();
expect(spyDownloadTool).not.toHaveBeenCalled();
expect(spyExec).not.toHaveBeenCalled();
});
it('found and install GraalPy', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '21.3.0', architecture)
);
spyChmodSync = jest.spyOn(fs, 'chmodSync');
spyChmodSync.mockImplementation(() => undefined);
await expect(
installer.installGraalPy('21.x', architecture, false, undefined)
).resolves.toEqual({
installDir: path.join(toolDir, 'GraalPy', '21.3.0', architecture),
resolvedGraalPyVersion: '21.3.0'
});
expect(spyHttpClient).toHaveBeenCalled();
expect(spyDownloadTool).toHaveBeenCalled();
expect(spyCacheDir).toHaveBeenCalled();
expect(spyExec).toHaveBeenCalled();
});
it('found and install GraalPy, pre-release fallback', async () => {
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(() =>
path.join(toolDir, 'GraalPy', '23.1.0', architecture)
);
spyChmodSync = jest.spyOn(fs, 'chmodSync');
spyChmodSync.mockImplementation(() => undefined);
await expect(
installer.installGraalPy('23.1.x', architecture, false, undefined)
).rejects.toThrow();
await expect(
installer.installGraalPy('23.1.x', architecture, true, undefined)
).resolves.toEqual({
installDir: path.join(toolDir, 'GraalPy', '23.1.0', architecture),
resolvedGraalPyVersion: '23.1.0-a.1'
});
expect(spyHttpClient).toHaveBeenCalled();
expect(spyDownloadTool).toHaveBeenCalled();
expect(spyCacheDir).toHaveBeenCalled();
expect(spyExec).toHaveBeenCalled();
});
});

View File

@ -11,7 +11,8 @@ import {
isCacheFeatureAvailable,
getVersionInputFromFile,
getVersionInputFromPlainFile,
getVersionInputFromTomlFile
getVersionInputFromTomlFile,
getNextPageUrl
} from '../src/utils';
jest.mock('@actions/cache');
@ -107,7 +108,7 @@ describe('Version from file test', () => {
await io.mkdirP(tempDir);
const pythonVersionFileName = 'pyproject.toml';
const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName);
const pythonVersion = '>=3.7';
const pythonVersion = '>=3.7.0';
const pythonVersionFileContent = `[project]\nrequires-python = "${pythonVersion}"`;
fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent);
expect(_fn(pythonVersionFilePath)).toEqual([pythonVersion]);
@ -119,7 +120,7 @@ describe('Version from file test', () => {
await io.mkdirP(tempDir);
const pythonVersionFileName = 'pyproject.toml';
const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName);
const pythonVersion = '>=3.7';
const pythonVersion = '>=3.7.0';
const pythonVersionFileContent = `[tool.poetry.dependencies]\npython = "${pythonVersion}"`;
fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent);
expect(_fn(pythonVersionFilePath)).toEqual([pythonVersion]);
@ -136,3 +137,25 @@ describe('Version from file test', () => {
}
);
});
describe('getNextPageUrl', () => {
it('GitHub API pagination next page is parsed correctly', () => {
function generateResponse(link: string) {
return {
statusCode: 200,
result: null,
headers: {
link: link
}
};
}
const page1Links =
'<https://api.github.com/repositories/129883600/releases?page=2>; rel="next", <https://api.github.com/repositories/129883600/releases?page=3>; rel="last"';
expect(getNextPageUrl(generateResponse(page1Links))).toStrictEqual(
'https://api.github.com/repositories/129883600/releases?page=2'
);
const page2Links =
'<https://api.github.com/repositories/129883600/releases?page=1>; rel="prev", <https://api.github.com/repositories/129883600/releases?page=1>; rel="first"';
expect(getNextPageUrl(generateResponse(page2Links))).toBeNull();
});
});

View File

@ -3300,8 +3300,11 @@ var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||
// Max safe segment length for coercion.
var MAX_SAFE_COMPONENT_LENGTH = 16
var MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6
// The actual regexps go on exports.re
var re = exports.re = []
var safeRe = exports.safeRe = []
var src = exports.src = []
var t = exports.tokens = {}
var R = 0
@ -3310,6 +3313,31 @@ function tok (n) {
t[n] = R++
}
var LETTERDASHNUMBER = '[a-zA-Z0-9-]'
// Replace some greedy regex tokens to prevent regex dos issues. These regex are
// used internally via the safeRe object since all inputs in this library get
// normalized first to trim and collapse all extra whitespace. The original
// regexes are exported for userland consumption and lower level usage. A
// future breaking change could export the safer regex only with a note that
// all input should have extra whitespace removed.
var safeRegexReplacements = [
['\\s', 1],
['\\d', MAX_LENGTH],
[LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH],
]
function makeSafeRe (value) {
for (var i = 0; i < safeRegexReplacements.length; i++) {
var token = safeRegexReplacements[i][0]
var max = safeRegexReplacements[i][1]
value = value
.split(token + '*').join(token + '{0,' + max + '}')
.split(token + '+').join(token + '{1,' + max + '}')
}
return value
}
// The following Regular Expressions can be used for tokenizing,
// validating, and parsing SemVer version strings.
@ -3319,14 +3347,14 @@ function tok (n) {
tok('NUMERICIDENTIFIER')
src[t.NUMERICIDENTIFIER] = '0|[1-9]\\d*'
tok('NUMERICIDENTIFIERLOOSE')
src[t.NUMERICIDENTIFIERLOOSE] = '[0-9]+'
src[t.NUMERICIDENTIFIERLOOSE] = '\\d+'
// ## Non-numeric Identifier
// Zero or more digits, followed by a letter or hyphen, and then zero or
// more letters, digits, or hyphens.
tok('NONNUMERICIDENTIFIER')
src[t.NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-][a-zA-Z0-9-]*'
src[t.NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-]' + LETTERDASHNUMBER + '*'
// ## Main Version
// Three dot-separated numeric identifiers.
@ -3368,7 +3396,7 @@ src[t.PRERELEASELOOSE] = '(?:-?(' + src[t.PRERELEASEIDENTIFIERLOOSE] +
// Any combination of digits, letters, or hyphens.
tok('BUILDIDENTIFIER')
src[t.BUILDIDENTIFIER] = '[0-9A-Za-z-]+'
src[t.BUILDIDENTIFIER] = LETTERDASHNUMBER + '+'
// ## Build Metadata
// Plus sign, followed by one or more period-separated build metadata
@ -3448,6 +3476,7 @@ src[t.COERCE] = '(^|[^\\d])' +
'(?:$|[^\\d])'
tok('COERCERTL')
re[t.COERCERTL] = new RegExp(src[t.COERCE], 'g')
safeRe[t.COERCERTL] = new RegExp(makeSafeRe(src[t.COERCE]), 'g')
// Tilde ranges.
// Meaning is "reasonably at or greater than"
@ -3457,6 +3486,7 @@ src[t.LONETILDE] = '(?:~>?)'
tok('TILDETRIM')
src[t.TILDETRIM] = '(\\s*)' + src[t.LONETILDE] + '\\s+'
re[t.TILDETRIM] = new RegExp(src[t.TILDETRIM], 'g')
safeRe[t.TILDETRIM] = new RegExp(makeSafeRe(src[t.TILDETRIM]), 'g')
var tildeTrimReplace = '$1~'
tok('TILDE')
@ -3472,6 +3502,7 @@ src[t.LONECARET] = '(?:\\^)'
tok('CARETTRIM')
src[t.CARETTRIM] = '(\\s*)' + src[t.LONECARET] + '\\s+'
re[t.CARETTRIM] = new RegExp(src[t.CARETTRIM], 'g')
safeRe[t.CARETTRIM] = new RegExp(makeSafeRe(src[t.CARETTRIM]), 'g')
var caretTrimReplace = '$1^'
tok('CARET')
@ -3493,6 +3524,7 @@ src[t.COMPARATORTRIM] = '(\\s*)' + src[t.GTLT] +
// this one has to use the /g flag
re[t.COMPARATORTRIM] = new RegExp(src[t.COMPARATORTRIM], 'g')
safeRe[t.COMPARATORTRIM] = new RegExp(makeSafeRe(src[t.COMPARATORTRIM]), 'g')
var comparatorTrimReplace = '$1$2$3'
// Something like `1.2.3 - 1.2.4`
@ -3521,6 +3553,14 @@ for (var i = 0; i < R; i++) {
debug(i, src[i])
if (!re[i]) {
re[i] = new RegExp(src[i])
// Replace all greedy whitespace to prevent regex dos issues. These regex are
// used internally via the safeRe object since all inputs in this library get
// normalized first to trim and collapse all extra whitespace. The original
// regexes are exported for userland consumption and lower level usage. A
// future breaking change could export the safer regex only with a note that
// all input should have extra whitespace removed.
safeRe[i] = new RegExp(makeSafeRe(src[i]))
}
}
@ -3545,7 +3585,7 @@ function parse (version, options) {
return null
}
var r = options.loose ? re[t.LOOSE] : re[t.FULL]
var r = options.loose ? safeRe[t.LOOSE] : safeRe[t.FULL]
if (!r.test(version)) {
return null
}
@ -3600,7 +3640,7 @@ function SemVer (version, options) {
this.options = options
this.loose = !!options.loose
var m = version.trim().match(options.loose ? re[t.LOOSE] : re[t.FULL])
var m = version.trim().match(options.loose ? safeRe[t.LOOSE] : safeRe[t.FULL])
if (!m) {
throw new TypeError('Invalid Version: ' + version)
@ -4045,6 +4085,7 @@ function Comparator (comp, options) {
return new Comparator(comp, options)
}
comp = comp.trim().split(/\s+/).join(' ')
debug('comparator', comp, options)
this.options = options
this.loose = !!options.loose
@ -4061,7 +4102,7 @@ function Comparator (comp, options) {
var ANY = {}
Comparator.prototype.parse = function (comp) {
var r = this.options.loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR]
var r = this.options.loose ? safeRe[t.COMPARATORLOOSE] : safeRe[t.COMPARATOR]
var m = comp.match(r)
if (!m) {
@ -4185,9 +4226,16 @@ function Range (range, options) {
this.loose = !!options.loose
this.includePrerelease = !!options.includePrerelease
// First, split based on boolean or ||
// First reduce all whitespace as much as possible so we do not have to rely
// on potentially slow regexes like \s*. This is then stored and used for
// future error messages as well.
this.raw = range
this.set = range.split(/\s*\|\|\s*/).map(function (range) {
.trim()
.split(/\s+/)
.join(' ')
// First, split based on boolean or ||
this.set = this.raw.split('||').map(function (range) {
return this.parseRange(range.trim())
}, this).filter(function (c) {
// throw out any that are not relevant for whatever reason
@ -4195,7 +4243,7 @@ function Range (range, options) {
})
if (!this.set.length) {
throw new TypeError('Invalid SemVer Range: ' + range)
throw new TypeError('Invalid SemVer Range: ' + this.raw)
}
this.format()
@ -4214,20 +4262,19 @@ Range.prototype.toString = function () {
Range.prototype.parseRange = function (range) {
var loose = this.options.loose
range = range.trim()
// `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4`
var hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE]
var hr = loose ? safeRe[t.HYPHENRANGELOOSE] : safeRe[t.HYPHENRANGE]
range = range.replace(hr, hyphenReplace)
debug('hyphen replace', range)
// `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5`
range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace)
debug('comparator trim', range, re[t.COMPARATORTRIM])
range = range.replace(safeRe[t.COMPARATORTRIM], comparatorTrimReplace)
debug('comparator trim', range, safeRe[t.COMPARATORTRIM])
// `~ 1.2.3` => `~1.2.3`
range = range.replace(re[t.TILDETRIM], tildeTrimReplace)
range = range.replace(safeRe[t.TILDETRIM], tildeTrimReplace)
// `^ 1.2.3` => `^1.2.3`
range = range.replace(re[t.CARETTRIM], caretTrimReplace)
range = range.replace(safeRe[t.CARETTRIM], caretTrimReplace)
// normalize spaces
range = range.split(/\s+/).join(' ')
@ -4235,7 +4282,7 @@ Range.prototype.parseRange = function (range) {
// At this point, the range is completely trimmed and
// ready to be split into comparators.
var compRe = loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR]
var compRe = loose ? safeRe[t.COMPARATORLOOSE] : safeRe[t.COMPARATOR]
var set = range.split(' ').map(function (comp) {
return parseComparator(comp, this.options)
}, this).join(' ').split(/\s+/)
@ -4335,7 +4382,7 @@ function replaceTildes (comp, options) {
}
function replaceTilde (comp, options) {
var r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE]
var r = options.loose ? safeRe[t.TILDELOOSE] : safeRe[t.TILDE]
return comp.replace(r, function (_, M, m, p, pr) {
debug('tilde', comp, _, M, m, p, pr)
var ret
@ -4376,7 +4423,7 @@ function replaceCarets (comp, options) {
function replaceCaret (comp, options) {
debug('caret', comp, options)
var r = options.loose ? re[t.CARETLOOSE] : re[t.CARET]
var r = options.loose ? safeRe[t.CARETLOOSE] : safeRe[t.CARET]
return comp.replace(r, function (_, M, m, p, pr) {
debug('caret', comp, _, M, m, p, pr)
var ret
@ -4435,7 +4482,7 @@ function replaceXRanges (comp, options) {
function replaceXRange (comp, options) {
comp = comp.trim()
var r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE]
var r = options.loose ? safeRe[t.XRANGELOOSE] : safeRe[t.XRANGE]
return comp.replace(r, function (ret, gtlt, M, m, p, pr) {
debug('xRange', comp, ret, gtlt, M, m, p, pr)
var xM = isX(M)
@ -4510,7 +4557,7 @@ function replaceXRange (comp, options) {
function replaceStars (comp, options) {
debug('replaceStars', comp, options)
// Looseness is ignored here. star is always as loose as it gets!
return comp.trim().replace(re[t.STAR], '')
return comp.trim().replace(safeRe[t.STAR], '')
}
// This function is passed to string.replace(re[t.HYPHENRANGE])
@ -4836,7 +4883,7 @@ function coerce (version, options) {
var match = null
if (!options.rtl) {
match = version.match(re[t.COERCE])
match = version.match(safeRe[t.COERCE])
} else {
// Find the right-most coercible string that does not share
// a terminus with a more left-ward coercible string.
@ -4847,17 +4894,17 @@ function coerce (version, options) {
// Stop when we get a match that ends at the string end, since no
// coercible string can be more right-ward without the same terminus.
var next
while ((next = re[t.COERCERTL].exec(version)) &&
while ((next = safeRe[t.COERCERTL].exec(version)) &&
(!match || match.index + match[0].length !== version.length)
) {
if (!match ||
next.index + next[0].length !== match.index + match[0].length) {
match = next
}
re[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length
safeRe[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length
}
// leave it in a clean state
re[t.COERCERTL].lastIndex = -1
safeRe[t.COERCERTL].lastIndex = -1
}
if (match === null) {
@ -59042,7 +59089,11 @@ module.exports = v4;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
@ -59151,7 +59202,11 @@ exports.CACHE_DEPENDENCY_BACKUP_PATH = '**/pyproject.toml';
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];

747
dist/setup/index.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,10 @@
- [Outputs](advanced-usage.md#outputs)
- [Environment variables](advanced-usage.md#environment-variables)
- [Using update-environment flag](advanced-usage.md#using-update-environment-flag)
- [Available versions of Python and PyPy](advanced-usage.md#available-versions-of-python-and-pypy)
- [Available versions of Python and PyPy](advanced-usage.md#available-versions-of-python-pypy-and-graalpy)
- [Python](advanced-usage.md#python)
- [PyPy](advanced-usage.md#pypy)
- [GraalPy](advanced-usage.md#graalpy)
- [Hosted tool cache](advanced-usage.md#hosted-tool-cache)
- [Using `setup-python` with a self-hosted runner](advanced-usage.md#using-setup-python-with-a-self-hosted-runner)
- [Windows](advanced-usage.md#windows)
@ -30,7 +31,7 @@ If there is a specific version of Python that you need and you don't want to wor
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.7.5'
@ -44,7 +45,7 @@ You can specify **only a major and minor version** if you are okay with the most
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.7'
@ -58,7 +59,7 @@ You can specify the version with **prerelease tag** to download and set up an ac
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.12.0-alpha.1'
@ -69,7 +70,7 @@ It's also possible to use **x.y-dev syntax** to download and set up the latest p
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.12-dev'
@ -82,7 +83,7 @@ You can also use several types of ranges that are specified in [semver](https://
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '>=3.9 <3.10'
@ -93,7 +94,7 @@ steps:
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.12.0-alpha - 3.12.0'
@ -104,7 +105,7 @@ steps:
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.x'
@ -117,7 +118,7 @@ The version of PyPy should be specified in the format `pypy<python_version>[-v<p
The `-v<pypy_version>` parameter is optional and can be skipped. The latest PyPy version will be used in this case.
```
pypy3.8 or pypy-3.8 # the latest available version of PyPy that supports Python 3.8
pypy3.9 or pypy-3.9 # the latest available version of PyPy that supports Python 3.9
pypy2.7 or pypy-2.7 # the latest available version of PyPy that supports Python 2.7
pypy3.7-v7.3.3 or pypy-3.7-v7.3.3 # Python 3.7 and PyPy 7.3.3
pypy3.7-v7.x or pypy-3.7-v7.x # Python 3.7 and the latest available PyPy 7.x
@ -137,7 +138,7 @@ jobs:
- 'pypy3.7' # the latest available version of PyPy that supports Python 3.7
- 'pypy3.7-v7.3.3' # Python 3.7 and PyPy 7.3.3
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
@ -155,7 +156,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: |
@ -172,7 +173,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: |
@ -189,7 +190,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: |
@ -211,10 +212,10 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '2.x', '3.x', 'pypy2.7', 'pypy3.7', 'pypy3.8' ]
python-version: [ '2.x', '3.x', 'pypy2.7', 'pypy3.8', 'pypy3.9' ]
name: Python ${{ matrix.python-version }} sample
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
@ -232,14 +233,14 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['2.7', '3.7', '3.8', '3.9', '3.10', 'pypy2.7', 'pypy3.8']
python-version: ['2.7', '3.7', '3.8', '3.9', '3.10', 'pypy2.7', 'pypy3.9']
exclude:
- os: macos-latest
python-version: '3.8'
- os: windows-latest
python-version: '3.6'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
@ -256,7 +257,7 @@ jobs:
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version-file: '.python-version' # Read python version from a file .python-version
@ -265,7 +266,7 @@ steps:
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version-file: 'pyproject.toml' # Read python version from a file pyproject.toml
@ -280,7 +281,7 @@ If `check-latest` is set to `true`, the action first checks if the cached versio
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.7'
@ -295,7 +296,7 @@ steps:
**Caching pipenv dependencies:**
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.9'
@ -308,7 +309,7 @@ steps:
**Caching poetry dependencies:**
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install poetry
run: pipx install poetry
- uses: actions/setup-python@v4
@ -322,7 +323,7 @@ steps:
**Using a list of file paths to cache dependencies**
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.9'
@ -337,7 +338,7 @@ steps:
**Using wildcard patterns to cache dependencies**
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.9'
@ -349,7 +350,7 @@ steps:
**Using a list of wildcard patterns to cache dependencies**
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.10'
@ -364,7 +365,7 @@ steps:
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.11'
@ -387,7 +388,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
id: cp310
with:
@ -404,7 +405,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
id: cp310
with:
@ -420,7 +421,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
id: cp310
with:
@ -451,7 +452,7 @@ Such a requirement on side-effect could be because you don't want your composite
```yaml
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
id: cp310
with:
@ -459,7 +460,7 @@ Such a requirement on side-effect could be because you don't want your composite
update-environment: false
- run: ${{ steps.cp310.outputs.python-path }} my_script.py
```
## Available versions of Python and PyPy
## Available versions of Python, PyPy and GraalPy
### Python
`setup-python` is able to configure **Python** from two sources:
@ -490,6 +491,11 @@ Such a requirement on side-effect could be because you don't want your composite
- 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/
### GraalPy
`setup-python` is able to download GraalPy versions from the [official GraalPy repository](https://github.com/oracle/graalpython).
- All available versions that we can download are listed in [releases](https://github.com/oracle/graalpython/releases).
## Hosted tool cache
GitHub hosted runners have a tool cache that comes with a few versions of Python + PyPy already installed. This tool cache helps speed up runs and tool setup by not requiring any new downloads. There is an environment variable called `RUNNER_TOOL_CACHE` on each runner that describes the location of the tool cache with Python and PyPy installed. `setup-python` works by taking a specific version of Python or PyPy from this tool cache and adding it to PATH.
@ -611,7 +617,7 @@ jobs:
python_version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "${{ matrix.python_version }}"

7134
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -36,21 +36,21 @@
"semver": "^7.5.2"
},
"devDependencies": {
"@types/jest": "^27.0.2",
"@types/jest": "^29.5.6",
"@types/node": "^16.11.25",
"@types/semver": "^7.1.0",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"@vercel/ncc": "^0.33.4",
"@vercel/ncc": "^0.38.0",
"eslint": "^8.35.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-node": "^11.1.0",
"husky": "^7.0.2",
"jest": "^27.2.5",
"jest-circus": "^27.2.5",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"prettier": "^2.8.4",
"ts-jest": "^27.0.5",
"ts-jest": "^29.1.1",
"typescript": "^4.2.3"
},
"husky": {

146
src/find-graalpy.ts Normal file
View File

@ -0,0 +1,146 @@
import * as path from 'path';
import * as graalpyInstall from './install-graalpy';
import {
IS_WINDOWS,
validateVersion,
IGraalPyManifestRelease,
getBinaryDirectory
} from './utils';
import * as semver from 'semver';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
export async function findGraalPyVersion(
versionSpec: string,
architecture: string,
updateEnvironment: boolean,
checkLatest: boolean,
allowPreReleases: boolean
): Promise<string> {
let resolvedGraalPyVersion = '';
let installDir: string | null;
let releases: IGraalPyManifestRelease[] | undefined;
let graalpyVersionSpec = parseGraalPyVersion(versionSpec);
if (checkLatest) {
releases = await graalpyInstall.getAvailableGraalPyVersions();
if (releases && releases.length > 0) {
const releaseData = graalpyInstall.findRelease(
releases,
graalpyVersionSpec,
architecture,
false
);
if (releaseData) {
core.info(`Resolved as GraalPy ${releaseData.resolvedGraalPyVersion}`);
graalpyVersionSpec = releaseData.resolvedGraalPyVersion;
} else {
core.info(
`Failed to resolve GraalPy ${graalpyVersionSpec} from manifest`
);
}
}
}
({installDir, resolvedGraalPyVersion} = findGraalPyToolCache(
graalpyVersionSpec,
architecture
));
if (!installDir) {
({installDir, resolvedGraalPyVersion} = await graalpyInstall.installGraalPy(
graalpyVersionSpec,
architecture,
allowPreReleases,
releases
));
}
const pipDir = IS_WINDOWS ? 'Scripts' : 'bin';
const _binDir = path.join(installDir, pipDir);
const binaryExtension = IS_WINDOWS ? '.exe' : '';
const pythonPath = path.join(
IS_WINDOWS ? installDir : _binDir,
`python${binaryExtension}`
);
const pythonLocation = getBinaryDirectory(installDir);
if (updateEnvironment) {
core.exportVariable('pythonLocation', installDir);
// https://cmake.org/cmake/help/latest/module/FindPython.html#module:FindPython
core.exportVariable('Python_ROOT_DIR', installDir);
// https://cmake.org/cmake/help/latest/module/FindPython2.html#module:FindPython2
core.exportVariable('Python2_ROOT_DIR', installDir);
// https://cmake.org/cmake/help/latest/module/FindPython3.html#module:FindPython3
core.exportVariable('Python3_ROOT_DIR', installDir);
core.exportVariable('PKG_CONFIG_PATH', pythonLocation + '/lib/pkgconfig');
core.addPath(pythonLocation);
core.addPath(_binDir);
}
core.setOutput('python-version', 'graalpy' + resolvedGraalPyVersion);
core.setOutput('python-path', pythonPath);
return resolvedGraalPyVersion;
}
export function findGraalPyToolCache(
graalpyVersion: string,
architecture: string
) {
let resolvedGraalPyVersion = '';
let installDir: string | null = tc.find(
'GraalPy',
graalpyVersion,
architecture
);
if (installDir) {
// 'tc.find' finds tool based on Python version but we also need to check
// whether GraalPy version satisfies requested version.
resolvedGraalPyVersion = path.basename(path.dirname(installDir));
const isGraalPyVersionSatisfies = semver.satisfies(
resolvedGraalPyVersion,
graalpyVersion
);
if (!isGraalPyVersionSatisfies) {
installDir = null;
resolvedGraalPyVersion = '';
}
}
if (!installDir) {
core.info(
`GraalPy version ${graalpyVersion} was not found in the local cache`
);
}
return {installDir, resolvedGraalPyVersion};
}
export function parseGraalPyVersion(versionSpec: string): string {
const versions = versionSpec.split('-').filter(item => !!item);
if (/^(graalpy)(.+)/.test(versions[0])) {
const version = versions[0].replace('graalpy', '');
versions.splice(0, 1, 'graalpy', version);
}
if (versions.length < 2 || versions[0] != 'graalpy') {
throw new Error(
"Invalid 'version' property for GraalPy. GraalPy version should be specified as 'graalpy<python-version>' or 'graalpy-<python-version>'. See README for examples and documentation."
);
}
const pythonVersion = versions[1];
if (!validateVersion(pythonVersion)) {
throw new Error(
"Invalid 'version' property for GraalPy. GraalPy versions should satisfy SemVer notation. See README for examples and documentation."
);
}
return pythonVersion;
}

View File

@ -7,7 +7,8 @@ import {
getPyPyVersionFromPath,
readExactPyPyVersionFile,
validatePythonVersionFormatForPyPy,
IPyPyManifestRelease
IPyPyManifestRelease,
getBinaryDirectory
} from './utils';
import * as semver from 'semver';
@ -82,7 +83,7 @@ export async function findPyPyVersion(
IS_WINDOWS ? installDir : _binDir,
`python${binaryExtension}`
);
const pythonLocation = pypyInstall.getPyPyBinaryPath(installDir);
const pythonLocation = getBinaryDirectory(installDir);
if (updateEnvironment) {
core.exportVariable('pythonLocation', installDir);
// https://cmake.org/cmake/help/latest/module/FindPython.html#module:FindPython

262
src/install-graalpy.ts Normal file
View File

@ -0,0 +1,262 @@
import * as os from 'os';
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 ifm from '@actions/http-client/interfaces';
import * as exec from '@actions/exec';
import fs from 'fs';
import {
IS_WINDOWS,
IGraalPyManifestRelease,
createSymlinkInFolder,
isNightlyKeyword,
getBinaryDirectory,
getNextPageUrl
} from './utils';
const TOKEN = core.getInput('token');
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
export async function installGraalPy(
graalpyVersion: string,
architecture: string,
allowPreReleases: boolean,
releases: IGraalPyManifestRelease[] | undefined
) {
let downloadDir;
releases = releases ?? (await getAvailableGraalPyVersions());
if (!releases || !releases.length) {
throw new Error('No release was found in GraalPy version.json');
}
let releaseData = findRelease(releases, graalpyVersion, architecture, false);
if (allowPreReleases && (!releaseData || !releaseData.foundAsset)) {
// check for pre-release
core.info(
[
`Stable GraalPy version ${graalpyVersion} with arch ${architecture} not found`,
`Trying pre-release versions`
].join(os.EOL)
);
releaseData = findRelease(releases, graalpyVersion, architecture, true);
}
if (!releaseData || !releaseData.foundAsset) {
throw new Error(
`GraalPy version ${graalpyVersion} with arch ${architecture} not found`
);
}
const {foundAsset, resolvedGraalPyVersion} = releaseData;
const downloadUrl = `${foundAsset.browser_download_url}`;
core.info(`Downloading GraalPy from "${downloadUrl}" ...`);
try {
const graalpyPath = await tc.downloadTool(downloadUrl, undefined, AUTH);
core.info('Extracting downloaded archive...');
downloadDir = await tc.extractTar(graalpyPath);
// 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(resolvedGraalPyVersion)) {
installDir = await tc.cacheDir(
toolDir,
'GraalPy',
resolvedGraalPyVersion,
architecture
);
}
const binaryPath = getBinaryDirectory(installDir);
await createGraalPySymlink(binaryPath, resolvedGraalPyVersion);
await installPip(binaryPath);
return {installDir, resolvedGraalPyVersion};
} catch (err) {
if (err instanceof Error) {
// Rate limit?
if (
err instanceof tc.HTTPError &&
(err.httpStatusCode === 403 || err.httpStatusCode === 429)
) {
core.info(
`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`
);
} else {
core.info(err.message);
}
if (err.stack !== undefined) {
core.debug(err.stack);
}
}
throw err;
}
}
export async function getAvailableGraalPyVersions() {
const http: httpm.HttpClient = new httpm.HttpClient('tool-cache');
const headers: ifm.IHeaders = {};
if (AUTH) {
headers.authorization = AUTH;
}
let url: string | null =
'https://api.github.com/repos/oracle/graalpython/releases';
const result: IGraalPyManifestRelease[] = [];
do {
const response: ifm.ITypedResponse<IGraalPyManifestRelease[]> =
await http.getJson(url, headers);
if (!response.result) {
throw new Error(
`Unable to retrieve the list of available GraalPy versions from '${url}'`
);
}
result.push(...response.result);
url = getNextPageUrl(response);
} while (url);
return result;
}
async function createGraalPySymlink(
graalpyBinaryPath: string,
graalpyVersion: string
) {
const version = semver.coerce(graalpyVersion)!;
const pythonBinaryPostfix = semver.major(version);
const pythonMinor = semver.minor(version);
const graalpyMajorMinorBinaryPostfix = `${pythonBinaryPostfix}.${pythonMinor}`;
const binaryExtension = IS_WINDOWS ? '.exe' : '';
core.info('Creating symlinks...');
createSymlinkInFolder(
graalpyBinaryPath,
`graalpy${binaryExtension}`,
`python${pythonBinaryPostfix}${binaryExtension}`,
true
);
createSymlinkInFolder(
graalpyBinaryPath,
`graalpy${binaryExtension}`,
`python${binaryExtension}`,
true
);
createSymlinkInFolder(
graalpyBinaryPath,
`graalpy${binaryExtension}`,
`graalpy${graalpyMajorMinorBinaryPostfix}${binaryExtension}`,
true
);
}
async function installPip(pythonLocation: string) {
core.info(
"Installing pip (GraalPy doesn't update pip because it uses a patched version of pip)"
);
const pythonBinary = path.join(pythonLocation, 'python');
await exec.exec(`${pythonBinary} -m ensurepip --default-pip`);
}
export function graalPyTagToVersion(tag: string) {
const versionPattern = /.*-(\d+\.\d+\.\d+(?:\.\d+)?)((?:a|b|rc))?(\d*)?/;
const match = tag.match(versionPattern);
if (match && match[2]) {
return `${match[1]}-${match[2]}.${match[3]}`;
} else if (match) {
return match[1];
} else {
return tag.replace(/.*-/, '');
}
}
export function findRelease(
releases: IGraalPyManifestRelease[],
graalpyVersion: string,
architecture: string,
includePrerelease: boolean
) {
const options = {includePrerelease: includePrerelease};
const filterReleases = releases.filter(item => {
const isVersionSatisfied = semver.satisfies(
graalPyTagToVersion(item.tag_name),
graalpyVersion,
options
);
return (
isVersionSatisfied && !!findAsset(item, architecture, process.platform)
);
});
if (!filterReleases.length) {
return null;
}
const sortedReleases = filterReleases.sort((previous, current) =>
semver.compare(
semver.coerce(graalPyTagToVersion(current.tag_name))!,
semver.coerce(graalPyTagToVersion(previous.tag_name))!
)
);
const foundRelease = sortedReleases[0];
const foundAsset = findAsset(foundRelease, architecture, process.platform);
return {
foundAsset,
resolvedGraalPyVersion: graalPyTagToVersion(foundRelease.tag_name)
};
}
export function toGraalPyPlatform(platform: string) {
switch (platform) {
case 'win32':
return 'windows';
case 'darwin':
return 'macos';
}
return platform;
}
export function toGraalPyArchitecture(architecture: string) {
switch (architecture) {
case 'x64':
return 'amd64';
case 'arm64':
return 'aarch64';
}
return architecture;
}
export function findAsset(
item: IGraalPyManifestRelease,
architecture: string,
platform: string
) {
const graalpyArch = toGraalPyArchitecture(architecture);
const graalpyPlatform = toGraalPyPlatform(platform);
const found = item.assets.filter(
file =>
file.name.startsWith('graalpy') &&
file.name.endsWith(`-${graalpyPlatform}-${graalpyArch}.tar.gz`)
);
/*
In the future there could be more variants of GraalPy for a single release. Pick the shortest name, that one is the most likely to be the primary variant.
*/
found.sort((f1, f2) => f1.name.length - f2.name.length);
return found[0];
}

View File

@ -13,7 +13,8 @@ import {
IPyPyManifestRelease,
createSymlinkInFolder,
isNightlyKeyword,
writeExactPyPyVersionFile
writeExactPyPyVersionFile,
getBinaryDirectory
} from './utils';
export async function installPyPy(
@ -94,7 +95,7 @@ export async function installPyPy(
writeExactPyPyVersionFile(installDir, resolvedPyPyVersion);
const binaryPath = getPyPyBinaryPath(installDir);
const binaryPath = getBinaryDirectory(installDir);
await createPyPySymlink(binaryPath, resolvedPythonVersion);
await installPip(binaryPath);
@ -237,15 +238,6 @@ export function 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.
*/
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');

View File

@ -1,6 +1,7 @@
import * as core from '@actions/core';
import * as finder from './find-python';
import * as finderPyPy from './find-pypy';
import * as finderGraalPy from './find-graalpy';
import * as path from 'path';
import * as os from 'os';
import fs from 'fs';
@ -17,6 +18,10 @@ function isPyPyVersion(versionSpec: string) {
return versionSpec.startsWith('pypy');
}
function isGraalPyVersion(versionSpec: string) {
return versionSpec.startsWith('graalpy');
}
async function cacheDependencies(cache: string, pythonVersion: string) {
const cacheDependencyPath =
core.getInput('cache-dependency-path') || undefined;
@ -106,10 +111,20 @@ async function run() {
core.info(
`Successfully set up PyPy ${installed.resolvedPyPyVersion} with Python (${installed.resolvedPythonVersion})`
);
} else if (isGraalPyVersion(version)) {
const installed = await finderGraalPy.findGraalPyVersion(
version,
arch,
updateEnvironment,
checkLatest,
allowPreReleases
);
pythonVersion = `${installed}`;
core.info(`Successfully set up GraalPy ${installed}`);
} else {
if (version.startsWith('2')) {
core.warning(
'The support for python 2.7 will be removed on June 19. Related issue: https://github.com/actions/setup-python/issues/672'
'The support for python 2.7 was removed on June 19, 2023. Related issue: https://github.com/actions/setup-python/issues/672'
);
}
const installed = await finder.useCpythonVersion(

View File

@ -6,6 +6,7 @@ import * as path from 'path';
import * as semver from 'semver';
import * as toml from '@iarna/toml';
import * as exec from '@actions/exec';
import * as ifm from '@actions/http-client/interfaces';
export const IS_WINDOWS = process.platform === 'win32';
export const IS_LINUX = process.platform === 'linux';
@ -29,6 +30,16 @@ export interface IPyPyManifestRelease {
files: IPyPyManifestAsset[];
}
export interface IGraalPyManifestAsset {
name: string;
browser_download_url: string;
}
export interface IGraalPyManifestRelease {
tag_name: string;
assets: IGraalPyManifestAsset[];
}
/** create Symlinks for downloaded PyPy
* It should be executed only for downloaded versions in runtime, because
* toolcache versions have this setup.
@ -90,7 +101,7 @@ export function writeExactPyPyVersionFile(
}
/**
* Python version should be specified explicitly like "x.y" (2.7, 3.6, 3.7)
* Python version should be specified explicitly like "x.y" (3.10, 3.11, etc)
* "3.x" or "3" are not supported
* because it could cause ambiguity when both PyPy version and Python version are not precise
*/
@ -229,7 +240,21 @@ export function getVersionInputFromTomlFile(versionFile: string): string[] {
}
core.info(`Extracted ${versions} from ${versionFile}`);
return Array.from(versions, version => version.split(',').join(' '));
const rawVersions = Array.from(versions, version =>
version.split(',').join(' ')
);
const validatedVersions = rawVersions
.map(item => semver.validRange(item, true))
.filter((versionRange, index) => {
if (!versionRange) {
core.debug(
`The version ${rawVersions[index]} is not valid SemVer range`
);
}
return !!versionRange;
}) as string[];
return validatedVersions;
}
/**
@ -237,7 +262,7 @@ export function getVersionInputFromTomlFile(versionFile: string): string[] {
*/
export function getVersionInputFromPlainFile(versionFile: string): string[] {
core.debug(`Trying to resolve version form ${versionFile}`);
const version = fs.readFileSync(versionFile, 'utf8');
const version = fs.readFileSync(versionFile, 'utf8').trim();
core.info(`Resolved ${versionFile} as ${version}`);
return [version];
}
@ -252,3 +277,34 @@ export function getVersionInputFromFile(versionFile: string): string[] {
return getVersionInputFromPlainFile(versionFile);
}
}
/**
* Get the directory containing interpreter binary from installation directory of PyPy or GraalPy
* - On Linux and macOS, the Python interpreter is in 'bin'.
* - On Windows, it is in the installation root.
*/
export function getBinaryDirectory(installDir: string) {
return IS_WINDOWS ? installDir : path.join(installDir, 'bin');
}
/**
* Extract next page URL from a HTTP response "link" header. Such headers are used in GitHub APIs.
*/
export function getNextPageUrl<T>(response: ifm.ITypedResponse<T>) {
const responseHeaders = <ifm.IHeaders>response.headers;
const linkHeader = responseHeaders.link;
if (typeof linkHeader === 'string') {
for (const link of linkHeader.split(/\s*,\s*/)) {
const match = link.match(/<([^>]+)>(.*)/);
if (match) {
const url = match[1];
for (const param of match[2].split(/\s*;\s*/)) {
if (param.match(/rel="?next"?/)) {
return url;
}
}
}
}
}
return null;
}