├── .github └── workflows │ └── main.yml ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── action.yml ├── bin └── install-python └── setup.cfg /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | push: 4 | branches: [main, test-me-*] 5 | 6 | jobs: 7 | main: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | include: 12 | - {python: '3.7', debug: true, nogil: false} 13 | - {python: '3.13-dev', debug: false, nogil: false} 14 | - {python: '3.14-dev', debug: false, nogil: true} 15 | - {python: '3.14-dev', debug: false, nogil: false, tk: true} 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: ./. 19 | with: 20 | python-version: ${{ matrix.python }} 21 | debug: ${{ matrix.debug }} 22 | nogil: ${{ matrix.nogil }} 23 | tk: ${{ matrix.tk }} 24 | 25 | - name: check tk 26 | if: matrix.tk 27 | run: python -c 'import tkinter' 28 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: debug-statements 9 | - id: double-quote-string-fixer 10 | - id: name-tests-test 11 | - id: requirements-txt-fixer 12 | - repo: https://github.com/asottile/reorder-python-imports 13 | rev: v3.15.0 14 | hooks: 15 | - id: reorder-python-imports 16 | args: [--py39-plus, --add-import, 'from __future__ import annotations'] 17 | - repo: https://github.com/asottile/add-trailing-comma 18 | rev: v3.2.0 19 | hooks: 20 | - id: add-trailing-comma 21 | - repo: https://github.com/asottile/pyupgrade 22 | rev: v3.20.0 23 | hooks: 24 | - id: pyupgrade 25 | args: [--py39-plus] 26 | - repo: https://github.com/hhatto/autopep8 27 | rev: v2.3.2 28 | hooks: 29 | - id: autopep8 30 | - repo: https://github.com/PyCQA/flake8 31 | rev: 7.2.0 32 | hooks: 33 | - id: flake8 34 | - repo: https://github.com/pre-commit/mirrors-mypy 35 | rev: v1.16.0 36 | hooks: 37 | - id: mypy 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Anthony Sottile 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/deadsnakes/action/main.svg)](https://results.pre-commit.ci/latest/github/deadsnakes/action/main) 2 | 3 | deadsnakes/action 4 | ================= 5 | 6 | a GitHub action to install (pre-release) pythons from [deadsnakes] 7 | 8 | [deadsnakes]: https://github.com/deadsnakes 9 | 10 | ### using this action 11 | 12 | To use this action, add it adjacent to `setup-python` and opt into it 13 | conditionally. Here's an example which uses `python-version` as a matrix. 14 | 15 | ```yaml 16 | on: 17 | push: 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | python-version: [3.6, 3.7, 3.8, 3.9-dev, 3.10-dev] 25 | name: main 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions/setup-python@v2 29 | if: "!endsWith(matrix.python-version, '-dev')" 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - uses: deadsnakes/action@v3.2.0 33 | if: endsWith(matrix.python-version, '-dev') 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | # debug: true # Optional, to select a Python debug build 37 | # nogil: true # Optional, to select a free-threaded Python build (3.13+ only) 38 | - run: python --version --version && which python 39 | ``` 40 | 41 | ### available versions 42 | 43 | - to use nightly builds, add `-dev` to the end of the version name. 44 | - [available nightly versions] 45 | - to use tagged builds, just use the version number 46 | - [available versions] 47 | 48 | In either case, the actions's `debug` input can be used to install a 49 | debug build of the selected Python version, by adding `debug: true`. 50 | 51 | The `nogil` input can be used instead of `debug` to install an *experimental* 52 | free-threaded build of the selected Python version, by adding `nogil: true` 53 | Only available for Python 3.13 and later. 54 | 55 | The action's `tk` input can be used to install Tkinter, which is not included 56 | by default. If `debug` is set then `tk-dbg` will be used. If `nogil` is set 57 | then `tk-nogil` will be used; only available for Python 3.13 and later. 58 | 59 | [available nightly versions]: https://launchpad.net/~deadsnakes/+archive/ubuntu/nightly/+packages 60 | [available versions]: https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa/+packages 61 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: deadsnakes 2 | description: install (pre-release) pythons from deadsnakes 3 | inputs: 4 | python-version: 5 | description: python version to use, such as '3.9' 6 | required: true 7 | debug: 8 | description: use debug version of python 9 | required: false 10 | default: false 11 | nogil: 12 | description: use free-threaded version of python 13 | required: false 14 | default: false 15 | tk: 16 | description: include Tkinter 17 | required: false 18 | default: false 19 | runs: 20 | using: composite 21 | steps: 22 | - name: add deadsnakes ppa and install ${{ inputs.python-version }} ${{ inputs.debug == 'true' && '(debug)' || '' }} ${{ inputs.tk == 'true' && '(tk)' || '' }} 23 | run: ${{ github.action_path }}/bin/install-python ${{ inputs.python-version }} ${{ inputs.debug == 'true' && '--debug' || '' }} ${{ inputs.nogil == 'true' && '--nogil' || '' }} ${{ inputs.tk == 'true' && '--tk' || '' }} 24 | shell: bash 25 | -------------------------------------------------------------------------------- /bin/install-python: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import argparse 5 | import contextlib 6 | import os.path 7 | import shlex 8 | import subprocess 9 | from collections.abc import Generator 10 | from typing import NamedTuple 11 | 12 | 13 | class Group(NamedTuple): 14 | section: str 15 | cmds: tuple[tuple[str, ...], ...] 16 | 17 | @classmethod 18 | def make(cls, section: str, *cmds: tuple[str, ...]) -> Group: 19 | return cls(section, cmds) 20 | 21 | 22 | @contextlib.contextmanager 23 | def _group(s: str) -> Generator[None]: 24 | print(f'::group::{s}') 25 | try: 26 | yield 27 | finally: 28 | print('::endgroup::') 29 | 30 | 31 | def _print_call(*args: str) -> int: 32 | print(f'[command] {shlex.join(args)}', flush=True) 33 | return subprocess.call(args) 34 | 35 | 36 | def main() -> int: 37 | parser = argparse.ArgumentParser() 38 | parser.add_argument('version') 39 | mut = parser.add_mutually_exclusive_group() 40 | mut.add_argument('--debug', action='store_true') 41 | mut.add_argument('--nogil', action='store_true') 42 | parser.add_argument('--tk', action='store_true') 43 | args = parser.parse_args() 44 | 45 | if args.version.endswith('-dev'): 46 | version = args.version[:-1 * len('-dev')] 47 | ppa = 'ppa:deadsnakes/nightly' 48 | else: 49 | version = args.version 50 | ppa = 'ppa:deadsnakes/ppa' 51 | 52 | major_s, minor_s = version.split('.') 53 | major, minor = int(major_s), int(minor_s) 54 | py = f'python{version}' 55 | packages = [f'{py}-dev', f'{py}-venv'] 56 | if (major, minor) < (3, 12): 57 | packages.append(f'{py}-distutils') 58 | if args.debug: 59 | packages.append(f'{py}-dbg') 60 | py_executable = f'{py}-dbg' 61 | elif args.nogil: 62 | packages.append(f'{py}-nogil') 63 | py_executable = f'{py}-nogil' 64 | else: 65 | py_executable = py 66 | if args.tk: 67 | if args.debug: 68 | packages.append(f'{py}-tk-dbg') 69 | elif args.nogil: 70 | packages.append(f'{py}-tk-nogil') 71 | else: 72 | packages.append(f'{py}-tk') 73 | 74 | envdir = os.path.expanduser(f'~/venv-{version}') 75 | bindir = os.path.join(envdir, 'bin') 76 | pip = os.path.join(bindir, 'pip') 77 | 78 | groups = ( 79 | Group.make( 80 | f'add ppa {ppa}', 81 | ('sudo', 'add-apt-repository', '--yes', ppa), 82 | ), 83 | Group.make( 84 | f'install {py}', 85 | ( 86 | 'sudo', 'apt-get', 'install', '-y', '--no-install-recommends', 87 | *packages, 88 | ), 89 | ), 90 | Group.make( 91 | f'set up {py_executable} environment', 92 | (py_executable, '-mvenv', envdir), 93 | (pip, 'install', '--upgrade', 'pip', 'setuptools', 'wheel'), 94 | ), 95 | ) 96 | 97 | for group in groups: 98 | with _group(group.section): 99 | for cmd in group.cmds: 100 | if _print_call(*cmd): 101 | return 1 102 | 103 | # check whether the installed package came from deadsnakes 104 | out = subprocess.check_output(('apt-cache', 'policy', py)).decode() 105 | seen = False 106 | for line in out.splitlines(): 107 | if line.strip().startswith('*** '): 108 | seen = True 109 | elif seen and '/deadsnakes/' not in line: 110 | raise SystemExit(f'package {py} is not from deadsnakes') 111 | elif seen: 112 | break 113 | 114 | with open(os.environ['GITHUB_PATH'], 'a') as fp: 115 | fp.write(f'{bindir}\n') 116 | 117 | return 0 118 | 119 | 120 | if __name__ == '__main__': 121 | raise SystemExit(main()) 122 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [mypy] 2 | check_untyped_defs = true 3 | disallow_any_generics = true 4 | disallow_incomplete_defs = true 5 | disallow_untyped_defs = true 6 | warn_redundant_casts = true 7 | warn_unused_ignores = true 8 | --------------------------------------------------------------------------------