Repository: python/release-tools Branch: main Commit: e7515f5ae838 Files: 83 Total size: 496.9 KB Directory structure: gitextract_8g54ntzm/ ├── .coveragerc ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── build-release.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .ruff.toml ├── LICENSE.txt ├── README.md ├── add_to_pydotorg.py ├── buildbotapi.py ├── dev-requirements.in ├── dev-requirements.txt ├── mypy-requirements.txt ├── pyproject.toml ├── release.py ├── requirements.in ├── requirements.txt ├── run_release.py ├── sbom.py ├── select_jobs.py ├── tests/ │ ├── README.rst │ ├── __init__.py │ ├── buildbotapi/ │ │ ├── builders.json │ │ ├── failure.json │ │ ├── no-builds.json │ │ └── success.json │ ├── fake-artifact.txt │ ├── fake-ftp-files.txt │ ├── magicdata/ │ │ ├── Include/ │ │ │ └── internal/ │ │ │ └── pycore_magic_number.h │ │ └── Lib/ │ │ └── test/ │ │ └── test_importlib/ │ │ └── test_util.py │ ├── patchlevel.h │ ├── sbom/ │ │ ├── sbom-with-pip-removed.json │ │ └── sbom-with-pip.json │ ├── test_add_to_pydotorg.py │ ├── test_buildbotapi.py │ ├── test_release.py │ ├── test_release_tag.py │ ├── test_run_release.py │ ├── test_sbom.py │ ├── test_select_jobs.py │ ├── test_update_version_next.py │ └── whatsnew_index.rst ├── tox.ini ├── update_version_next.py └── windows-release/ ├── README.md ├── acquire-vcruntime.yml ├── azure-pipelines.yml ├── build-steps-pgo.yml ├── build-steps.yml ├── checkout.yml ├── find-sdk.yml ├── find-tools.yml ├── layout-command.yml ├── libffi-build.yml ├── merge-and-upload.py ├── msi-steps.yml ├── openssl-build.yml ├── purge.py ├── sign-files.yml ├── stage-build.yml ├── stage-layout-embed.yml ├── stage-layout-full.yml ├── stage-layout-msix.yml ├── stage-layout-nuget.yml ├── stage-layout-pymanager.yml ├── stage-layout-symbols.yml ├── stage-msi.yml ├── stage-pack-msix.yml ├── stage-pack-nuget.yml ├── stage-pack-pymanager.yml ├── stage-publish-nugetorg.yml ├── stage-publish-pymanager.yml ├── stage-publish-pythonorg.yml ├── stage-sign.yml ├── stage-test-embed.yml ├── stage-test-msi.yml ├── stage-test-nuget.yml ├── stage-test-pymanager.yml ├── start-arm64vm.yml ├── tcltk-build.yml └── uploadrelease.ps1 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ # .coveragerc to control coverage.py [report] # Regexes for lines to exclude from consideration exclude_also = # Don't complain if non-runnable code isn't run: if __name__ == .__main__. def main def get_arg_parser ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: monthly groups: actions: patterns: - "*" cooldown: default-days: 7 - package-ecosystem: pip directory: "/" schedule: interval: monthly assignees: - "ezio-melotti" groups: pip: patterns: - "*" cooldown: default-days: 7 ================================================ FILE: .github/workflows/build-release.yml ================================================ on: push: paths-ignore: - ".github/dependabot.yml" - ".github/workflows/lint.yml" - ".github/workflows/test.yml" - ".pre-commit-config.yaml" - ".ruff.toml" - "README.md" - "tests/**" pull_request: paths-ignore: - ".github/dependabot.yml" - ".github/workflows/lint.yml" - ".github/workflows/test.yml" - ".pre-commit-config.yaml" - ".ruff.toml" - "README.md" - "tests/**" workflow_dispatch: inputs: git_remote: type: choice description: "Git remote to checkout" options: - python - savannahostrowski - hugovk - Yhg1s - pablogsal - ambv git_commit: type: string description: "Git commit to target for the release. Must use the full commit SHA, not the short ID" cpython_release: type: string description: "CPython release number (ie '3.11.5', note without the 'v' prefix)" name: "Build release artifacts" permissions: {} # Set from inputs for workflow_dispatch, or set defaults to test push/PR events env: GIT_REMOTE: ${{ github.event.inputs.git_remote || 'python' }} GIT_COMMIT: ${{ github.event.inputs.git_commit || '55ea59e7dc35e1363b203ae4dd9cfc3a0ac0a844' }} CPYTHON_RELEASE: ${{ github.event.inputs.cpython_release || '3.15.0a8' }} jobs: verify-input: runs-on: ubuntu-24.04 timeout-minutes: 5 outputs: build-docs: ${{ steps.select-jobs.outputs.docs }} build-android: ${{ steps.select-jobs.outputs.android }} build-ios: ${{ steps.select-jobs.outputs.ios }} steps: - name: "Workflow run information" run: | echo "git_remote: $GIT_REMOTE" echo "git_commit: $GIT_COMMIT" echo "cpython_release: $CPYTHON_RELEASE" { echo "| Variable | Value |" echo "| --------------- | -------------------- |" echo "| git_remote | \`$GIT_REMOTE\` |" echo "| git_commit | \`$GIT_COMMIT\` |" echo "| cpython_release | \`$CPYTHON_RELEASE\` |" } >> "$GITHUB_STEP_SUMMARY" - name: "Checkout python/release-tools" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Checkout ${{ env.GIT_REMOTE }}/cpython" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false repository: "${{ env.GIT_REMOTE }}/cpython" ref: "v${{ env.CPYTHON_RELEASE }}" path: "cpython" - name: "Verify CPython commit matches tag" run: | if [[ "$GIT_COMMIT" != "$(cd cpython && git rev-parse HEAD)" ]]; then echo "expected git commit ('$GIT_COMMIT') didn't match tagged commit ('$(git rev-parse HEAD)')" exit 1 fi - name: "Setup Python" uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.12 - name: "Select jobs" id: select-jobs run: | test_flag="" if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then test_flag="--test" fi output=$(./select_jobs.py $test_flag "$CPYTHON_RELEASE") echo "$output" | tee -a "$GITHUB_OUTPUT" { echo "| Job | Enabled |" echo "| ------- | ------- |" echo "$output" | while IFS='=' read -r key value; do echo "| $key | $value |" done } >> "$GITHUB_STEP_SUMMARY" build-source: runs-on: ubuntu-24.04 timeout-minutes: 15 needs: - verify-input steps: - name: "Checkout python/release-tools" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Checkout ${{ env.GIT_REMOTE }}/cpython" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false repository: "${{ env.GIT_REMOTE }}/cpython" ref: "v${{ env.CPYTHON_RELEASE }}" path: "cpython" - name: "Setup Python" uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.12 - name: "Install source dependencies" run: | python -m pip install --no-deps \ -r requirements.txt - name: "Build Python release artifacts" run: | cd cpython python ../release.py --export "$CPYTHON_RELEASE" --skip-docs - name: "Upload the source artifacts" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: source path: | cpython/${{ env.CPYTHON_RELEASE }}/src build-docs: runs-on: ubuntu-24.04 timeout-minutes: 45 needs: - verify-input if: fromJSON(needs.verify-input.outputs.build-docs) steps: - name: "Checkout ${{ env.GIT_REMOTE }}/cpython" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false repository: "${{ env.GIT_REMOTE }}/cpython" ref: "v${{ env.CPYTHON_RELEASE }}" - name: "Setup Python" uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.12 - name: "Install docs dependencies" run: | python -m pip install -r Doc/requirements.txt - name: "Build docs" env: SPHINXOPTS: "-j10" run: | cd Doc make dist-epub make dist-html make dist-texinfo make dist-text - name: "Upload the docs artifacts" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs path: | Doc/dist/ test-source: runs-on: ubuntu-24.04 timeout-minutes: 60 needs: - build-source steps: - name: "Download the source artifacts" uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: source - name: "Test Python source tarballs" run: | mkdir -p ./tmp/installation/ cp "Python-$CPYTHON_RELEASE.tgz" ./tmp/ cd tmp/ tar xvf "Python-$CPYTHON_RELEASE.tgz" cd "Python-$CPYTHON_RELEASE" ./configure "--prefix=$(realpath '../installation/')" make -j make install -j cd ../installation ./bin/python3 -m test -uall -j4 test-docs: runs-on: ubuntu-24.04 timeout-minutes: 15 needs: - build-docs steps: - name: "Download the docs artifacts" uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: docs - name: "Set up Python" uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - name: "Install epubcheck" run: python -m pip install epubcheck - name: "Run epubcheck" continue-on-error: true run: | ls -la epubcheck "python-$CPYTHON_RELEASE-docs.epub" &> epubcheck.txt - name: "Show epubcheck output" run: cat epubcheck.txt - name: "Check for fatal errors in EPUB" run: | if grep -q "^FATAL" epubcheck.txt; then echo "Fatal errors found in EPUB:" grep "^FATAL" epubcheck.txt exit 1 fi echo "No fatal errors found in EPUB" build-android: name: build-android (${{ matrix.arch }}) needs: - verify-input if: fromJSON(needs.verify-input.outputs.build-android) strategy: matrix: include: - arch: aarch64 runs-on: macos-15 - arch: x86_64 runs-on: ubuntu-24.04 runs-on: ${{ matrix.runs-on }} timeout-minutes: 60 env: triplet: ${{ matrix.arch }}-linux-android steps: - name: "Checkout ${{ env.GIT_REMOTE }}/cpython" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false repository: "${{ env.GIT_REMOTE }}/cpython" ref: "v${{ env.CPYTHON_RELEASE }}" # Python 3.15 moved the build tools to the Platforms directory. Add a # compatibility shim to simplify execution. Can be removed when 3.14 # reaches EOL - name: Set up compatibility symlink run: | if [ ! -e Platforms/Android ]; then mkdir -p Platforms ln -s ../Android Platforms/Android ln -s ./android.py Android/__main__.py fi - name: Build and test run: python3 Platforms/Android ci --fast-ci "$triplet" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.triplet }} path: cross-build/${{ env.triplet }}/dist/* if-no-files-found: error build-ios: name: build-iOS needs: - verify-input if: fromJSON(needs.verify-input.outputs.build-ios) runs-on: macos-14 timeout-minutes: 60 steps: - name: "Checkout ${{ env.GIT_REMOTE }}/cpython" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false repository: "${{ env.GIT_REMOTE }}/cpython" ref: "v${{ env.CPYTHON_RELEASE }}" - name: Build and test run: python3 Platforms/Apple ci iOS --slow-ci - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ios path: cross-build/dist/* if-no-files-found: error ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: [push, pull_request, workflow_dispatch] permissions: {} env: FORCE_COLOR: 1 RUFF_OUTPUT_FORMAT: github jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" cache: pip - uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2 - name: Install dependencies run: | python3 -m pip install -U pip python3 -m pip install -U tox - name: Mypy run: tox -e mypy - name: Run PSScriptAnalyzer on PowerShell scripts shell: pwsh run: | Invoke-ScriptAnalyzer -Path . -Recurse -Severity ParseError,Error -EnableExit ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: [push, pull_request, workflow_dispatch] permissions: {} env: FORCE_COLOR: 1 jobs: tests: name: "Tests" runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python-version: ["3.12", "3.13", "3.14"] os: [macos-latest, ubuntu-latest] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} cache: pip cache-dependency-path: dev-requirements.txt - run: | python -m pip install tox - run: | tox -e py - name: Upload coverage uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: token: ${{ secrets.CODECOV_ORG_TOKEN }} ================================================ FILE: .gitignore ================================================ # .idea/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.10 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror rev: 25.12.0 hooks: - id: black - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-toml - id: check-yaml exclude: windows-release/(azure-pipelines|msi-steps).yml - id: debug-statements - id: end-of-file-fixer - id: forbid-submodules - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.36.0 hooks: - id: check-dependabot - id: check-github-workflows - repo: https://github.com/rhysd/actionlint rev: v1.7.10 hooks: - id: actionlint - repo: https://github.com/woodruffw/zizmor-pre-commit rev: v1.19.0 hooks: - id: zizmor - repo: https://github.com/tox-dev/pyproject-fmt rev: v2.11.1 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject rev: v0.24.1 hooks: - id: validate-pyproject - repo: https://github.com/tox-dev/tox-ini-fmt rev: 1.7.1 hooks: - id: tox-ini-fmt - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes ================================================ FILE: .ruff.toml ================================================ fix = true [lint] select = [ "C4", # flake8-comprehensions "E", # pycodestyle errors "F", # pyflakes errors "I", # isort "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging "PGH", # pygrep-hooks "RUF100", # unused noqa (yesqa) "UP", # pyupgrade "W", # pycodestyle warnings "YTT", # flake8-2020 ] ignore = [ "E203", # Whitespace before ':' "E221", # Multiple spaces before operator "E226", # Missing whitespace around arithmetic operator "E241", # Multiple spaces after ',' "E501", # Line too long ] ================================================ FILE: LICENSE.txt ================================================ PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 -------------------------------------------- 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2008 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. ================================================ FILE: README.md ================================================ # release-tools [![.github/workflows/test.yml](https://github.com/python/release-tools/actions/workflows/test.yml/badge.svg)](https://github.com/python/release-tools/actions/workflows/test.yml) Scripts for making (C)Python releases. ================================================ FILE: add_to_pydotorg.py ================================================ #!/usr/bin/env python """ Script to add ReleaseFile objects for Python releases on the new pydotorg. To use (RELEASE is the full Python version number): * Copy this script to dl-files (it needs access to all the release files). You could also download all files, then you need to use the "--ftp-root" argument. * Make sure all download files are in place in the correct FTP subdirectory. * Create a new Release object via the Django admin (adding via API is currently broken), the name MUST be "Python RELEASE". * Put an AUTH_INFO variable containing "username:api_key" in your environment. * Call this script as "python add_to_pydotorg.py RELEASE". Each call will remove all previous file objects, so you can call the script multiple times. Georg Brandl, March 2014. """ import argparse import hashlib import json import os import re import subprocess import sys from collections.abc import Generator from os import path from typing import Any, NoReturn import requests # Copied from release.py def error(*msgs: Any) -> NoReturn: print("**ERROR**", file=sys.stderr) for msg in msgs: print(msg, file=sys.stderr) sys.exit(1) # Copied from release.py def run_cmd( cmd: list[str] | str, silent: bool = False, shell: bool = False, **kwargs: Any ) -> None: if shell: cmd = " ".join(cmd) if not silent: print(f"Executing {cmd}") try: if silent: subprocess.check_call(cmd, shell=shell, stdout=subprocess.PIPE, **kwargs) else: subprocess.check_call(cmd, shell=shell, **kwargs) except subprocess.CalledProcessError: error(f"{cmd} failed") try: auth_info = os.environ["AUTH_INFO"] except KeyError: print( "Please set an environment variable named AUTH_INFO " 'containing "username:api_key".' ) sys.exit() download_root = "https://www.python.org/ftp/python/" tag_cre = re.compile(r"(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:([ab]|rc)(\d+))?$") headers = {"Authorization": f"ApiKey {auth_info}", "Content-Type": "application/json"} github_oidc_provider = "https://github.com/login/oauth" google_oidc_provider = "https://accounts.google.com" # Update this list when new release managers are added. release_to_sigstore_identity_and_oidc_issuer = { "3.10": ("pablogsal@python.org", google_oidc_provider), "3.11": ("pablogsal@python.org", google_oidc_provider), "3.12": ("thomas@python.org", google_oidc_provider), "3.13": ("thomas@python.org", google_oidc_provider), "3.14": ("hugo@python.org", github_oidc_provider), "3.15": ("hugo@python.org", github_oidc_provider), "3.16": ("savannah@python.org", github_oidc_provider), "3.17": ("savannah@python.org", github_oidc_provider), } def macos_description(version: tuple[int, int, int]) -> str: if version >= (3, 14): return "for macOS 10.15 and later" else: return "for macOS 10.13 and later" def get_file_descriptions( release: str, ) -> list[tuple[re.Pattern[str], tuple[str, str, bool, str]]]: v = base_version_tuple(release) rx = re.compile # value is (file "name", OS slug, download button, file "description"). # OS=None means no ReleaseFile object. Only one matching *file* (not regex) # per OS can have download=True. return [ (rx(r"\.tgz$"), ("Gzipped source tarball", "source", False, "")), (rx(r"\.tar\.xz$"), ("XZ compressed source tarball", "source", True, "")), ( rx(r"windows-.+\.json"), ( "Windows release manifest", "windows", False, f"Install with 'py install {v[0]}.{v[1]}'", ), ), ( rx(r"-embed-amd64\.zip$"), ("Windows embeddable package (64-bit)", "windows", False, ""), ), ( rx(r"-embed-arm64\.zip$"), ("Windows embeddable package (ARM64)", "windows", False, ""), ), ( rx(r"-arm64\.exe$"), ("Windows installer (ARM64)", "windows", False, "Experimental"), ), ( rx(r"-amd64\.exe$"), ("Windows installer (64-bit)", "windows", True, "Recommended"), ), ( rx(r"-embed-win32\.zip$"), ("Windows embeddable package (32-bit)", "windows", False, ""), ), (rx(r"\.exe$"), ("Windows installer (32-bit)", "windows", False, "")), ( rx(r"-macos(x)?1[1-9](\.[0-9]*)?\.pkg$"), ( "macOS installer", "macos", True, macos_description(v), ), ), ( rx(r"-aarch64-linux-android.tar.gz$"), ("Android embeddable package (aarch64)", "android", False, ""), ), ( rx(r"-x86_64-linux-android.tar.gz$"), ("Android embeddable package (x86_64)", "android", False, ""), ), ( rx(r"-iOS-XCframework.tar.gz$"), ("iOS XCframework", "ios", False, ""), ), ] def slug_for(release: str) -> str: return base_version(release).replace(".", "") + ( "-" + release[len(base_version(release)) :] if release[len(base_version(release)) :] else "" ) def sigfile_for(release: str, rfile: str) -> str: return download_root + f"{release}/{rfile}.asc" def sha256sum_for(filename: str) -> str: """Returns SHA-256 checksum for filename.""" return hashlib.sha256(open(filename, "rb").read()).hexdigest() def filesize_for(filename: str) -> int: return path.getsize(filename) def make_slug(text: str) -> str: return re.sub("[^a-zA-Z0-9_-]", "", text.replace(" ", "-")) def base_version(release: str) -> str: m = tag_cre.match(release) assert m is not None, f"Invalid release: {release}" return ".".join(m.groups()[:3]) def base_version_tuple(release: str) -> tuple[int, int, int]: m = tag_cre.match(release) assert m is not None, f"Invalid release: {release}" return int(m.groups()[0]), int(m.groups()[1]), int(m.groups()[2]) def minor_version(release: str) -> str: m = tag_cre.match(release) assert m is not None, f"Invalid release: {release}" return ".".join(m.groups()[:2]) def minor_version_tuple(release: str) -> tuple[int, int]: m = tag_cre.match(release) assert m is not None, f"Invalid release: {release}" return int(m.groups()[0]), int(m.groups()[1]) def build_file_dict( ftp_root: str, release: str, rfile: str, rel_pk: int, file_desc: str, os_pk: int, add_download: bool, add_desc: str, ) -> dict[str, Any]: """Return a dictionary with all needed fields for a ReleaseFile object.""" filename = path.join(ftp_root, base_version(release), rfile) d = { "name": file_desc, "slug": slug_for(release) + "-" + make_slug(file_desc)[:40], "os": f"/api/v1/downloads/os/{os_pk}/", "release": f"/api/v1/downloads/release/{rel_pk}/", "description": add_desc, "is_source": os_pk == 3, "url": download_root + f"{base_version(release)}/{rfile}", "sha256_sum": sha256sum_for(filename), "filesize": filesize_for(filename), "download_button": add_download, } # Upload GPG signature if os.path.exists(filename + ".asc"): d["gpg_signature_file"] = sigfile_for(base_version(release), rfile) # Upload Sigstore signature if os.path.exists(filename + ".sig"): d["sigstore_signature_file"] = ( download_root + f"{base_version(release)}/{rfile}.sig" ) # Upload Sigstore certificate if os.path.exists(filename + ".crt"): d["sigstore_cert_file"] = download_root + f"{base_version(release)}/{rfile}.crt" # Upload Sigstore bundle if os.path.exists(filename + ".sigstore"): d["sigstore_bundle_file"] = ( download_root + f"{base_version(release)}/{rfile}.sigstore" ) # Upload SPDX SBOM file if os.path.exists(filename + ".spdx.json"): d["sbom_spdx2_file"] = ( download_root + f"{base_version(release)}/{rfile}.spdx.json" ) return d def list_files( ftp_root: str, release: str ) -> Generator[tuple[str, str, str, bool, str], None, None]: """List all of the release's download files.""" reldir = base_version(release) for rfile in sorted(os.listdir(path.join(ftp_root, reldir))): if not path.isfile(path.join(ftp_root, reldir, rfile)): continue if rfile.endswith((".asc", ".sig", ".crt", ".sigstore", ".spdx.json")): continue prefix, _, rest = rfile.partition("-") if prefix.lower() not in ("python", "windows"): print(f" File {reldir}/{rfile} has wrong prefix") continue if not rest.startswith((release + "-", release + ".")): print(f" File {reldir}/{rfile} has a different version") continue for rx, info in get_file_descriptions(release): if rx.search(rfile): yield (rfile, *info) break else: print(f" File {reldir}/{rfile} not recognized") continue def query_object(base_url: str, objtype: str, **params: Any) -> int: """Find an API object by query parameters.""" uri = base_url + f"downloads/{objtype}/" uri += "?" + "&".join(f"{k}={v}" for k, v in params.items()) resp = requests.get(uri, headers=headers) if resp.status_code != 200 or not json.loads(resp.text)["objects"]: raise RuntimeError(f"no object for {objtype} params={params!r}") obj = json.loads(resp.text)["objects"][0] return int(obj["resource_uri"].strip("/").split("/")[-1]) def post_object(base_url: str, objtype: str, datadict: dict[str, Any]) -> int: """Create a new API object.""" resp = requests.post( base_url + "downloads/" + objtype + "/", data=json.dumps(datadict), headers=headers, ) if resp.status_code != 201: try: info = json.loads(resp.text) print(info.get("error_message", "No error message.")) print(info.get("traceback", "")) except: # noqa: E722 pass print(f"Creating {objtype} failed: {resp.status_code}") return -1 newloc = resp.headers["Location"] pk = int(newloc.strip("/").split("/")[-1]) return pk def sign_release_files_with_sigstore( ftp_root: str, release: str, release_files: list[tuple[str, str, str, bool, str]] ) -> None: filenames = [ ftp_root + f"{base_version(release)}/{rfile}" for rfile, *_ in release_files ] def has_sigstore_signature(filename: str) -> bool: return os.path.exists(filename + ".sigstore") or ( os.path.exists(filename + ".sig") and os.path.exists(filename + ".crt") ) # Skip files that already have a signature (likely source distributions) unsigned_files = [ filename for filename in filenames if not has_sigstore_signature(filename) ] if unsigned_files: print("Signing release files with Sigstore") for filename in unsigned_files: cert_file = filename + ".crt" sig_file = filename + ".sig" bundle_file = filename + ".sigstore" run_cmd( [ "python3", "-m", "sigstore", "sign", "--oidc-disable-ambient-providers", "--signature", sig_file, "--certificate", cert_file, "--bundle", bundle_file, filename, ] ) run_cmd(["chmod", "644", sig_file]) run_cmd(["chmod", "644", cert_file]) run_cmd(["chmod", "644", bundle_file]) else: print("All release files already signed with Sigstore") # Verify all the files we expect to be signed with sigstore # against the documented release manager identities and providers. try: sigstore_identity_and_oidc_issuer = ( release_to_sigstore_identity_and_oidc_issuer[minor_version(release)] ) except KeyError: error(["No release manager defined for Python release " + release]) sigstore_identity, sigstore_oidc_issuer = sigstore_identity_and_oidc_issuer print("Verifying release files were signed correctly with Sigstore") sigstore_verify_argv = [ "python3", "-m", "sigstore", "verify", "identity", "--cert-identity", sigstore_identity, "--cert-oidc-issuer", sigstore_oidc_issuer, ] for filename in filenames: filename_crt = filename + ".crt" filename_sig = filename + ".sig" filename_sigstore = filename + ".sigstore" if os.path.exists(filename_sigstore): run_cmd( sigstore_verify_argv + ["--bundle", filename_sigstore, filename], stderr=subprocess.STDOUT, # Sigstore sends stderr on success. ) # We use an 'or' here to error out if one of the files is missing. if os.path.exists(filename_sig) or os.path.exists(filename_crt): run_cmd( sigstore_verify_argv + [ "--certificate", filename_crt, "--signature", filename_sig, filename, ], stderr=subprocess.STDOUT, # Sigstore sends stderr on success. ) def parse_args() -> argparse.Namespace: def ensure_trailing_slash(s: str) -> str: if not s.endswith("/"): s += "/" return s parser = argparse.ArgumentParser() parser.add_argument( "--base-url", metavar="URL", type=ensure_trailing_slash, default="https://www.python.org/api/v1/", help="API URL; defaults to %(default)s", ) parser.add_argument( "--ftp-root", metavar="DIR", type=ensure_trailing_slash, default="/srv/www.python.org/ftp/python/", help="FTP root; defaults to %(default)s", ) parser.add_argument( "release", help="Python version number, e.g. 3.14.0rc2", ) return parser.parse_args() def main() -> None: args = parse_args() rel = args.release print("Querying python.org for release", rel) rel_pk = query_object(args.base_url, "release", name="Python+" + rel) print("Found Release object: id =", rel_pk) release_files = list(list_files(args.ftp_root, rel)) sign_release_files_with_sigstore(args.ftp_root, rel, release_files) n = 0 file_dicts = {} for rfile, file_desc, os_slug, add_download, add_desc in release_files: if not os_slug: continue os_pk = query_object(args.base_url, "os", slug=os_slug) file_dict = build_file_dict( args.ftp_root, rel, rfile, rel_pk, file_desc, os_pk, add_download, add_desc ) key = file_dict["slug"] print("Creating ReleaseFile object for", rfile, key) if key in file_dicts: raise RuntimeError(f"duplicate slug generated: {key}") file_dicts[key] = file_dict print("Deleting previous release files") resp = requests.delete( args.base_url + f"downloads/release_file/?release={rel_pk}", headers=headers ) if resp.status_code != 204: raise RuntimeError(f"deleting previous releases failed: {resp.status_code}") for file_dict in file_dicts.values(): file_pk = post_object(args.base_url, "release_file", file_dict) if file_pk >= 0: print("Created as id =", file_pk) n += 1 print(f"Done - {n} files added") if __name__ == "__main__" and not sys.flags.interactive: main() ================================================ FILE: buildbotapi.py ================================================ import json from dataclasses import dataclass from typing import Any, cast from aiohttp.client import ClientSession JSON = dict[str, Any] @dataclass class Builder: builderid: int description: str | None name: str tags: list[str] def __init__(self, **kwargs: Any) -> None: self.__dict__.update(**kwargs) def __hash__(self) -> int: return hash(self.builderid) class BuildBotAPI: def __init__(self, session: ClientSession) -> None: self._session = session async def authenticate(self, token: str) -> None: await self._session.get( "https://buildbot.python.org/all/auth/login", params={"token": token} ) async def _fetch_text(self, url: str) -> str: async with self._session.get(url) as resp: return await resp.text() async def _fetch_json(self, url: str) -> JSON: return cast(JSON, json.loads(await self._fetch_text(url))) async def stable_builders(self, branch: str | None = None) -> dict[int, Builder]: stable_builders = { id: builder for (id, builder) in (await self.all_builders(branch=branch)).items() if "stable" in builder.tags } return stable_builders async def all_builders(self, branch: str | None = None) -> dict[int, Builder]: url = "https://buildbot.python.org/all/api/v2/builders" if branch is not None: url = f"{url}?tags__contains={branch}" _builders: dict[str, Any] = await self._fetch_json(url) builders = _builders["builders"] all_builders = { builder["builderid"]: Builder(**builder) for builder in builders } return all_builders async def is_builder_failing_currently(self, builder: Builder) -> bool: builds_: dict[str, Any] = await self._fetch_json( f"https://buildbot.python.org/all/api/v2/builds?complete__eq=true" f"&&builderid__eq={builder.builderid}&&order=-complete_at" f"&&limit=1" ) builds = builds_["builds"] if not builds: return False (build,) = builds if build["results"] == 2: return True return False ================================================ FILE: dev-requirements.in ================================================ pyfakefs pytest pytest-aiohttp pytest-cov pytest-mock ================================================ FILE: dev-requirements.txt ================================================ # # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --generate-hashes --output-file=dev-requirements.txt dev-requirements.in # aiohappyeyeballs==2.6.1 \ --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 # via aiohttp aiohttp==3.13.3 \ --hash=sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf \ --hash=sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c \ --hash=sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c \ --hash=sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423 \ --hash=sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f \ --hash=sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40 \ --hash=sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2 \ --hash=sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf \ --hash=sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821 \ --hash=sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64 \ --hash=sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7 \ --hash=sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998 \ --hash=sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d \ --hash=sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea \ --hash=sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463 \ --hash=sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80 \ --hash=sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4 \ --hash=sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767 \ --hash=sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43 \ --hash=sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592 \ --hash=sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a \ --hash=sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e \ --hash=sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687 \ --hash=sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8 \ --hash=sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261 \ --hash=sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd \ --hash=sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a \ --hash=sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4 \ --hash=sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587 \ --hash=sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91 \ --hash=sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f \ --hash=sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3 \ --hash=sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344 \ --hash=sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6 \ --hash=sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3 \ --hash=sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce \ --hash=sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808 \ --hash=sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1 \ --hash=sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29 \ --hash=sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3 \ --hash=sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b \ --hash=sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51 \ --hash=sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c \ --hash=sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926 \ --hash=sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64 \ --hash=sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f \ --hash=sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b \ --hash=sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e \ --hash=sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440 \ --hash=sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6 \ --hash=sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3 \ --hash=sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d \ --hash=sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415 \ --hash=sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279 \ --hash=sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce \ --hash=sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603 \ --hash=sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0 \ --hash=sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c \ --hash=sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf \ --hash=sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591 \ --hash=sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540 \ --hash=sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e \ --hash=sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26 \ --hash=sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a \ --hash=sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845 \ --hash=sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a \ --hash=sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9 \ --hash=sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6 \ --hash=sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba \ --hash=sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df \ --hash=sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43 \ --hash=sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679 \ --hash=sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7 \ --hash=sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7 \ --hash=sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc \ --hash=sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29 \ --hash=sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02 \ --hash=sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984 \ --hash=sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1 \ --hash=sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6 \ --hash=sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632 \ --hash=sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56 \ --hash=sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239 \ --hash=sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168 \ --hash=sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88 \ --hash=sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc \ --hash=sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11 \ --hash=sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046 \ --hash=sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0 \ --hash=sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3 \ --hash=sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877 \ --hash=sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1 \ --hash=sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c \ --hash=sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25 \ --hash=sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704 \ --hash=sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a \ --hash=sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033 \ --hash=sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1 \ --hash=sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29 \ --hash=sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d \ --hash=sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160 \ --hash=sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d \ --hash=sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f \ --hash=sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f \ --hash=sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538 \ --hash=sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29 \ --hash=sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7 \ --hash=sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72 \ --hash=sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af \ --hash=sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455 \ --hash=sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57 \ --hash=sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558 \ --hash=sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c \ --hash=sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808 \ --hash=sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7 \ --hash=sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0 \ --hash=sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3 \ --hash=sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730 \ --hash=sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa \ --hash=sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940 # via pytest-aiohttp aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 # via aiohttp attrs==24.3.0 \ --hash=sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff \ --hash=sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308 # via aiohttp coverage[toml]==7.10.7 \ --hash=sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9 \ --hash=sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880 \ --hash=sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999 \ --hash=sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1 \ --hash=sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13 \ --hash=sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b \ --hash=sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82 \ --hash=sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973 \ --hash=sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f \ --hash=sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681 \ --hash=sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0 \ --hash=sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541 \ --hash=sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32 \ --hash=sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17 \ --hash=sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a \ --hash=sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40 \ --hash=sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd \ --hash=sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6 \ --hash=sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7 \ --hash=sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb \ --hash=sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f \ --hash=sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d \ --hash=sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe \ --hash=sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c \ --hash=sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807 \ --hash=sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab \ --hash=sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2 \ --hash=sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546 \ --hash=sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e \ --hash=sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65 \ --hash=sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396 \ --hash=sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431 \ --hash=sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb \ --hash=sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699 \ --hash=sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0 \ --hash=sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f \ --hash=sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a \ --hash=sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235 \ --hash=sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911 \ --hash=sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23 \ --hash=sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87 \ --hash=sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699 \ --hash=sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a \ --hash=sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b \ --hash=sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256 \ --hash=sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a \ --hash=sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417 \ --hash=sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0 \ --hash=sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a \ --hash=sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360 \ --hash=sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0 \ --hash=sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b \ --hash=sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb \ --hash=sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2 \ --hash=sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d \ --hash=sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a \ --hash=sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e \ --hash=sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69 \ --hash=sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14 \ --hash=sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d \ --hash=sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f \ --hash=sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2 \ --hash=sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c \ --hash=sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0 \ --hash=sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399 \ --hash=sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59 \ --hash=sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63 \ --hash=sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b \ --hash=sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2 \ --hash=sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e \ --hash=sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0 \ --hash=sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520 \ --hash=sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df \ --hash=sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c \ --hash=sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b \ --hash=sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2 \ --hash=sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f \ --hash=sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61 \ --hash=sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a \ --hash=sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59 \ --hash=sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c \ --hash=sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf \ --hash=sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07 \ --hash=sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6 \ --hash=sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e \ --hash=sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594 \ --hash=sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49 \ --hash=sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843 \ --hash=sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14 \ --hash=sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3 \ --hash=sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1 \ --hash=sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698 \ --hash=sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15 \ --hash=sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d \ --hash=sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5 \ --hash=sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e \ --hash=sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0 \ --hash=sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b \ --hash=sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239 \ --hash=sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba \ --hash=sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4 \ --hash=sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260 \ --hash=sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a \ --hash=sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3 # via pytest-cov frozenlist==1.5.0 \ --hash=sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e \ --hash=sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf \ --hash=sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6 \ --hash=sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a \ --hash=sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d \ --hash=sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f \ --hash=sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28 \ --hash=sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b \ --hash=sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9 \ --hash=sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2 \ --hash=sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec \ --hash=sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2 \ --hash=sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c \ --hash=sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336 \ --hash=sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4 \ --hash=sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d \ --hash=sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b \ --hash=sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c \ --hash=sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10 \ --hash=sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08 \ --hash=sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942 \ --hash=sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8 \ --hash=sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f \ --hash=sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10 \ --hash=sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5 \ --hash=sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6 \ --hash=sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21 \ --hash=sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c \ --hash=sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d \ --hash=sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923 \ --hash=sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608 \ --hash=sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de \ --hash=sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17 \ --hash=sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0 \ --hash=sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f \ --hash=sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641 \ --hash=sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c \ --hash=sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a \ --hash=sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0 \ --hash=sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9 \ --hash=sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab \ --hash=sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f \ --hash=sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3 \ --hash=sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a \ --hash=sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784 \ --hash=sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604 \ --hash=sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d \ --hash=sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5 \ --hash=sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03 \ --hash=sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e \ --hash=sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953 \ --hash=sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee \ --hash=sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d \ --hash=sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817 \ --hash=sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3 \ --hash=sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039 \ --hash=sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f \ --hash=sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9 \ --hash=sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf \ --hash=sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76 \ --hash=sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba \ --hash=sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171 \ --hash=sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb \ --hash=sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439 \ --hash=sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631 \ --hash=sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972 \ --hash=sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d \ --hash=sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869 \ --hash=sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9 \ --hash=sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411 \ --hash=sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723 \ --hash=sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2 \ --hash=sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b \ --hash=sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99 \ --hash=sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e \ --hash=sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840 \ --hash=sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3 \ --hash=sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb \ --hash=sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3 \ --hash=sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0 \ --hash=sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca \ --hash=sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45 \ --hash=sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e \ --hash=sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f \ --hash=sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5 \ --hash=sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307 \ --hash=sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e \ --hash=sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2 \ --hash=sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778 \ --hash=sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a \ --hash=sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30 \ --hash=sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a # via # aiohttp # aiosignal idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 # via yarl iniconfig==2.0.0 \ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 # via pytest multidict==6.1.0 \ --hash=sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f \ --hash=sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056 \ --hash=sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761 \ --hash=sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3 \ --hash=sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b \ --hash=sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6 \ --hash=sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748 \ --hash=sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966 \ --hash=sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f \ --hash=sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1 \ --hash=sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6 \ --hash=sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada \ --hash=sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305 \ --hash=sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2 \ --hash=sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d \ --hash=sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a \ --hash=sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef \ --hash=sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c \ --hash=sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb \ --hash=sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60 \ --hash=sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6 \ --hash=sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4 \ --hash=sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478 \ --hash=sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81 \ --hash=sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7 \ --hash=sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56 \ --hash=sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3 \ --hash=sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6 \ --hash=sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30 \ --hash=sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb \ --hash=sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506 \ --hash=sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0 \ --hash=sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925 \ --hash=sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c \ --hash=sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6 \ --hash=sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e \ --hash=sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95 \ --hash=sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2 \ --hash=sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133 \ --hash=sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2 \ --hash=sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa \ --hash=sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3 \ --hash=sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3 \ --hash=sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436 \ --hash=sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657 \ --hash=sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581 \ --hash=sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492 \ --hash=sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43 \ --hash=sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2 \ --hash=sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2 \ --hash=sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926 \ --hash=sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057 \ --hash=sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc \ --hash=sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80 \ --hash=sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255 \ --hash=sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1 \ --hash=sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972 \ --hash=sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53 \ --hash=sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1 \ --hash=sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423 \ --hash=sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a \ --hash=sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160 \ --hash=sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c \ --hash=sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd \ --hash=sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa \ --hash=sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5 \ --hash=sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b \ --hash=sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa \ --hash=sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef \ --hash=sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44 \ --hash=sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4 \ --hash=sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156 \ --hash=sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753 \ --hash=sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28 \ --hash=sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d \ --hash=sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a \ --hash=sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304 \ --hash=sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008 \ --hash=sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429 \ --hash=sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72 \ --hash=sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399 \ --hash=sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3 \ --hash=sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392 \ --hash=sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167 \ --hash=sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c \ --hash=sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774 \ --hash=sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351 \ --hash=sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76 \ --hash=sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875 \ --hash=sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd \ --hash=sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28 \ --hash=sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db # via # aiohttp # yarl packaging==23.2 \ --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 # via pytest pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via # pytest # pytest-cov propcache==0.2.1 \ --hash=sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4 \ --hash=sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4 \ --hash=sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a \ --hash=sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f \ --hash=sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9 \ --hash=sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d \ --hash=sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e \ --hash=sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6 \ --hash=sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf \ --hash=sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034 \ --hash=sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d \ --hash=sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16 \ --hash=sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30 \ --hash=sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba \ --hash=sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95 \ --hash=sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d \ --hash=sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae \ --hash=sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348 \ --hash=sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2 \ --hash=sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64 \ --hash=sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce \ --hash=sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54 \ --hash=sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629 \ --hash=sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54 \ --hash=sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1 \ --hash=sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b \ --hash=sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf \ --hash=sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b \ --hash=sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587 \ --hash=sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097 \ --hash=sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea \ --hash=sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24 \ --hash=sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7 \ --hash=sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541 \ --hash=sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6 \ --hash=sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634 \ --hash=sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3 \ --hash=sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d \ --hash=sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034 \ --hash=sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465 \ --hash=sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2 \ --hash=sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf \ --hash=sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1 \ --hash=sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04 \ --hash=sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5 \ --hash=sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583 \ --hash=sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb \ --hash=sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b \ --hash=sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c \ --hash=sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958 \ --hash=sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc \ --hash=sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4 \ --hash=sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82 \ --hash=sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e \ --hash=sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce \ --hash=sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9 \ --hash=sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518 \ --hash=sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536 \ --hash=sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505 \ --hash=sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052 \ --hash=sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff \ --hash=sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1 \ --hash=sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f \ --hash=sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681 \ --hash=sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347 \ --hash=sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af \ --hash=sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246 \ --hash=sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787 \ --hash=sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0 \ --hash=sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f \ --hash=sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439 \ --hash=sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3 \ --hash=sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6 \ --hash=sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca \ --hash=sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec \ --hash=sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d \ --hash=sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3 \ --hash=sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16 \ --hash=sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717 \ --hash=sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6 \ --hash=sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd \ --hash=sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212 # via # aiohttp # yarl pyfakefs==6.2.0 \ --hash=sha256:0968a49db692694ffed420e54a9f1cbae4636637b880e8ab09c8ccc0f11bd7ae \ --hash=sha256:e59a36db447bf509ce9c97ab3d1510c08cc51895c5311325a560a5e5b5dc1940 # via -r dev-requirements.in pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via pytest pytest==9.0.3 \ --hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \ --hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c # via # -r dev-requirements.in # pytest-aiohttp # pytest-asyncio # pytest-cov # pytest-mock pytest-aiohttp==1.1.0 \ --hash=sha256:147de8cb164f3fc9d7196967f109ab3c0b93ea3463ab50631e56438eab7b5adc \ --hash=sha256:f39a11693a0dce08dd6c542d241e199dd8047a6e6596b2bcfa60d373f143456d # via -r dev-requirements.in pytest-asyncio==1.3.0 \ --hash=sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5 \ --hash=sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5 # via pytest-aiohttp pytest-cov==7.1.0 \ --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 # via -r dev-requirements.in pytest-mock==3.15.1 \ --hash=sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d \ --hash=sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f # via -r dev-requirements.in typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via # aiosignal # pytest-asyncio yarl==1.18.3 \ --hash=sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba \ --hash=sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193 \ --hash=sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318 \ --hash=sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee \ --hash=sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e \ --hash=sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1 \ --hash=sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a \ --hash=sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186 \ --hash=sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1 \ --hash=sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50 \ --hash=sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640 \ --hash=sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb \ --hash=sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8 \ --hash=sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc \ --hash=sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5 \ --hash=sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58 \ --hash=sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2 \ --hash=sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393 \ --hash=sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24 \ --hash=sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b \ --hash=sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910 \ --hash=sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c \ --hash=sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272 \ --hash=sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed \ --hash=sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1 \ --hash=sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04 \ --hash=sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d \ --hash=sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5 \ --hash=sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d \ --hash=sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889 \ --hash=sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae \ --hash=sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b \ --hash=sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c \ --hash=sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576 \ --hash=sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34 \ --hash=sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477 \ --hash=sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990 \ --hash=sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2 \ --hash=sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512 \ --hash=sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069 \ --hash=sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a \ --hash=sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6 \ --hash=sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0 \ --hash=sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8 \ --hash=sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb \ --hash=sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa \ --hash=sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8 \ --hash=sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e \ --hash=sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e \ --hash=sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985 \ --hash=sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8 \ --hash=sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1 \ --hash=sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5 \ --hash=sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690 \ --hash=sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10 \ --hash=sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789 \ --hash=sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b \ --hash=sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca \ --hash=sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e \ --hash=sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5 \ --hash=sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59 \ --hash=sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9 \ --hash=sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8 \ --hash=sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db \ --hash=sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde \ --hash=sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7 \ --hash=sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb \ --hash=sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3 \ --hash=sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6 \ --hash=sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285 \ --hash=sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb \ --hash=sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8 \ --hash=sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482 \ --hash=sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd \ --hash=sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75 \ --hash=sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760 \ --hash=sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782 \ --hash=sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53 \ --hash=sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2 \ --hash=sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1 \ --hash=sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719 \ --hash=sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62 # via aiohttp ================================================ FILE: mypy-requirements.txt ================================================ aiohttp==3.13.5 alive_progress>=3.3.0 mypy==1.20.2 pyfakefs pytest pytest-mock python-gnupg # untyped :( sigstore==3.6.7 types-paramiko types-requests ================================================ FILE: pyproject.toml ================================================ [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" [tool.mypy] python_version = "3.12" pretty = true strict = true # Extra checks that aren't included in --strict enable_error_code = "ignore-without-code,redundant-expr,truthy-iterable" extra_checks = true warn_unreachable = true exclude = [ "^tests/test_release_tag.py$", "^tests/test_run_release.py$", "^tests/test_sbom.py$", "^windows-release/merge-and-upload.py$", "^windows-release/purge.py$", ] ================================================ FILE: release.py ================================================ #!/usr/bin/env python3 """An assistant for making Python releases. Original code by Benjamin Peterson Additions by Barry Warsaw, Georg Brandl and Benjamin Peterson """ from __future__ import annotations import datetime import glob import hashlib import json import optparse import os import re import readline # noqa: F401 import shutil import subprocess import sys import tempfile import urllib.request from collections.abc import Callable, Generator, Sequence from contextlib import contextmanager from dataclasses import dataclass from functools import cache from pathlib import Path from typing import ( Any, Literal, Protocol, Self, overload, ) from urllib.request import urlopen COMMASPACE = ", " SPACE = " " tag_cre = re.compile(r"(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:([ab]|rc)(\d+))?$") class ReleaseShelf(Protocol): def close(self) -> None: ... @overload def get(self, key: Literal["finished"], default: bool | None = None) -> bool: ... @overload def get( self, key: Literal["completed_tasks"], default: list[Task] | None = None ) -> list[Task]: ... @overload def get(self, key: Literal["gpg_key"], default: str | None = None) -> str: ... @overload def get(self, key: Literal["git_repo"], default: Path | None = None) -> Path: ... @overload def get(self, key: Literal["auth_info"], default: str | None = None) -> str: ... @overload def get(self, key: Literal["ssh_user"], default: str | None = None) -> str: ... @overload def get( self, key: Literal["ssh_key"], default: str | None = None ) -> str | None: ... @overload def get(self, key: Literal["sign_gpg"], default: bool | None = None) -> bool: ... @overload def get( self, key: Literal["security_release"], default: bool | None = None ) -> bool: ... @overload def get(self, key: Literal["release"], default: Tag | None = None) -> Tag: ... @overload def __getitem__(self, key: Literal["finished"]) -> bool: ... @overload def __getitem__(self, key: Literal["completed_tasks"]) -> list[Task]: ... @overload def __getitem__(self, key: Literal["gpg_key"]) -> str: ... @overload def __getitem__(self, key: Literal["git_repo"]) -> Path: ... @overload def __getitem__(self, key: Literal["auth_info"]) -> str: ... @overload def __getitem__(self, key: Literal["ssh_user"]) -> str: ... @overload def __getitem__(self, key: Literal["ssh_key"]) -> str | None: ... @overload def __getitem__(self, key: Literal["sign_gpg"]) -> bool: ... @overload def __getitem__(self, key: Literal["security_release"]) -> bool: ... @overload def __getitem__(self, key: Literal["release"]) -> Tag: ... @overload def __setitem__(self, key: Literal["finished"], value: bool) -> None: ... @overload def __setitem__( self, key: Literal["completed_tasks"], value: list[Task] ) -> None: ... @overload def __setitem__(self, key: Literal["gpg_key"], value: str) -> None: ... @overload def __setitem__(self, key: Literal["git_repo"], value: Path) -> None: ... @overload def __setitem__(self, key: Literal["auth_info"], value: str) -> None: ... @overload def __setitem__(self, key: Literal["ssh_user"], value: str) -> None: ... @overload def __setitem__(self, key: Literal["ssh_key"], value: str | None) -> None: ... @overload def __setitem__(self, key: Literal["sign_gpg"], value: bool) -> None: ... @overload def __setitem__(self, key: Literal["security_release"], value: bool) -> None: ... @overload def __setitem__(self, key: Literal["release"], value: Tag) -> None: ... @dataclass class Task: function: Callable[[ReleaseShelf], None] description: str def __call__(self, db: ReleaseShelf) -> Any: return getattr(self, "function")(db) class Tag: def __init__(self, tag_name: str) -> None: # if tag is ".", use current directory name as tag # e.g. if current directory name is "3.4.6", # "release.py --bump 3.4.6" and "release.py --bump ." are the same if tag_name == ".": tag_name = os.path.basename(os.getcwd()) result = tag_cre.match(tag_name) if result is None: error(f"tag {tag_name} is not valid") assert result is not None data = list(result.groups()) if data[3] is None: # A final release. self.is_final = True data[3] = "f" else: self.is_final = False # For everything else, None means 0. for i, thing in enumerate(data): if thing is None: data[i] = 0 self.major = int(data[0]) self.minor = int(data[1]) self.patch = int(data[2]) self.level = data[3] self.serial = int(data[4]) # This has the effect of normalizing the version. self.text = self.normalized() if self.level != "f": assert self.level is not None self.text += self.level + str(self.serial) self.basic_version = f"{self.major}.{self.minor}" def __str__(self) -> str: return self.text def normalized(self) -> str: return f"{self.major}.{self.minor}.{self.patch}" @property def branch(self) -> str: if self.is_alpha_release or self.is_feature_freeze_release: return "main" return f"{self.major}.{self.minor}" @property def is_alpha_release(self) -> bool: return self.level == "a" @property def is_release_candidate(self) -> bool: return self.level == "rc" @property def is_feature_freeze_release(self) -> bool: return self.level == "b" and self.serial == 1 @property def is_security_release(self) -> bool: url = "https://peps.python.org/api/release-cycle.json" with urlopen(url) as response: data = json.loads(response.read()) return str(data[self.basic_version]["status"]) == "security" @property def nickname(self) -> str: return self.text.replace(".", "") @property def gitname(self) -> str: return "v" + self.text @property def long_name(self) -> str: if self.is_final: return self.text level = { "a": "alpha", "b": "beta", "rc": "release candidate", }[self.level] return f"{self.normalized()} {level} {self.serial}" def next_minor_release(self) -> Self: return self.__class__(f"{self.major}.{int(self.minor)+1}.0a0") def as_tuple(self) -> tuple[int, int, int, str, int]: assert isinstance(self.level, str) return self.major, self.minor, self.patch, self.level, self.serial @property def committed_at(self) -> datetime.datetime: # Fetch the epoch of the tagged commit for build reproducibility. proc = subprocess.run( ["git", "log", self.gitname, "-1", "--pretty=%ct"], stdout=subprocess.PIPE ) if proc.returncode != 0: error(f"Couldn't fetch the epoch of tag {self.gitname}") return datetime.datetime.fromtimestamp( int(proc.stdout.decode().strip()), tz=datetime.timezone.utc ) @property def includes_docs(self) -> bool: """True if docs should be included in the release""" return self.is_final or self.is_release_candidate @property def doc_version(self) -> str: """Text used for notes in docs like 'Added in x.y'""" # - ignore levels (alpha/beta/rc are preparation for the full release) # - use just X.Y for patch 0 if self.patch == 0: return f"{self.major}.{self.minor}" else: return f"{self.major}.{self.minor}.{self.patch}" def error(*msgs: str) -> None: print("**ERROR**", file=sys.stderr) for msg in msgs: print(msg, file=sys.stderr) sys.exit(1) def run_cmd( cmd: Sequence[str] | str, silent: bool = False, shell: bool = False, **kwargs: Any ) -> None: if shell: cmd = SPACE.join(cmd) if not silent: print(f"Executing {cmd}") try: if silent: subprocess.check_call(cmd, shell=shell, stdout=subprocess.PIPE, **kwargs) else: subprocess.check_call(cmd, shell=shell, **kwargs) except subprocess.CalledProcessError: error(f"{cmd} failed") def ask_question(question: str) -> bool: answer = "" print(question) while answer not in ("yes", "no"): answer = input("Enter yes or no: ") if answer == "yes": return True elif answer == "no": return False else: print("Please enter yes or no.") return True readme_re = re.compile(r"This is Python version 3\.\d").match def chdir_to_repo_root() -> str: # find the root of the local CPython repo # note that we can't ask git, because we might # be in an exported directory tree! # we intentionally start in a (probably nonexistent) subtree # the first thing the while loop does is .., basically path = os.path.abspath("garglemox") while True: next_path = os.path.dirname(path) if next_path == path: sys.exit("You're not inside a CPython repo right now!") path = next_path os.chdir(path) def test_first_line( filename: str, test: Callable[[str], object], ) -> bool: if not os.path.exists(filename): return False with open(filename) as f: lines = f.read().split("\n") if not (lines and test(lines[0])): return False return True if not test_first_line("README.rst", readme_re): continue if not test_first_line("LICENSE", "A. HISTORY OF THE SOFTWARE".__eq__): continue if not os.path.exists("Include/Python.h"): continue if not os.path.exists("Python/ceval.c"): continue break root = path return root def get_output(args: list[str]) -> bytes: return subprocess.check_output(args) def check_env() -> None: if "EDITOR" not in os.environ: error("editor not detected.", "Please set your EDITOR environment variable") if not os.path.exists(".git"): error("CWD is not a git clone") def get_arg_parser() -> optparse.OptionParser: usage = "%prog [options] tagname" p = optparse.OptionParser(usage=usage) p.add_option( "-b", "--bump", default=False, action="store_true", help="bump the revision number in important files", ) p.add_option( "-e", "--export", default=False, action="store_true", help="Export the git tag to a tarball and build docs", ) p.add_option( "-u", "--upload", metavar="username", help="Upload the tarballs and docs to dinsdale", ) p.add_option( "-m", "--branch", default=False, action="store_true", help="Create a maintenance branch to go along with the release", ) p.add_option( "-t", "--tag", default=False, action="store_true", help="Tag the release in Subversion", ) p.add_option( "-d", "--done", default=False, action="store_true", help="Do post-release cleanups (i.e. you're done!)", ) p.add_option( "--skip-docs", default=False, action="store_true", help="Skip building the documentation during export", ) return p def constant_replace( filename: str, updated_constants: str, comment_start: str = "/*", comment_end: str = "*/", ) -> None: """Inserts in between --start constant-- and --end constant-- in a file""" start_tag = comment_start + "--start constants--" + comment_end end_tag = comment_start + "--end constants--" + comment_end with open(filename, encoding="ascii") as infile, open( filename + ".new", "w", encoding="ascii" ) as outfile: found_constants = False waiting_for_end = False for line in infile: if line[:-1] == start_tag: print(start_tag, file=outfile) print(updated_constants, file=outfile) print(end_tag, file=outfile) waiting_for_end = True found_constants = True elif line[:-1] == end_tag: waiting_for_end = False elif waiting_for_end: pass else: outfile.write(line) if not found_constants: error(f"Constant section delimiters not found: {filename}") os.rename(filename + ".new", filename) def tweak_patchlevel( tag: Tag, filename: str = "Include/patchlevel.h", done: bool = False ) -> None: print(f"Updating {filename}...", end=" ") template = ''' #define PY_MAJOR_VERSION\t{tag.major} #define PY_MINOR_VERSION\t{tag.minor} #define PY_MICRO_VERSION\t{tag.patch} #define PY_RELEASE_LEVEL\t{level_def} #define PY_RELEASE_SERIAL\t{tag.serial} /* Version as a string */ #define PY_VERSION \t\"{tag.text}{plus}"'''.strip() assert isinstance(tag.level, str) level_def = { "a": "PY_RELEASE_LEVEL_ALPHA", "b": "PY_RELEASE_LEVEL_BETA", "rc": "PY_RELEASE_LEVEL_GAMMA", "f": "PY_RELEASE_LEVEL_FINAL", }[tag.level] new_constants = template.format( tag=tag, level_def=level_def, plus=done and "+" or "" ) if tag.as_tuple() >= (3, 7, 0, "a", 3): new_constants = new_constants.expandtabs() constant_replace(filename, new_constants) print("done") @cache def get_pep_number(version: str) -> str: """Fetch PEP number for a Python version from peps.python.org. Returns the PEP number as a string, or "TODO" if not found. """ url = "https://peps.python.org/api/release-cycle.json" with urllib.request.urlopen(url, timeout=10) as response: data = json.loads(response.read().decode()) if version in data: pep = data[version].get("pep") if pep: return str(pep) return "TODO" def tweak_readme(tag: Tag, filename: str = "README.rst") -> None: print(f"Updating {filename}...", end=" ") readme = Path(filename) # Update first line: "This is Python version X.Y.Z {release_level} N" # and update length of underline in second line to match. lines = readme.read_text().split("\n") this_is = f"This is Python version {tag.long_name}" underline = "=" * len(this_is) lines[0] = this_is lines[1] = underline content = "\n".join(lines) DOCS_URL = r"https://docs\.python\.org/" X_Y = r"\d+\.\d+" # Replace in: 3.14 `_ content = re.sub( rf"{X_Y} (<{DOCS_URL}){X_Y}(/whatsnew/){X_Y}(\.html>`_)", rf"{tag.basic_version} \g<1>{tag.basic_version}\g<2>{tag.basic_version}\g<3>", content, ) # Replace in: `Documentation for Python 3.14 `_ content = re.sub( rf"(`Documentation for Python ){X_Y}( <{DOCS_URL}){X_Y}(/>`_)", rf"\g<1>{tag.basic_version}\g<2>{tag.basic_version}\g<3>", content, ) # Get PEP number for this version pep_number = get_pep_number(tag.basic_version) pep_padded = pep_number.zfill(4) if pep_number != "TODO" else "TODO" # Replace in: `PEP 745 `__ for Python 3.14 content = re.sub( rf"(`PEP )\d+( `__ for Python ){X_Y}", rf"\g<1>{pep_number}\g<2>{pep_padded}\g<3>{tag.basic_version}", content, ) readme.write_text(content) print("done") def bump(tag: Tag) -> None: print(f"Bumping version to {tag}") tweak_patchlevel(tag) tweak_readme(tag) extra_work = False other_files = [] if tag.patch == 0 and tag.level == "a" and tag.serial == 0: extra_work = True other_files += [ "configure.ac", "Doc/tutorial/interpreter.rst", "Doc/tutorial/stdlib.rst", "Doc/tutorial/stdlib2.rst", "PC/pyconfig.h.in", "PCbuild/rt.bat", ".github/ISSUE_TEMPLATE/bug.yml", ".github/ISSUE_TEMPLATE/crash.yml", ] print("\nManual editing time...") for filename in other_files: if os.path.exists(filename): print(f"Edit {filename}") manual_edit(filename) else: print(f"Skipping {filename}") print("Bumped revision") if extra_work: print("configure.ac has changed; re-run autotools!") print("Please commit and use --tag") def manual_edit(filename: str) -> None: editor = os.environ["EDITOR"].split() run_cmd([*editor, filename]) @contextmanager def pushd(new: str) -> Generator[None, None, None]: print(f"chdir'ing to {new}") old = os.getcwd() os.chdir(new) try: yield finally: os.chdir(old) def make_dist(name: str) -> None: try: os.mkdir(name) except OSError: if os.path.isdir(name): print(f"WARNING: dist dir {name} already exists", file=sys.stderr) else: error(f"{name}/ is not a directory") else: print(f"created dist directory {name}") def tarball(source: str, clamp_mtime: str) -> None: """Build tarballs for a directory.""" print("Making .tgz") base = os.path.basename(source) tgz = os.path.join("src", base + ".tgz") xz = os.path.join("src", base + ".tar.xz") # Recommended options for creating reproducible tarballs from: # https://www.gnu.org/software/tar/manual/html_node/Reproducibility.html#Reproducibility # and https://reproducible-builds.org/docs/archives/ repro_options = [ # Sorts the entries in the tarball by name. "--sort=name", # Sets a maximum 'modified time' of entries in tarball. f"--mtime={clamp_mtime}", "--clamp-mtime", # Sets the owner uid and gid to 0. "--owner=0", "--group=0", "--numeric-owner", # Omits process ID, file access, and status change times. "--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime", # Omit irrelevant info about file permissions. "--mode=go+u,go-w", ] run_cmd( [ "tar", "cf", tgz, *repro_options, "--use-compress-program", "gzip --no-name -9", source, ] ) print("Making .tar.xz") run_cmd(["tar", "cJf", xz, *repro_options, source]) print("Calculating SHA-256 sums") checksum_tgz = hashlib.sha256() with open(tgz, "rb") as data: checksum_tgz.update(data.read()) checksum_xz = hashlib.sha256() with open(xz, "rb") as data: checksum_xz.update(data.read()) print(f" {checksum_tgz.hexdigest()} {os.path.getsize(tgz):8} {tgz}") print(f" {checksum_xz.hexdigest()} {os.path.getsize(xz):8} {xz}") def export(tag: Tag, silent: bool = False, skip_docs: bool = False) -> None: make_dist(tag.text) print("Exporting tag:", tag.text) archivename = f"Python-{tag.text}" # I have not figured out how to get git to directly produce an # archive directory like hg can, so use git to produce a temporary # tarball then expand it with tar. archivetempfile = f"{archivename}.tar" run_cmd( [ "git", "archive", "--format=tar", f"--prefix={archivename}/", "-o", archivetempfile, tag.gitname, ], silent=silent, ) with pushd(tag.text): archivetempfile = f"../{archivetempfile}" run_cmd(["tar", "-xf", archivetempfile], silent=silent) os.unlink(archivetempfile) with pushd(archivename): # Touch a few files that get generated so they're up-to-date in # the tarball. # # Note, with the demise of "make touch" and the hg touch # extension, touches should not be needed anymore, # but keep it for now as a reminder. maybe_touchables = [ "Include/internal/pycore_ast.h", "Include/internal/pycore_ast_state.h", "Python/Python-ast.c", "Python/opcode_targets.h", ] touchables = [file for file in maybe_touchables if os.path.exists(file)] print( "Touching:", COMMASPACE.join(name.rsplit("/", 1)[-1] for name in touchables), ) for name in touchables: os.utime(name, None) # build docs *before* we do "blurb export" # because docs now depend on Misc/NEWS.d # and we remove Misc/NEWS.d as part of cleanup for export # # If --skip-docs is provided we don't build and docs. if not skip_docs and (tag.is_final or tag.level == "rc"): docdist = build_docs() print("Using blurb to build Misc/NEWS") run_cmd(["blurb", "merge"], silent=silent) # Remove files we don't want to ship in tarballs. print("Removing VCS .*ignore, .git*, Misc/NEWS.d, et al") for name in (".gitattributes", ".gitignore"): try: os.unlink(name) except OSError: pass # Remove directories we don't want to ship in tarballs. run_cmd(["blurb", "export"], silent=silent) for name in (".azure-pipelines", ".git", ".github", "Misc/mypy"): shutil.rmtree(name, ignore_errors=True) if not skip_docs and (tag.is_final or tag.level == "rc"): shutil.copytree(docdist, "docs") with pushd(os.path.join(archivename, "Doc")): print("Removing doc build artifacts") shutil.rmtree("venv", ignore_errors=True) shutil.rmtree("build", ignore_errors=True) shutil.rmtree("dist", ignore_errors=True) with pushd(archivename): print("Zapping pycs") run_cmd( [ "find", ".", "-depth", "-name", "__pycache__", "-exec", "rm", "-rf", "{}", ";", ], silent=silent, ) run_cmd( ["find", ".", "-name", "*.py[co]", "-exec", "rm", "-f", "{}", ";"], silent=silent, ) os.mkdir("src") tarball(archivename, tag.committed_at.strftime("%Y-%m-%d %H:%M:%SZ")) print() print(f"**Now extract the archives in {tag.text}/src and run the tests**") print("**You may also want to run make install and re-test**") def build_docs() -> str: """Build and tarball the documentation""" print("Building docs") with tempfile.TemporaryDirectory() as venv: run_cmd(["python3", "-m", "venv", venv]) pip = os.path.join(venv, "bin", "pip") run_cmd([pip, "install", "-r", "Doc/requirements.txt"]) sphinx_build = os.path.join(venv, "bin", "sphinx-build") blurb = os.path.join(venv, "bin", "blurb") docs_env = { **os.environ, "BLURB": blurb, "SPHINXBUILD": sphinx_build, "SPHINXOPTS": "-j10", } with pushd("Doc"): run_cmd(("make", "dist-epub"), env=docs_env) run_cmd(("make", "dist-html"), env=docs_env) run_cmd(("make", "dist-texinfo"), env=docs_env) run_cmd(("make", "dist-text"), env=docs_env) return os.path.abspath("dist") def upload(tag: Tag, username: str) -> None: """scp everything to dinsdale""" address = f'"{username}@dinsdale.python.org:' def scp(from_loc: str, to_loc: str) -> None: run_cmd(["scp", from_loc, address + to_loc]) with pushd(tag.text): print("Uploading source tarballs") scp("src", f"/data/python-releases/{tag.nickname}") print("Upload doc tarballs") scp("docs", f"/data/python-releases/doc/{tag.nickname}") print( "* Now change the permissions on the tarballs so they are " "writable by the webmaster group. *" ) def make_tag(tag: Tag, *, sign_gpg: bool = True) -> bool: # make sure we've run blurb export good_files = glob.glob("Misc/NEWS.d/" + str(tag) + ".rst") bad_files = list(glob.glob("Misc/NEWS.d/next/*/0*.rst")) bad_files.extend(glob.glob("Misc/NEWS.d/next/*/2*.rst")) if bad_files or not good_files: print('It doesn\'t look like you ran "blurb release" yet.') if bad_files: print("There are still reST files in NEWS.d/next/...") if not good_files: print(f"There is no Misc/NEWS.d/{tag}.rst file.") if not ask_question("Are you sure you want to tag?"): print("Aborting.") return False # make sure we're on the correct branch if tag.patch > 0: if ( get_output(["git", "name-rev", "--name-only", "HEAD"]).strip().decode() != f"branch-{tag}" ): print("It doesn't look like you're on the correct branch.") if not ask_question("Are you sure you want to tag?"): print("Aborting.") return False if sign_gpg: print("Signing tag") uid = os.environ.get("GPG_KEY_FOR_RELEASE") if not uid: print("List of available private keys:") run_cmd(['gpg -K | grep -A 1 "^sec"'], shell=True) uid = input("Please enter key ID to use for signing: ") run_cmd( ["git", "tag", "-s", "-u", uid, tag.gitname, "-m", "Python " + str(tag)] ) else: print("Creating tag") run_cmd(["git", "tag", tag.gitname, "-m", "Python " + str(tag)]) return True def done(tag: Tag) -> None: tweak_patchlevel(tag, done=True) def main(argv: Any) -> None: chdir_to_repo_root() parser = get_arg_parser() options, args = parser.parse_args(argv) if options.skip_docs and not options.export: error("--skip-docs option has no effect without --export") if len(args) != 2: if "RELEASE_TAG" not in os.environ: parser.print_usage() sys.exit(1) tagname = os.environ["RELEASE_TAG"] else: tagname = args[1] tag = Tag(tagname) if not (options.export or options.upload): check_env() if options.bump: bump(tag) if options.tag: make_tag(tag) if options.export: export(tag, skip_docs=options.skip_docs) if options.upload: upload(tag, options.upload) if options.done: done(tag) if __name__ == "__main__": main(sys.argv) ================================================ FILE: requirements.in ================================================ --only-binary :all: paramiko alive_progress>=3.3.0 python-gnupg aiohttp blurb>=1.2.1 # Pending https://github.com/python/release-tools/pull/289 sigstore>=3,<4 ================================================ FILE: requirements.txt ================================================ # # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --generate-hashes --output-file=requirements.txt requirements.in # --only-binary :all: about-time==4.2.1 \ --hash=sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341 # via alive-progress aiohappyeyeballs==2.6.1 \ --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 # via aiohttp aiohttp==3.13.3 \ --hash=sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf \ --hash=sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c \ --hash=sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c \ --hash=sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423 \ --hash=sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f \ --hash=sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40 \ --hash=sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2 \ --hash=sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf \ --hash=sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821 \ --hash=sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64 \ --hash=sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7 \ --hash=sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998 \ --hash=sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d \ --hash=sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea \ --hash=sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463 \ --hash=sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80 \ --hash=sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4 \ --hash=sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767 \ --hash=sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43 \ --hash=sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592 \ --hash=sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a \ --hash=sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e \ --hash=sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687 \ --hash=sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8 \ --hash=sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261 \ --hash=sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd \ --hash=sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a \ --hash=sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4 \ --hash=sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587 \ --hash=sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91 \ --hash=sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f \ --hash=sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3 \ --hash=sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344 \ --hash=sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6 \ --hash=sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3 \ --hash=sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce \ --hash=sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808 \ --hash=sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1 \ --hash=sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29 \ --hash=sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3 \ --hash=sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b \ --hash=sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51 \ --hash=sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c \ --hash=sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926 \ --hash=sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64 \ --hash=sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f \ --hash=sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b \ --hash=sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e \ --hash=sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440 \ --hash=sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6 \ --hash=sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3 \ --hash=sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d \ --hash=sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415 \ --hash=sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279 \ --hash=sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce \ --hash=sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603 \ --hash=sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0 \ --hash=sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c \ --hash=sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf \ --hash=sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591 \ --hash=sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540 \ --hash=sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e \ --hash=sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26 \ --hash=sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a \ --hash=sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845 \ --hash=sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a \ --hash=sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9 \ --hash=sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6 \ --hash=sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba \ --hash=sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df \ --hash=sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43 \ --hash=sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679 \ --hash=sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7 \ --hash=sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7 \ --hash=sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc \ --hash=sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29 \ --hash=sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02 \ --hash=sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984 \ --hash=sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1 \ --hash=sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6 \ --hash=sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632 \ --hash=sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56 \ --hash=sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239 \ --hash=sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168 \ --hash=sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc \ --hash=sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11 \ --hash=sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046 \ --hash=sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0 \ --hash=sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3 \ --hash=sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877 \ --hash=sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1 \ --hash=sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c \ --hash=sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25 \ --hash=sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704 \ --hash=sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a \ --hash=sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033 \ --hash=sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1 \ --hash=sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29 \ --hash=sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d \ --hash=sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160 \ --hash=sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d \ --hash=sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f \ --hash=sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f \ --hash=sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538 \ --hash=sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29 \ --hash=sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7 \ --hash=sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72 \ --hash=sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af \ --hash=sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455 \ --hash=sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57 \ --hash=sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558 \ --hash=sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c \ --hash=sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808 \ --hash=sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7 \ --hash=sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0 \ --hash=sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3 \ --hash=sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730 \ --hash=sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa \ --hash=sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940 # via -r requirements.in aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e # via aiohttp alive-progress==3.3.0 \ --hash=sha256:63dd33bb94cde15ad9e5b666dbba8fedf71b72a4935d6fb9a92931e69402c9ff # via -r requirements.in annotated-types==0.7.0 \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 # via pydantic attrs==24.3.0 \ --hash=sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308 # via aiohttp bcrypt==4.2.1 \ --hash=sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837 \ --hash=sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6 \ --hash=sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17 \ --hash=sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99 \ --hash=sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54 \ --hash=sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e \ --hash=sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396 \ --hash=sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d \ --hash=sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685 \ --hash=sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413 \ --hash=sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526 \ --hash=sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad \ --hash=sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a \ --hash=sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea \ --hash=sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005 \ --hash=sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f \ --hash=sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf \ --hash=sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425 \ --hash=sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84 \ --hash=sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c \ --hash=sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139 \ --hash=sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f \ --hash=sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c \ --hash=sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331 # via paramiko betterproto==2.0.0b6 \ --hash=sha256:a0839ec165d110a69d0d116f4d0e2bec8d186af4db826257931f0831dab73fcf # via sigstore-protobuf-specs blurb==2.0.0 \ --hash=sha256:f6d0e858dbe94765f6a89b8228217ffdb9c19cff08fc8f2c3153954846d31aa1 # via -r requirements.in certifi==2024.12.14 \ --hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 # via requests cffi==2.0.0 \ --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf # via # cryptography # pynacl charset-normalizer==3.4.1 \ --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ --hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \ --hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \ --hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \ --hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \ --hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \ --hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \ --hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \ --hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \ --hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \ --hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \ --hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \ --hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \ --hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \ --hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \ --hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \ --hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \ --hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \ --hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \ --hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \ --hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \ --hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \ --hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \ --hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \ --hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \ --hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \ --hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \ --hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \ --hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \ --hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \ --hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \ --hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \ --hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \ --hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \ --hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \ --hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \ --hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \ --hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \ --hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \ --hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \ --hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \ --hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \ --hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \ --hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \ --hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \ --hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \ --hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \ --hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \ --hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \ --hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \ --hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \ --hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \ --hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \ --hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \ --hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \ --hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \ --hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \ --hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \ --hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \ --hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \ --hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \ --hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \ --hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \ --hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \ --hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \ --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \ --hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \ --hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \ --hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \ --hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \ --hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \ --hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \ --hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \ --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \ --hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \ --hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \ --hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \ --hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \ --hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \ --hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \ --hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \ --hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \ --hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \ --hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \ --hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \ --hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \ --hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \ --hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \ --hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \ --hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \ --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 # via requests cryptography==46.0.7 \ --hash=sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65 \ --hash=sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832 \ --hash=sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067 \ --hash=sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de \ --hash=sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4 \ --hash=sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0 \ --hash=sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b \ --hash=sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968 \ --hash=sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef \ --hash=sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b \ --hash=sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4 \ --hash=sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3 \ --hash=sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308 \ --hash=sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e \ --hash=sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163 \ --hash=sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f \ --hash=sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee \ --hash=sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77 \ --hash=sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85 \ --hash=sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99 \ --hash=sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7 \ --hash=sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83 \ --hash=sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85 \ --hash=sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006 \ --hash=sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb \ --hash=sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e \ --hash=sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba \ --hash=sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325 \ --hash=sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d \ --hash=sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1 \ --hash=sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1 \ --hash=sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2 \ --hash=sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0 \ --hash=sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455 \ --hash=sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842 \ --hash=sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457 \ --hash=sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15 \ --hash=sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2 \ --hash=sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c \ --hash=sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb \ --hash=sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4 \ --hash=sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902 \ --hash=sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246 \ --hash=sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022 \ --hash=sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f \ --hash=sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e \ --hash=sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298 \ --hash=sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce # via # paramiko # pyopenssl # rfc3161-client # sigstore dnspython==2.7.0 \ --hash=sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86 # via email-validator email-validator==2.2.0 \ --hash=sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631 # via pydantic frozenlist==1.5.0 \ --hash=sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e \ --hash=sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf \ --hash=sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6 \ --hash=sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a \ --hash=sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d \ --hash=sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f \ --hash=sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28 \ --hash=sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b \ --hash=sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9 \ --hash=sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2 \ --hash=sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec \ --hash=sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2 \ --hash=sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c \ --hash=sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336 \ --hash=sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4 \ --hash=sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d \ --hash=sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b \ --hash=sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c \ --hash=sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10 \ --hash=sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08 \ --hash=sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942 \ --hash=sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8 \ --hash=sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f \ --hash=sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10 \ --hash=sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5 \ --hash=sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6 \ --hash=sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21 \ --hash=sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c \ --hash=sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d \ --hash=sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923 \ --hash=sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608 \ --hash=sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de \ --hash=sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17 \ --hash=sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0 \ --hash=sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f \ --hash=sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641 \ --hash=sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c \ --hash=sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a \ --hash=sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0 \ --hash=sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9 \ --hash=sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab \ --hash=sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f \ --hash=sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3 \ --hash=sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a \ --hash=sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784 \ --hash=sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604 \ --hash=sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d \ --hash=sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5 \ --hash=sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03 \ --hash=sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e \ --hash=sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953 \ --hash=sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee \ --hash=sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d \ --hash=sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3 \ --hash=sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039 \ --hash=sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f \ --hash=sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9 \ --hash=sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf \ --hash=sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76 \ --hash=sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba \ --hash=sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171 \ --hash=sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb \ --hash=sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439 \ --hash=sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631 \ --hash=sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972 \ --hash=sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d \ --hash=sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869 \ --hash=sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9 \ --hash=sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411 \ --hash=sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723 \ --hash=sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2 \ --hash=sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b \ --hash=sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99 \ --hash=sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e \ --hash=sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840 \ --hash=sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3 \ --hash=sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb \ --hash=sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3 \ --hash=sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0 \ --hash=sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca \ --hash=sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45 \ --hash=sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e \ --hash=sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f \ --hash=sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5 \ --hash=sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307 \ --hash=sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e \ --hash=sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2 \ --hash=sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778 \ --hash=sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a \ --hash=sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30 \ --hash=sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a # via # aiohttp # aiosignal graphemeu==0.7.2 \ --hash=sha256:1444520f6899fd30114fc2a39f297d86d10fa0f23bf7579f772f8bc7efaa2542 # via alive-progress grpclib==0.4.8 \ --hash=sha256:a5047733a7acc1c1cee6abf3c841c7c6fab67d2844a45a853b113fa2e6cd2654 # via betterproto h2==4.3.0 \ --hash=sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd # via grpclib hpack==4.1.0 \ --hash=sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496 # via h2 hyperframe==6.1.0 \ --hash=sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5 # via h2 id==1.5.0 \ --hash=sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658 # via sigstore idna==3.10 \ --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 # via # email-validator # requests # yarl invoke==2.2.0 \ --hash=sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820 # via paramiko markdown-it-py==3.0.0 \ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 # via rich mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 # via markdown-it-py multidict==6.1.0 \ --hash=sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f \ --hash=sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056 \ --hash=sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761 \ --hash=sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3 \ --hash=sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b \ --hash=sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6 \ --hash=sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748 \ --hash=sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966 \ --hash=sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f \ --hash=sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1 \ --hash=sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6 \ --hash=sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada \ --hash=sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305 \ --hash=sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2 \ --hash=sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d \ --hash=sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef \ --hash=sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c \ --hash=sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb \ --hash=sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60 \ --hash=sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6 \ --hash=sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4 \ --hash=sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478 \ --hash=sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81 \ --hash=sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7 \ --hash=sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56 \ --hash=sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3 \ --hash=sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6 \ --hash=sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30 \ --hash=sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb \ --hash=sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506 \ --hash=sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0 \ --hash=sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925 \ --hash=sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c \ --hash=sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6 \ --hash=sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e \ --hash=sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95 \ --hash=sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2 \ --hash=sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133 \ --hash=sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2 \ --hash=sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa \ --hash=sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3 \ --hash=sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3 \ --hash=sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436 \ --hash=sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657 \ --hash=sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581 \ --hash=sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492 \ --hash=sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43 \ --hash=sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2 \ --hash=sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2 \ --hash=sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926 \ --hash=sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057 \ --hash=sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc \ --hash=sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80 \ --hash=sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255 \ --hash=sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1 \ --hash=sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972 \ --hash=sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53 \ --hash=sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1 \ --hash=sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423 \ --hash=sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a \ --hash=sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160 \ --hash=sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c \ --hash=sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd \ --hash=sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa \ --hash=sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5 \ --hash=sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b \ --hash=sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa \ --hash=sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef \ --hash=sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44 \ --hash=sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4 \ --hash=sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156 \ --hash=sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753 \ --hash=sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28 \ --hash=sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d \ --hash=sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a \ --hash=sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304 \ --hash=sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008 \ --hash=sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429 \ --hash=sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72 \ --hash=sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399 \ --hash=sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3 \ --hash=sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392 \ --hash=sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167 \ --hash=sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c \ --hash=sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774 \ --hash=sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351 \ --hash=sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76 \ --hash=sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875 \ --hash=sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd \ --hash=sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28 \ --hash=sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db # via # aiohttp # grpclib # yarl paramiko==4.0.0 \ --hash=sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9 # via -r requirements.in platformdirs==4.3.6 \ --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb # via sigstore propcache==0.2.1 \ --hash=sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4 \ --hash=sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4 \ --hash=sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a \ --hash=sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f \ --hash=sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9 \ --hash=sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d \ --hash=sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e \ --hash=sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6 \ --hash=sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf \ --hash=sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034 \ --hash=sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d \ --hash=sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16 \ --hash=sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30 \ --hash=sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba \ --hash=sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95 \ --hash=sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d \ --hash=sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae \ --hash=sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348 \ --hash=sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2 \ --hash=sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce \ --hash=sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54 \ --hash=sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629 \ --hash=sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54 \ --hash=sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1 \ --hash=sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b \ --hash=sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf \ --hash=sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b \ --hash=sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587 \ --hash=sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097 \ --hash=sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea \ --hash=sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24 \ --hash=sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7 \ --hash=sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541 \ --hash=sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6 \ --hash=sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634 \ --hash=sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3 \ --hash=sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d \ --hash=sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034 \ --hash=sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465 \ --hash=sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2 \ --hash=sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf \ --hash=sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1 \ --hash=sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04 \ --hash=sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5 \ --hash=sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583 \ --hash=sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb \ --hash=sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b \ --hash=sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c \ --hash=sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958 \ --hash=sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc \ --hash=sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4 \ --hash=sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82 \ --hash=sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e \ --hash=sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce \ --hash=sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9 \ --hash=sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518 \ --hash=sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536 \ --hash=sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505 \ --hash=sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052 \ --hash=sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff \ --hash=sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1 \ --hash=sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f \ --hash=sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681 \ --hash=sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347 \ --hash=sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af \ --hash=sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246 \ --hash=sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787 \ --hash=sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0 \ --hash=sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f \ --hash=sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439 \ --hash=sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3 \ --hash=sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6 \ --hash=sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca \ --hash=sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec \ --hash=sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d \ --hash=sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3 \ --hash=sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16 \ --hash=sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717 \ --hash=sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6 \ --hash=sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd \ --hash=sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212 # via # aiohttp # yarl pyasn1==0.6.3 \ --hash=sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde # via sigstore pycparser==2.22 \ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc # via cffi pydantic[email]==2.12.5 \ --hash=sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d # via # sigstore # sigstore-rekor-types pydantic-core==2.41.5 \ --hash=sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90 \ --hash=sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740 \ --hash=sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504 \ --hash=sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84 \ --hash=sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33 \ --hash=sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c \ --hash=sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0 \ --hash=sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 \ --hash=sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a \ --hash=sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 \ --hash=sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2 \ --hash=sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3 \ --hash=sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815 \ --hash=sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14 \ --hash=sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba \ --hash=sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375 \ --hash=sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf \ --hash=sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963 \ --hash=sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1 \ --hash=sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808 \ --hash=sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553 \ --hash=sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1 \ --hash=sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2 \ --hash=sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5 \ --hash=sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470 \ --hash=sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2 \ --hash=sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b \ --hash=sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660 \ --hash=sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c \ --hash=sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093 \ --hash=sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5 \ --hash=sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594 \ --hash=sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008 \ --hash=sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a \ --hash=sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a \ --hash=sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd \ --hash=sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284 \ --hash=sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 \ --hash=sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869 \ --hash=sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294 \ --hash=sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f \ --hash=sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66 \ --hash=sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51 \ --hash=sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc \ --hash=sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97 \ --hash=sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a \ --hash=sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d \ --hash=sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9 \ --hash=sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c \ --hash=sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07 \ --hash=sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36 \ --hash=sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e \ --hash=sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05 \ --hash=sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e \ --hash=sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941 \ --hash=sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3 \ --hash=sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612 \ --hash=sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3 \ --hash=sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b \ --hash=sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe \ --hash=sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146 \ --hash=sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11 \ --hash=sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60 \ --hash=sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd \ --hash=sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b \ --hash=sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c \ --hash=sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a \ --hash=sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460 \ --hash=sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1 \ --hash=sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf \ --hash=sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf \ --hash=sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858 \ --hash=sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2 \ --hash=sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9 \ --hash=sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2 \ --hash=sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3 \ --hash=sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6 \ --hash=sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770 \ --hash=sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d \ --hash=sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc \ --hash=sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23 \ --hash=sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26 \ --hash=sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa \ --hash=sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8 \ --hash=sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d \ --hash=sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3 \ --hash=sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d \ --hash=sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034 \ --hash=sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9 \ --hash=sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1 \ --hash=sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56 \ --hash=sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b \ --hash=sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c \ --hash=sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a \ --hash=sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e \ --hash=sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9 \ --hash=sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5 \ --hash=sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a \ --hash=sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556 \ --hash=sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e \ --hash=sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49 \ --hash=sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2 \ --hash=sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9 \ --hash=sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b \ --hash=sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc \ --hash=sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb \ --hash=sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0 \ --hash=sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8 \ --hash=sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82 \ --hash=sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69 \ --hash=sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b \ --hash=sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c \ --hash=sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75 \ --hash=sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5 \ --hash=sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f \ --hash=sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad \ --hash=sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b \ --hash=sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7 \ --hash=sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425 \ --hash=sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52 # via pydantic pygments==2.20.0 \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via rich pyjwt==2.12.0 \ --hash=sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e # via sigstore pynacl==1.6.2 \ --hash=sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574 \ --hash=sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4 \ --hash=sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130 \ --hash=sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b \ --hash=sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590 \ --hash=sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444 \ --hash=sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634 \ --hash=sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87 \ --hash=sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa \ --hash=sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594 \ --hash=sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0 \ --hash=sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e \ --hash=sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c \ --hash=sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0 \ --hash=sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c \ --hash=sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577 \ --hash=sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145 \ --hash=sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88 \ --hash=sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14 \ --hash=sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6 \ --hash=sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465 \ --hash=sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0 \ --hash=sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2 \ --hash=sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9 # via paramiko pyopenssl==26.0.0 \ --hash=sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81 # via sigstore python-dateutil==2.9.0.post0 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via betterproto python-gnupg==0.5.6 \ --hash=sha256:b5050a55663d8ab9fcc8d97556d229af337a87a3ebebd7054cbd8b7e2043394a # via -r requirements.in requests==2.33.0 \ --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b # via # id # sigstore rfc3161-client==1.0.6 \ --hash=sha256:0b3920334f7334ec3bb9c319d53a5d08cd43b6883f75e2669cfd869cd264d53a \ --hash=sha256:1671b1be16480ea54c0d36239efd0fb62c13dd572a9865a5e91fea39f1b95303 \ --hash=sha256:1be4e1133f0f7fe875629f2c358285503c1cfc79cebfbc3fb4e28b8a57d6f1a4 \ --hash=sha256:2bc9835467f6166edd6f876470484e5b294ee141add6eff6a59f5047937aaa75 \ --hash=sha256:3da328ba08139846b1ab3a03402ba8a5f3659a640dbe2cd6a18f7f342e99ba98 \ --hash=sha256:4ef4b096abe7d55b020526e39932c2721939a6c55e9a5cd3b3e77897a0942937 \ --hash=sha256:63355099d932851eac507806bb9d0937dab546a66d5857d888168799ec635f6d \ --hash=sha256:78cdc6bde331492cb94f69328831d5c56b271012b00c6f1784c2e4b33837d585 \ --hash=sha256:8102165201c5224cf6e6634bfd68c6a39e8f800601188216f8210face4861215 \ --hash=sha256:85a1d71d1eb2c9bced2b3eb75e96f9fe49732ec2567b5dafa1dd889fff42b7fe \ --hash=sha256:8631f7db7c1327bf87ee6a9a8681b4cd6bc2a90aae651388f29d045cd9ff1ac9 \ --hash=sha256:940e1fc95ec0ca734927a82bcb5363fa988ef1a085d238ff0c861f29c0cfb746 \ --hash=sha256:9a98e9c7ff632d9571fcea25fb70bde0e8339b86368aef67a65f6a301f125733 \ --hash=sha256:b7ad54288a49379b01b1d0d9d15167d2b7c6c7f940332ab85eeb4a6e844da8c7 \ --hash=sha256:bc379167238df32cbcc1dc9c324088559c1734331030f5293d75f4fd37b5f4f6 \ --hash=sha256:bed6ef8e194cab85f6ec5678995b6406bb568383ebb6a4301be40e7939dd28d9 \ --hash=sha256:e16ed34f6f33fd62aa3b1f83615ecf2f96e1b1f57df4e1a36570b3f895333972 \ --hash=sha256:e3caffaebf43242b000c4a6659d60eaf19c3b161ccbe05b15634a856c9ea7e61 # via sigstore rfc8785==0.1.4 \ --hash=sha256:520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48 # via sigstore rich==13.9.4 \ --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90 # via sigstore securesystemslib==1.2.0 \ --hash=sha256:fa63abcb1cf4dba4f2df964f623baa45bc39029980d7a0a2119d90731942afc6 # via tuf sigstore==3.6.7 \ --hash=sha256:85d7512499eded0ffc310462d8be81731a631320751e390d74370d6458864df9 # via -r requirements.in sigstore-protobuf-specs==0.3.2 \ --hash=sha256:50c99fa6747a3a9c5c562a43602cf76df0b199af28f0e9d4319b6775630425ea # via sigstore sigstore-rekor-types==0.0.18 \ --hash=sha256:b62bf38c5b1a62bc0d7fe0ee51a0709e49311d137c7880c329882a8f4b2d1d78 # via sigstore six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 # via python-dateutil tuf==6.0.0 \ --hash=sha256:458f663a233d95cc76dde0e1a3d01796516a05ce2781fefafebe037f7729601a # via sigstore typing-extensions==4.15.0 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via # aiosignal # pydantic # pydantic-core # pyopenssl # typing-inspection typing-inspection==0.4.2 \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 # via pydantic urllib3==2.6.3 \ --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 # via # requests # tuf yarl==1.18.3 \ --hash=sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba \ --hash=sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193 \ --hash=sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318 \ --hash=sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee \ --hash=sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e \ --hash=sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1 \ --hash=sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a \ --hash=sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186 \ --hash=sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1 \ --hash=sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50 \ --hash=sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640 \ --hash=sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb \ --hash=sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8 \ --hash=sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc \ --hash=sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5 \ --hash=sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58 \ --hash=sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2 \ --hash=sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393 \ --hash=sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24 \ --hash=sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b \ --hash=sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910 \ --hash=sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c \ --hash=sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272 \ --hash=sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed \ --hash=sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1 \ --hash=sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04 \ --hash=sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d \ --hash=sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5 \ --hash=sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d \ --hash=sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889 \ --hash=sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae \ --hash=sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b \ --hash=sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c \ --hash=sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576 \ --hash=sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34 \ --hash=sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477 \ --hash=sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990 \ --hash=sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2 \ --hash=sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512 \ --hash=sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069 \ --hash=sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a \ --hash=sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6 \ --hash=sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0 \ --hash=sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8 \ --hash=sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb \ --hash=sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa \ --hash=sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8 \ --hash=sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e \ --hash=sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e \ --hash=sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985 \ --hash=sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8 \ --hash=sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5 \ --hash=sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690 \ --hash=sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10 \ --hash=sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789 \ --hash=sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b \ --hash=sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca \ --hash=sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e \ --hash=sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5 \ --hash=sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59 \ --hash=sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9 \ --hash=sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8 \ --hash=sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db \ --hash=sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde \ --hash=sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7 \ --hash=sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb \ --hash=sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3 \ --hash=sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6 \ --hash=sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285 \ --hash=sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb \ --hash=sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8 \ --hash=sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482 \ --hash=sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd \ --hash=sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75 \ --hash=sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760 \ --hash=sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782 \ --hash=sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53 \ --hash=sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2 \ --hash=sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1 \ --hash=sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719 \ --hash=sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62 # via aiohttp ================================================ FILE: run_release.py ================================================ #!/usr/bin/env python3 """An automatic engine for Python releases Original code by Pablo Galindo """ from __future__ import annotations import argparse import asyncio import contextlib import functools import getpass import json import os import re import shelve import shlex import shutil import subprocess import sys import tempfile import time import urllib.request from collections.abc import Iterator from pathlib import Path from typing import Any, cast import aiohttp import gnupg # type: ignore[import-untyped] import paramiko import sigstore.oidc from alive_progress import alive_bar import release as release_mod import sbom import update_version_next from buildbotapi import BuildBotAPI, Builder from release import ReleaseShelf, Tag, Task, ask_question API_KEY_REGEXP = re.compile(r"(?P\w+):(?P\w+)") RELEASE_REGEXP = re.compile( r"(?P\d+)\.(?P\d+)\.(?P\d+)\.?(?P.*)?" ) DOWNLOADS_SERVER = "downloads.nyc1.psf.io" DOCS_SERVER = "docs.nyc1.psf.io" WHATS_NEW_TEMPLATE = """ **************************** What's new in Python {version} **************************** :Editor: TBD .. Rules for maintenance: * Anyone can add text to this document. Do not spend very much time on the wording of your changes, because your text will probably get rewritten to some degree. * The maintainer will go through Misc/NEWS periodically and add changes; it's therefore more important to add your changes to Misc/NEWS than to this file. * This is not a complete list of every single change; completeness is the purpose of Misc/NEWS. Some changes I consider too small or esoteric to include. If such a change is added to the text, I'll just remove it. (This is another reason you shouldn't spend too much time on writing your addition.) * If you want to draw your new text to the attention of the maintainer, add 'XXX' to the beginning of the paragraph or section. * It's OK to just add a fragmentary note about a change. For example: "XXX Describe the transmogrify() function added to the socket module." The maintainer will research the change and write the necessary text. * You can comment out your additions if you like, but it's not necessary (especially when a final release is some months away). * Credit the author of a patch or bugfix. Just the name is sufficient; the e-mail address isn't necessary. * It's helpful to add the issue number as a comment: XXX Describe the transmogrify() function added to the socket module. (Contributed by P.Y. Developer in :gh:`12345`.) This saves the maintainer the effort of going through the VCS log when researching a change. This article explains the new features in Python {version}, compared to {prev_version}. For full details, see the :ref:`changelog `. .. note:: Prerelease users should be aware that this document is currently in draft form. It will be updated substantially as Python {version} moves towards release, so it's worth checking back even after reading earlier versions. Summary --- release highlights ============================== .. This section singles out the most important changes in Python {version}. Brevity is key. .. PEP-sized items next. New features ============ Other language changes ====================== New modules =========== * None yet. Improved modules ================ module_name ----------- * TODO .. Add improved modules above alphabetically, not here at the end. Optimizations ============= module_name ----------- * TODO Removed ======= module_name ----------- * TODO .. Add removals above alphabetically, not here at the end. Deprecated ========== * module_name: TODO .. Add deprecations above alphabetically, not here at the end. Porting to Python {version} ====================== This section lists previously described changes and other bugfixes that may require changes to your code. Build changes ============= C API changes ============= New features ------------ * TODO Porting to Python {version} ---------------------- * TODO Deprecated C APIs ----------------- * TODO .. Add C API deprecations above alphabetically, not here at the end. Removed C APIs -------------- """ class ReleaseException(Exception): """An error happened in the release process""" class ReleaseDriver: def __init__( self, tasks: list[Task], *, release_tag: Tag, git_repo: str, api_key: str, ssh_user: str, sign_gpg: bool, ssh_key: str | None = None, first_state: Task | None = None, ) -> None: self.tasks = tasks dbfile = Path.home() / f".python_release-{release_tag}" self.db: ReleaseShelf = cast(ReleaseShelf, shelve.open(str(dbfile), "c")) if not self.db.get("finished"): self.db["finished"] = False else: self.db.close() self.db = cast(ReleaseShelf, shelve.open(str(dbfile), "n")) self.current_task: Task | None = first_state self.completed_tasks = self.db.get("completed_tasks", []) self.remaining_tasks = iter(tasks[len(self.completed_tasks) :]) if self.db.get("gpg_key"): os.environ["GPG_KEY_FOR_RELEASE"] = self.db["gpg_key"] if not self.db.get("git_repo"): self.db["git_repo"] = Path(git_repo) if not self.db.get("auth_info"): self.db["auth_info"] = api_key if not self.db.get("ssh_user"): self.db["ssh_user"] = ssh_user if not self.db.get("ssh_key"): self.db["ssh_key"] = ssh_key if not self.db.get("sign_gpg"): self.db["sign_gpg"] = sign_gpg if not self.db.get("release"): self.db["release"] = release_tag if not self.db.get("security_release"): self.db["security_release"] = self.db["release"].is_security_release print("Release data: ") print(f"- Branch: {release_tag.branch}") print(f"- Release tag: {self.db['release']}") print(f"- Normalized release tag: {release_tag.normalized()}") print(f"- Git repo: {self.db['git_repo']}") print(f"- SSH username: {self.db['ssh_user']}") print(f"- SSH key: {self.db['ssh_key'] or 'Default'}") print(f"- Sign with GPG: {self.db['sign_gpg']}") print(f"- Security release: {self.db['security_release']}") print() def checkpoint(self) -> None: self.db["completed_tasks"] = self.completed_tasks def run(self) -> None: for task in self.completed_tasks: print(f"✅ {task.description}") self.current_task = next(self.remaining_tasks, None) while self.current_task is not None: self.checkpoint() try: self.current_task(self.db) except Exception as e: print(f"\r💥 {self.current_task.description}") raise e from None print(f"\r✅ {self.current_task.description}") self.completed_tasks.append(self.current_task) self.current_task = next(self.remaining_tasks, None) self.db["finished"] = True print() print(f"Congratulations, Python {self.db['release']} is released 🎉🎉🎉") @contextlib.contextmanager def cd(path: Path) -> Iterator[None]: current_path = os.getcwd() os.chdir(path) yield os.chdir(current_path) def check_tool(db: ReleaseShelf, tool: str) -> None: if shutil.which(tool) is None: raise ReleaseException(f"{tool} is not available") check_gh = functools.partial(check_tool, tool="gh") check_git = functools.partial(check_tool, tool="git") check_make = functools.partial(check_tool, tool="make") check_blurb = functools.partial(check_tool, tool="blurb") check_autoconf = functools.partial(check_tool, tool="autoconf") check_docker = functools.partial(check_tool, tool="docker") def check_gpg_keys(db: ReleaseShelf) -> None: pg = gnupg.GPG() keys = pg.list_keys(secret=True) if not keys: raise ReleaseException("There are no valid GPG keys for release") for index, key in enumerate(keys): print(f"{index} - {key['keyid']}: {key['uids']}") selected_key_index = -1 while not (0 <= selected_key_index < len(keys)): with contextlib.suppress(ValueError): selected_key_index = int( input("Select one GPG key for release (by index):") ) selected_key = keys[selected_key_index]["keyid"] os.environ["GPG_KEY_FOR_db['release']"] = selected_key if selected_key not in {key["keyid"] for key in keys}: raise ReleaseException("Invalid GPG key selected") db["gpg_key"] = selected_key os.environ["GPG_KEY_FOR_RELEASE"] = db["gpg_key"] def check_ssh_connection(db: ReleaseShelf) -> None: client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.RejectPolicy) client.connect( DOWNLOADS_SERVER, port=22, username=db["ssh_user"], key_filename=db["ssh_key"] ) client.exec_command("pwd") client.connect( DOCS_SERVER, port=22, username=db["ssh_user"], key_filename=db["ssh_key"] ) client.exec_command("pwd") def check_sigstore_client(db: ReleaseShelf) -> None: client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.RejectPolicy) client.connect( DOWNLOADS_SERVER, port=22, username=db["ssh_user"], key_filename=db["ssh_key"] ) _, stdout, _ = client.exec_command("python3 -m sigstore --version") sigstore_version = stdout.read(1000).decode() check_sigstore_version(sigstore_version) def check_sigstore_version(version: str) -> None: version_match = re.match("^sigstore ([0-9.]+)", version) if version_match: version_tuple = tuple(int(part) for part in version_match.group(1).split(".")) if (3, 6, 2) <= version_tuple < (4, 0): # good version return raise ReleaseException( f"Sigstore version not detected or not valid. " f"Expecting >= 3.6.2 and < 4.0.0, got: {version}" ) def check_buildbots(db: ReleaseShelf) -> None: async def _check() -> set[Builder]: async def _get_builder_status( buildbot_api: BuildBotAPI, the_builder: Builder ) -> tuple[Builder, bool]: return the_builder, await buildbot_api.is_builder_failing_currently( the_builder ) async with aiohttp.ClientSession() as session: api = BuildBotAPI(session) await api.authenticate(token="") release_branch = db["release"].branch stable_builders = await api.stable_builders(branch=release_branch) if not stable_builders: release_branch = "3.x" stable_builders = await api.stable_builders(branch="3.x") if not stable_builders: raise ReleaseException( f"Failed to get the stable buildbots for the {release_branch} tag" ) builders = await asyncio.gather( *[ _get_builder_status(api, the_builder) for the_builder in stable_builders.values() ] ) return {the_builder for (the_builder, is_failing) in builders if is_failing} failing_builders = asyncio.run(_check()) if not failing_builders: return print() print("The following buildbots are failing:") for builder in failing_builders: print(f"- {builder.name}") print() print("Check https://buildbot.python.org/all/#/release_status for more information") print() if not ask_question("Do you want to continue even if these builders are failing?"): raise ReleaseException("Buildbots are failing!") def check_docker_running(db: ReleaseShelf) -> None: subprocess.check_call(["docker", "container", "ls"]) def run_blurb_release(db: ReleaseShelf) -> None: subprocess.check_call(["blurb", "release", str(db["release"])], cwd=db["git_repo"]) subprocess.check_call( ["git", "commit", "-m", f"Python {db['release']}"], cwd=db["git_repo"], ) def check_cpython_repo_branch(db: ReleaseShelf) -> None: current_branch = subprocess.check_output( shlex.split("git branch --show-current"), text=True, cwd=db["git_repo"] ).strip() expected_branch = db["release"].branch if current_branch != expected_branch: raise ReleaseException( f"CPython repository is on {current_branch} branch, " f"expected {expected_branch}" ) def check_cpython_repo_age(db: ReleaseShelf) -> None: # %ct = committer date, UNIX timestamp (for example, "1768300016") timestamp = subprocess.check_output( shlex.split('git log -1 --format="%ct"'), text=True, cwd=db["git_repo"] ).strip() age_seconds = time.time() - int(timestamp.strip()) is_old = age_seconds > 86400 # 1 day # cr = committer date, relative (for example, "3 days ago") out = subprocess.check_output( shlex.split('git log -1 --format="%cr"'), text=True, cwd=db["git_repo"] ) print(f"Last CPython commit was {out.strip()}") if is_old and not ask_question("Continue with old repo?"): raise ReleaseException("CPython repository is old") def check_cpython_repo_is_clean(db: ReleaseShelf) -> None: if subprocess.check_output(["git", "status", "--porcelain"], cwd=db["git_repo"]): raise ReleaseException("Git repository is not clean") def check_magic_number(db: ReleaseShelf) -> None: release_tag = db["release"] if release_tag.is_final or release_tag.is_release_candidate: def out(msg: str) -> None: raise ReleaseException(msg) else: def out(msg: str) -> None: print("warning:", msg, file=sys.stderr, flush=True) def get_magic(source: Path, regex: re.Pattern[str]) -> str: if m := regex.search(source.read_text()): return m.group("magic") out(f"Cannot find magic in {source}, tried {regex.pattern}") return "unknown" work_dir = Path(db["git_repo"]) magic_actual_file = work_dir / "Include" / "internal" / "pycore_magic_number.h" magic_actual_re = re.compile( r"^#define\s+PYC_MAGIC_NUMBER\s+(?P\d+)$", re.MULTILINE ) magic_actual = get_magic(magic_actual_file, magic_actual_re) magic_expected_file = work_dir / "Lib" / "test" / "test_importlib" / "test_util.py" magic_expected_re = re.compile( r"^\s+EXPECTED_MAGIC_NUMBER = (?P\d+)$", re.MULTILINE ) magic_expected = get_magic(magic_expected_file, magic_expected_re) if magic_actual == magic_expected: return out( f"Magic numbers in {magic_actual_file} ({magic_actual})" f" and {magic_expected_file} ({magic_expected}) don't match." ) if not ask_question("Do you want to continue? This will fail tests in RC stage."): raise ReleaseException("Magic numbers don't match!") def prepare_temporary_branch(db: ReleaseShelf) -> None: subprocess.check_call( ["git", "checkout", "-b", f"branch-{db['release']}"], cwd=db["git_repo"] ) def remove_temporary_branch(db: ReleaseShelf) -> None: subprocess.check_call( ["git", "branch", "-D", f"branch-{db['release']}"], cwd=db["git_repo"] ) def prepare_pydoc_topics(db: ReleaseShelf) -> None: subprocess.check_call(["make", "venv"], cwd=db["git_repo"] / "Doc") subprocess.check_call(["make", "pydoc-topics"], cwd=db["git_repo"] / "Doc") shutil.copy2( db["git_repo"] / "Doc" / "build" / "pydoc-topics" / "topics.py", db["git_repo"] / "Lib" / "pydoc_data" / "topics.py", ) if db["release"].as_tuple() >= (3, 13): shutil.copy2( db["git_repo"] / "Doc" / "build" / "pydoc-topics" / "module_docs.py", db["git_repo"] / "Lib" / "pydoc_data" / "module_docs.py", ) subprocess.check_call( ["git", "commit", "-a", "--amend", "--no-edit"], cwd=db["git_repo"] ) def run_autoconf(db: ReleaseShelf) -> None: # Python 3.12 and newer have a script that runs autoconf. regen_configure_sh = db["git_repo"] / "Tools/build/regen-configure.sh" if regen_configure_sh.exists(): subprocess.check_call( [regen_configure_sh], cwd=db["git_repo"], ) # Python 3.11 and prior rely on autoconf built within a container # in order to maintain stability of autoconf generation. else: # Corresponds to the tag '269' and 'cp311' cpython_autoconf_sha256 = ( "f370fee95eefa3d57b00488bce4911635411fa83e2d293ced8cf8a3674ead939" ) subprocess.check_call( [ "docker", "run", "--rm", "--pull=always", f"-v{db['git_repo']}:/src", f"quay.io/tiran/cpython_autoconf@sha256:{cpython_autoconf_sha256}", ], cwd=db["git_repo"], ) subprocess.check_call(["docker", "rmi", "quay.io/tiran/cpython_autoconf", "-f"]) subprocess.check_call( ["git", "commit", "-a", "--amend", "--no-edit"], cwd=db["git_repo"] ) def check_pyspecific(db: ReleaseShelf) -> None: with open( db["git_repo"] / "Doc" / "tools" / "extensions" / "pyspecific.py" ) as pyspecific: for line in pyspecific: if "SOURCE_URI =" in line: break expected_branch = db["release"].branch expected = ( f"SOURCE_URI = 'https://github.com/python/cpython/tree/{expected_branch}/%s'" ) if expected != line.strip(): raise ReleaseException( f"SOURCE_URI is incorrect (it needs changing before beta 1):\n" f"expected: {expected}\n" f"got : {line.strip()}" ) def bump_version(db: ReleaseShelf) -> None: with cd(db["git_repo"]): release_mod.bump(db["release"]) subprocess.check_call( ["git", "commit", "-a", "--amend", "--no-edit"], cwd=db["git_repo"] ) def bump_version_in_docs(db: ReleaseShelf) -> None: update_version_next.main([db["release"].doc_version, str(db["git_repo"])]) subprocess.check_call( ["git", "commit", "-a", "--amend", "--no-edit"], cwd=db["git_repo"] ) def create_tag(db: ReleaseShelf) -> None: with cd(db["git_repo"]): if not release_mod.make_tag(db["release"], sign_gpg=db["sign_gpg"]): raise ReleaseException("Error when creating tag") subprocess.check_call( ["git", "commit", "-a", "--amend", "--no-edit"], cwd=db["git_repo"] ) def wait_for_build_release(db: ReleaseShelf) -> None: # Determine if we need to wait for docs. release_tag = db["release"] should_wait_for_docs = release_tag.includes_docs # Create the directory so it's easier to place the artifacts there. release_path = Path(db["git_repo"] / str(release_tag)) downloads_path = release_path / "downloads" downloads_path.mkdir(parents=True, exist_ok=True) # Build the list of filepaths we're expecting. wait_for_paths = [ downloads_path / f"Python-{release_tag}.tgz", downloads_path / f"Python-{release_tag}.tar.xz", ] if release_tag.as_tuple() >= (3, 14): wait_for_paths += [ downloads_path / f"python-{release_tag}-{arch}-linux-android.tar.gz" for arch in ["aarch64", "x86_64"] ] if release_tag.as_tuple() >= (3, 15): wait_for_paths.append( downloads_path / f"python-{release_tag}-iOS-XCframework.tar.gz" ) if should_wait_for_docs: docs_path = release_path / "docs" docs_path.mkdir(parents=True, exist_ok=True) wait_for_paths.extend( [ docs_path / f"python-{release_tag}-docs.epub", docs_path / f"python-{release_tag}-docs-html.tar.bz2", docs_path / f"python-{release_tag}-docs-html.zip", docs_path / f"python-{release_tag}-docs-texinfo.tar.bz2", docs_path / f"python-{release_tag}-docs-texinfo.zip", docs_path / f"python-{release_tag}-docs-text.tar.bz2", docs_path / f"python-{release_tag}-docs-text.zip", ] ) print("Once the build-release workflow is complete:") print("- Download its artifacts from the workflow summary page.") print(f"- Copy the following files into {release_path}:") for path in wait_for_paths: print(f" - {os.path.relpath(path, release_path)}") print("The script will continue once all files are present.") while not all(path.exists() for path in wait_for_paths): time.sleep(1) def check_doc_unreleased_version(db: ReleaseShelf) -> None: print("Checking built docs for '(unreleased)'") # This string is generated when a `versionadded:: next` directive is # left in the docs, which means the `bump_version_in_docs` step # didn't do its job. # But, there could also be a false positive. release_tag = db["release"] docs_path = Path(db["git_repo"]) / str(release_tag) / "docs" archive_path = docs_path / f"python-{release_tag}-docs-html.tar.bz2" if release_tag.includes_docs: assert archive_path.exists() if archive_path.exists(): with tempfile.TemporaryDirectory() as temp_dir: subprocess.run(["tar", "-xjf", archive_path, "-C", temp_dir]) proc = subprocess.run(["grep", "-rHn", "[(]unreleased[)]", temp_dir]) if proc.returncode == 0: if not ask_question( "Are these `(unreleased)` strings in built docs OK?" ): raise AssertionError("`(unreleased)` strings found in docs") def sign_source_artifacts(db: ReleaseShelf) -> None: print("Signing tarballs with GPG") uid = os.environ.get("GPG_KEY_FOR_RELEASE") if not uid: print("List of available private keys:") subprocess.check_call('gpg -K | grep -A 1 "^sec"', shell=True) uid = input("Please enter key ID to use for signing: ") tarballs_path = Path(db["git_repo"] / str(db["release"]) / "downloads") tgz = str(tarballs_path / f"Python-{db['release']}.tgz") xz = str(tarballs_path / f"Python-{db['release']}.tar.xz") subprocess.check_call(["gpg", "-bas", "-u", uid, tgz]) subprocess.check_call(["gpg", "-bas", "-u", uid, xz]) print("Signing tarballs with Sigstore") for filename in (tgz, xz): cert_file = filename + ".crt" sig_file = filename + ".sig" bundle_file = filename + ".sigstore" subprocess.check_call( [ sys.executable, "-m", "sigstore", "sign", "--oidc-disable-ambient-providers", "--signature", sig_file, "--certificate", cert_file, "--bundle", bundle_file, filename, ] ) def build_sbom_artifacts(db: ReleaseShelf) -> None: # Skip building an SBOM if there isn't a 'Misc/sbom.spdx.json' file. if not (db["git_repo"] / "Misc/sbom.spdx.json").exists(): print("Skipping building an SBOM, missing 'Misc/sbom.spdx.json'") return release_version = db["release"] # For each source tarball build an SBOM. for ext in (".tgz", ".tar.xz"): tarball_name = f"Python-{release_version}{ext}" tarball_path = str( db["git_repo"] / str(db["release"]) / "downloads" / tarball_name ) print(f"Building an SBOM for artifact '{tarball_name}'") sbom_data = sbom.create_sbom_for_source_tarball(tarball_path) with open(tarball_path + ".spdx.json", mode="w") as f: f.write(json.dumps(sbom_data, indent=2, sort_keys=True)) class MySFTPClient(paramiko.SFTPClient): def put_dir( self, source: str | Path, target: str | Path, progress: Any = None ) -> None: for item in os.listdir(source): if os.path.isfile(os.path.join(source, item)): progress.text(item) self.put(os.path.join(source, item), f"{target}/{item}") progress() else: self.mkdir(f"{target}/{item}", ignore_existing=True) self.put_dir( os.path.join(source, item), f"{target}/{item}", progress=progress, ) def mkdir( self, path: bytes | str, mode: int = 511, ignore_existing: bool = False ) -> None: try: super().mkdir(path, mode) except OSError: if ignore_existing: pass else: raise def upload_files_to_server(db: ReleaseShelf, server: str) -> None: client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.RejectPolicy) client.connect(server, port=22, username=db["ssh_user"], key_filename=db["ssh_key"]) transport = client.get_transport() assert transport is not None, f"SSH transport to {server} is None" destination = Path(f"/home/psf-users/{db['ssh_user']}/{db['release']}") ftp_client = MySFTPClient.from_transport(transport) assert ftp_client is not None, f"SFTP client to {server} is None" client.exec_command(f"rm -rf {destination}") with contextlib.suppress(OSError): ftp_client.mkdir(str(destination)) artifacts_path = Path(db["git_repo"] / str(db["release"])) shutil.rmtree(artifacts_path / f"Python-{db['release']}", ignore_errors=True) def upload_subdir(subdir: str) -> None: with contextlib.suppress(OSError): ftp_client.mkdir(str(destination / subdir)) with alive_bar(len(tuple((artifacts_path / subdir).glob("**/*")))) as progress: ftp_client.put_dir( artifacts_path / subdir, str(destination / subdir), progress=progress, ) if server == DOCS_SERVER: upload_subdir("docs") elif server == DOWNLOADS_SERVER: upload_subdir("downloads") if (artifacts_path / "docs").exists(): upload_subdir("docs") ftp_client.close() def upload_files_to_downloads_server(db: ReleaseShelf) -> None: upload_files_to_server(db, DOWNLOADS_SERVER) def place_files_in_download_folder(db: ReleaseShelf) -> None: client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.RejectPolicy) client.connect( DOWNLOADS_SERVER, port=22, username=db["ssh_user"], key_filename=db["ssh_key"] ) transport = client.get_transport() assert transport is not None, f"SSH transport to {DOWNLOADS_SERVER} is None" # Downloads source = f"/home/psf-users/{db['ssh_user']}/{db['release']}" destination = f"/srv/www.python.org/ftp/python/{db['release'].normalized()}" def execute_command(command: str) -> None: channel = transport.open_session() channel.exec_command(command) if channel.recv_exit_status() != 0: raise ReleaseException(channel.recv_stderr(1000)) execute_command(f"mkdir -p {destination}") execute_command(f"cp {source}/downloads/* {destination}") execute_command(f"chgrp downloads {destination}") execute_command(f"chmod 775 {destination}") execute_command(f"find {destination} -type f -exec chmod 664 {{}} \\;") # Docs release_tag = db["release"] if release_tag.is_final or release_tag.is_release_candidate: source = f"/home/psf-users/{db['ssh_user']}/{db['release']}" destination = f"/srv/www.python.org/ftp/python/doc/{release_tag}" execute_command(f"mkdir -p {destination}") execute_command(f"cp {source}/docs/* {destination}") execute_command(f"chgrp downloads {destination}") execute_command(f"chmod 775 {destination}") execute_command(f"find {destination} -type f -exec chmod 664 {{}} \\;") def upload_docs_to_the_docs_server(db: ReleaseShelf) -> None: release_tag: release_mod.Tag = db["release"] if not (release_tag.is_final or release_tag.is_release_candidate): return upload_files_to_server(db, DOCS_SERVER) def unpack_docs_in_the_docs_server(db: ReleaseShelf) -> None: release_tag: release_mod.Tag = db["release"] if not (release_tag.is_final or release_tag.is_release_candidate): return client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.RejectPolicy) client.connect( DOCS_SERVER, port=22, username=db["ssh_user"], key_filename=db["ssh_key"] ) transport = client.get_transport() assert transport is not None, f"SSH transport to {DOCS_SERVER} is None" # Sources source = f"/home/psf-users/{db['ssh_user']}/{db['release']}" destination = f"/srv/docs.python.org/release/{release_tag}" def execute_command(command: str) -> None: channel = transport.open_session() channel.exec_command(command) if channel.recv_exit_status() != 0: raise ReleaseException(channel.recv_stderr(1000)) docs_filename = f"python-{release_tag}-docs-html" execute_command(f"mkdir -p {destination}") execute_command(f"unzip {source}/docs/{docs_filename}.zip -d {destination}") execute_command(f"mv /{destination}/{docs_filename}/* {destination}") execute_command(f"rm -rf /{destination}/{docs_filename}") execute_command(f"chgrp -R docs {destination}") execute_command(f"chmod -R 775 {destination}") execute_command(f"find {destination} -type f -exec chmod 664 {{}} \\;") @functools.cache def extract_github_owner(url: str) -> str: if https_match := re.match(r"(https://)?github\.com/([^/]+)/", url): return https_match.group(2) elif ssh_match := re.match(r"^git@github\.com:([^/]+)/", url): return ssh_match.group(1) else: raise ReleaseException( f"Could not parse GitHub owner from 'origin' remote URL: {url}" ) @functools.cache def get_commit_sha(git_version: str, git_repo: Path) -> str: """Get the Git commit SHA for the tag""" commit_sha = ( subprocess.check_output( ["git", "rev-list", "-n", "1", git_version], cwd=git_repo ) .decode() .strip() ) return commit_sha @functools.cache def get_origin_remote_url(git_repo: Path) -> str: """Get the owner of the GitHub repo (first path segment in a 'github.com' remote URL) This works for both 'https' and 'ssh' style remote URLs.""" origin_remote_url = ( subprocess.check_output( ["git", "ls-remote", "--get-url", "origin"], cwd=git_repo ) .decode() .strip() ) return origin_remote_url def start_build_release(db: ReleaseShelf) -> None: commit_sha = get_commit_sha(db["release"].gitname, db["git_repo"]) origin_remote_url = get_origin_remote_url(db["git_repo"]) origin_remote_github_owner = extract_github_owner(origin_remote_url) # We ask for human verification at this point since this commit SHA is 'locked in' print() print( f"Go to https://github.com/{origin_remote_github_owner}/cpython/commit/{commit_sha}" ) print("- Ensure that the commit diff does not contain any unexpected changes.") print( "- For the next step, ensure the commit SHA matches the one you verified on GitHub in this step." ) print() if not ask_question( "Have you verified the release commit hasn't been tampered with on GitHub?" ): raise ReleaseException("Commit must be visually reviewed before starting build") # After visually confirming the release manager can start the build process # with the known good commit SHA. print() cmd = ( "gh workflow run build-release.yml --repo python/release-tools" f" -f git_remote={origin_remote_github_owner}" f" -f git_commit={commit_sha}" f" -f cpython_release={db['release']}" ) subprocess.check_call(shlex.split(cmd)) print( "Go to https://github.com/python/release-tools/actions/workflows/build-release.yml" ) print() if not ask_question("Have you started the build-release workflow?"): raise ReleaseException("build-release workflow must be started") def send_email_to_platform_release_managers(db: ReleaseShelf) -> None: commit_sha = get_commit_sha(db["release"].gitname, db["git_repo"]) origin_remote_url = get_origin_remote_url(db["git_repo"]) origin_remote_github_owner = extract_github_owner(origin_remote_url) github_prefix = f"https://github.com/{origin_remote_github_owner}/cpython/tree" print() print(f"{github_prefix}/{db['release'].gitname}") print(f"Git commit SHA: {commit_sha}") print( "build-release workflow: https://github.com/python/release-tools/actions/runs/[ENTER-RUN-ID-HERE]" ) print() if not ask_question( "Have you notified the platform release managers about the availability of the commit SHA and tag?" ): raise ReleaseException("Platform release managers must be notified") def create_release_object_in_db(db: ReleaseShelf) -> None: print( "Go to https://www.python.org/admin/downloads/release/add/ and create a new release" ) if not ask_question(f"Have you already created a new release for {db['release']}?"): raise ReleaseException("The Django release object has not been created") def wait_until_all_files_are_in_folder(db: ReleaseShelf) -> None: client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.RejectPolicy) client.connect( DOWNLOADS_SERVER, port=22, username=db["ssh_user"], key_filename=db["ssh_key"] ) ftp_client = client.open_sftp() destination = f"/srv/www.python.org/ftp/python/{db['release'].normalized()}" are_all_files_there = False release = str(db["release"]) print() while not are_all_files_there: try: all_files = set(ftp_client.listdir(destination)) except FileNotFoundError: raise FileNotFoundError( f"The release folder in {destination} has not been created" ) from None are_windows_files_there = f"python-{release}.exe" in all_files are_macos_files_there = f"python-{release}-macos11.pkg" in all_files are_linux_files_there = f"Python-{release}.tgz" in all_files if db["security_release"]: # For security releases, only check Linux files are_all_files_there = are_linux_files_there else: # For regular releases, check all platforms are_all_files_there = ( are_linux_files_there and are_windows_files_there and are_macos_files_there ) if not are_all_files_there: linux_tick = "✅" if are_linux_files_there else "❌" windows_tick = "✅" if are_windows_files_there else "❌" macos_tick = "✅" if are_macos_files_there else "❌" if db["security_release"]: waiting = f"\rWaiting for files: Linux {linux_tick} (security release - only checking Linux)" else: waiting = f"\rWaiting for files: Linux {linux_tick} Windows {windows_tick} Mac {macos_tick} " print(waiting, flush=True, end="") time.sleep(1) print() def run_add_to_python_dot_org(db: ReleaseShelf) -> None: client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.RejectPolicy) client.connect( DOWNLOADS_SERVER, port=22, username=db["ssh_user"], key_filename=db["ssh_key"] ) transport = client.get_transport() assert transport is not None, f"SSH transport to {DOWNLOADS_SERVER} is None" # Ensure the file is there source = Path(__file__).parent / "add_to_pydotorg.py" destination = Path(f"/home/psf-users/{db['ssh_user']}/add_to_pydotorg.py") ftp_client = MySFTPClient.from_transport(transport) assert ftp_client is not None, f"SFTP client to {DOWNLOADS_SERVER} is None" ftp_client.put(str(source), str(destination)) ftp_client.close() auth_info = db["auth_info"] assert auth_info is not None # Do the interactive flow to get an identity for Sigstore issuer = sigstore.oidc.Issuer(sigstore.oidc.DEFAULT_OAUTH_ISSUER_URL) identity_token = issuer.identity_token() print("Adding files to python.org...") stdin, stdout, stderr = client.exec_command( f"AUTH_INFO={auth_info} SIGSTORE_IDENTITY_TOKEN={identity_token} python3 add_to_pydotorg.py {db['release']}" ) stderr_text = stderr.read().decode() if stderr_text: raise paramiko.SSHException(f"Failed to execute the command: {stderr_text}") stdout_text = stdout.read().decode() print("-- Command output --") print(stdout_text) print("-- End of command output --") def purge_the_cdn(db: ReleaseShelf) -> None: headers = { "User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6" } normalized_release = db["release"].normalized() urls = [ f"https://www.python.org/downloads/release/python-{str(db['release']).replace('.', '')}/", f"https://docs.python.org/release/{db['release']}/", f"https://www.python.org/ftp/python/{normalized_release}/", f"https://docs.python.org/release/{normalized_release}/", "https://www.python.org/downloads/", "https://www.python.org/downloads/windows/", "https://www.python.org/downloads/macos/", ] # Purge the source URLs and their associated metadata files. source_urls = [ f"https://www.python.org/ftp/python/{normalized_release}/Python-{db['release']}.tgz", f"https://www.python.org/ftp/python/{normalized_release}/Python-{db['release']}.tar.xz", ] for source_url in source_urls: urls.extend( [ f"{source_url}", f"{source_url}.asc", f"{source_url}.crt", f"{source_url}.sig", f"{source_url}.sigstore", f"{source_url}.spdx.json", ] ) for url in urls: req = urllib.request.Request(url=url, headers=headers, method="PURGE") # try: response = urllib.request.urlopen(req) if response.code != 200: raise RuntimeError("Failed to purge the python.org/downloads CDN") def announce_release(db: ReleaseShelf) -> None: if not ask_question( "Have you announced the release at https://discuss.python.org/c/core-dev/23 " "and https://blog.python.org?\n" "Tip: use the 'release' and 'releases' tags respectively." ): raise ReleaseException("The release has not been announced") def post_release_merge(db: ReleaseShelf) -> None: subprocess.check_call( ["git", "fetch", "--all"], cwd=db["git_repo"], ) release_tag: release_mod.Tag = db["release"] if release_tag.is_feature_freeze_release: subprocess.check_call( ["git", "checkout", "main"], cwd=db["git_repo"], ) else: subprocess.check_call( ["git", "checkout", release_tag.branch], cwd=db["git_repo"], ) subprocess.check_call( ["git", "merge", "--no-squash", f"v{db['release']}"], cwd=db["git_repo"], ) def post_release_tagging(db: ReleaseShelf) -> None: release_tag: release_mod.Tag = db["release"] subprocess.check_call( ["git", "fetch", "--all"], cwd=db["git_repo"], ) if release_tag.is_feature_freeze_release: checkout_branch = release_tag.basic_version else: checkout_branch = release_tag.branch subprocess.check_call( ["git", "checkout", checkout_branch], cwd=db["git_repo"], ) with cd(db["git_repo"]): release_mod.done(db["release"]) subprocess.check_call( ["git", "commit", "-a", "-m", f"Post {db['release']}"], cwd=db["git_repo"], ) def maybe_prepare_new_main_branch(db: ReleaseShelf) -> None: release_tag: release_mod.Tag = db["release"] if not release_tag.is_feature_freeze_release: return subprocess.check_call( ["git", "checkout", "main"], cwd=db["git_repo"], ) new_release = release_tag.next_minor_release() with cd(db["git_repo"]): release_mod.bump(new_release) prev_branch = f"{release_tag.major}.{release_tag.minor}" new_branch = f"{release_tag.major}.{int(release_tag.minor)+1}" whatsnew_file = f"Doc/whatsnew/{new_branch}.rst" with cd(db["git_repo"]), open(whatsnew_file, "w") as f: f.write(WHATS_NEW_TEMPLATE.format(version=new_branch, prev_version=prev_branch)) subprocess.check_call( ["git", "add", whatsnew_file], cwd=db["git_repo"], ) whatsnew_toctree_file = "Doc/whatsnew/index.rst" with cd(db["git_repo"]): update_whatsnew_toctree(db, whatsnew_toctree_file) subprocess.check_call( ["git", "add", whatsnew_toctree_file], cwd=db["git_repo"], ) subprocess.check_call( ["git", "commit", "-a", "-m", f"Python {new_release}"], cwd=db["git_repo"], ) def update_whatsnew_toctree(db: ReleaseShelf, filename: str) -> None: release_tag: release_mod.Tag = db["release"] this_rst = f" {release_tag.major}.{release_tag.minor}.rst" next_rst = f" {release_tag.major}.{release_tag.minor+1}.rst" new = next_rst + "\n" + this_rst with open(filename) as f: contents = f.read() contents = contents.replace(this_rst, new) with open(filename, "w") as f: f.write(contents) def branch_new_versions(db: ReleaseShelf) -> None: release_tag: release_mod.Tag = db["release"] if not release_tag.is_feature_freeze_release: return subprocess.check_call(["git", "checkout", "main"], cwd=db["git_repo"]) subprocess.check_call( ["git", "checkout", "-b", release_tag.basic_version], cwd=db["git_repo"], ) def is_mirror(repo: Path, remote: str) -> bool: """Return True if the `repo` directory was created with --mirror.""" cmd = ["git", "config", "--local", "--get", f"remote.{remote}.mirror"] try: out = subprocess.check_output(cmd, cwd=repo) except subprocess.CalledProcessError: return False return out.startswith(b"true") def push_to_local_fork(db: ReleaseShelf) -> None: def _push_to_local(dry_run: bool = False) -> None: git_command = ["git", "push"] if dry_run: git_command.append("--dry-run") git_command.append("origin") if not is_mirror(db["git_repo"], "origin"): # mirrors push everything always, specifying `--tags` or refspecs doesn't work. git_command += ["HEAD", "--tags"] subprocess.check_call( git_command, cwd=db["git_repo"], ) _push_to_local(dry_run=True) if not ask_question( "Does these operations look reasonable? ⚠️⚠️⚠️ Answering 'yes' will push to your origin remote ⚠️⚠️⚠️" ): raise ReleaseException("Something is wrong - Push to remote aborted") _push_to_local(dry_run=False) def push_to_upstream(db: ReleaseShelf) -> None: release_tag: release_mod.Tag = db["release"] def _push_to_upstream(dry_run: bool = False) -> None: branch = f"{release_tag.major}.{release_tag.minor}" git_command = ["git", "push"] if dry_run: git_command.append("--dry-run") if release_tag.is_alpha_release: subprocess.check_call( git_command + ["--tags", "git@github.com:python/cpython.git", "main"], cwd=db["git_repo"], ) elif release_tag.is_feature_freeze_release: subprocess.check_call( git_command + ["--tags", "git@github.com:python/cpython.git", branch], cwd=db["git_repo"], ) subprocess.check_call( git_command + ["--tags", "git@github.com:python/cpython.git", "main"], cwd=db["git_repo"], ) else: subprocess.check_call( git_command + ["--tags", "git@github.com:python/cpython.git", branch], cwd=db["git_repo"], ) _push_to_upstream(dry_run=True) if not ask_question( "Do these operations look reasonable? ⚠️⚠️⚠️ Answering 'yes' will push to the upstream repository ⚠️⚠️⚠️" ): raise ReleaseException("Something is wrong - Push to upstream aborted") if not ask_question( "Is the target branch unprotected for your user? " "Check at https://github.com/python/cpython/settings/branches" ): raise ReleaseException("The target branch is not unprotected for your user") _push_to_upstream(dry_run=False) def main() -> None: parser = argparse.ArgumentParser(description="Make a CPython release.") def _release_type(release: str) -> str: if not RELEASE_REGEXP.match(release): raise argparse.ArgumentTypeError("Invalid release string") return release parser.add_argument( "--release", dest="release", help="Release tag", required=True, type=_release_type, ) parser.add_argument( "--repository", dest="repo", help="Location of the CPython repository", required=True, type=str, ) def _api_key(api_key: str) -> str: if not API_KEY_REGEXP.match(api_key): raise argparse.ArgumentTypeError( "Invalid API key format. It must be on the form USER:API_KEY" ) return api_key parser.add_argument( "--auth-key", dest="auth_key", help="API key for python.org in the form 'USER:API_KEY'", type=_api_key, ) parser.add_argument( "--ssh-user", dest="ssh_user", default=getpass.getuser(), help="Username to be used when authenticating via ssh", type=str, ) parser.add_argument( "--ssh-key", dest="ssh_key", default=None, help="Path to the SSH key file to use for authentication", type=str, ) args = parser.parse_args() auth_key = args.auth_key or os.getenv("AUTH_INFO") assert isinstance(auth_key, str), "We need an AUTH_INFO env var or --auth-key" if sys.platform not in ("darwin", "linux"): print( """\ WARNING! This script has not been tested on a platform other than Linux and macOS. Although it should work correctly as long as you have all the dependencies, some things may not work as expected. As a release manager, you should try to fix these things in this script so it also supports your platform. """ ) if not ask_question("Do you want to continue?"): raise ReleaseException( "This release script is not compatible with the running platform" ) release_tag = release_mod.Tag(args.release) magic = release_tag.as_tuple() >= (3, 14) no_gpg = release_tag.as_tuple() >= (3, 14) # see PEP 761 tasks = [ Task(check_gh, "Checking gh is available"), Task(check_git, "Checking Git is available"), Task(check_make, "Checking make is available"), Task(check_blurb, "Checking blurb is available"), Task(check_docker, "Checking Docker is available"), Task(check_docker_running, "Checking Docker is running"), Task(check_autoconf, "Checking autoconf is available"), *([] if no_gpg else [Task(check_gpg_keys, "Checking GPG keys")]), Task( check_ssh_connection, f"Validating ssh connection to {DOWNLOADS_SERVER} and {DOCS_SERVER}", ), Task(check_sigstore_client, "Checking Sigstore CLI"), Task(check_buildbots, "Check buildbots are good"), Task(check_cpython_repo_branch, "Checking CPython repository branch"), Task(check_cpython_repo_age, "Checking CPython repository age"), Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"), *( [Task(check_magic_number, "Checking the magic number is up-to-date")] if magic else [] ), Task(prepare_temporary_branch, "Checking out a temporary release branch"), Task(run_blurb_release, "Run blurb release"), Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"), Task(prepare_pydoc_topics, "Preparing pydoc topics"), Task(bump_version, "Bump version"), Task(bump_version_in_docs, "Bump version in docs"), Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"), Task(run_autoconf, "Running autoconf"), Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"), Task(check_pyspecific, "Checking pyspecific"), Task(check_cpython_repo_is_clean, "Checking CPython repository is clean"), Task(create_tag, "Create tag"), Task(push_to_local_fork, "Push new tags and branches to private fork"), Task(start_build_release, "Start the build-release workflow"), Task( send_email_to_platform_release_managers, "Platform release managers have been notified of the commit SHA", ), Task(wait_for_build_release, "Wait for build-release workflow"), Task(check_doc_unreleased_version, "Check docs for `(unreleased)`"), Task(build_sbom_artifacts, "Building SBOM artifacts"), *([] if no_gpg else [Task(sign_source_artifacts, "Sign source artifacts")]), Task( upload_files_to_downloads_server, "Upload files to the PSF downloads server" ), Task(place_files_in_download_folder, "Place files in the download folder"), Task(upload_docs_to_the_docs_server, "Upload docs to the PSF docs server"), Task(unpack_docs_in_the_docs_server, "Place docs files in the docs folder"), Task(wait_until_all_files_are_in_folder, "Wait until all files are ready"), Task(create_release_object_in_db, "The Django release object has been created"), Task(post_release_merge, "Merge the tag into the release branch"), Task(branch_new_versions, "Branch out new versions and prepare main branch"), Task(post_release_tagging, "Final touches for the release"), Task( maybe_prepare_new_main_branch, "prepare new main branch for feature freeze", ), Task(push_to_upstream, "Push new tags and branches to upstream"), Task(remove_temporary_branch, "Removing temporary release branch"), Task(run_add_to_python_dot_org, "Add files to python.org download page"), Task(purge_the_cdn, "Purge the CDN of python.org/downloads"), Task(announce_release, "Announce the release"), ] automata = ReleaseDriver( git_repo=args.repo, release_tag=release_tag, api_key=auth_key, ssh_user=args.ssh_user, sign_gpg=not no_gpg, ssh_key=args.ssh_key, tasks=tasks, ) automata.run() if __name__ == "__main__": main() ================================================ FILE: sbom.py ================================================ """ Utility which creates Software Bill-of-Materials (SBOM) for CPython release artifacts. Can also be run manually with: $ python sbom.py For example: $ python sbom.py ./Python-3.13.0a3.tar.xz """ from __future__ import annotations import argparse import datetime import hashlib import io import json import os import re import subprocess import sys import tarfile import typing import zipfile from functools import cache from pathlib import Path from typing import Any, LiteralString, NotRequired, TypedDict, cast from urllib.request import urlopen class SBOM(TypedDict): SPDXID: str spdxVersion: str name: str dataLicense: str documentNamespace: str creationInfo: CreationInfo packages: list[Package] files: list[File] relationships: list[Relationship] class Package(TypedDict): SPDXID: str name: str versionInfo: str packageFileName: NotRequired[str] supplier: NotRequired[str] originator: NotRequired[str] licenseConcluded: str downloadLocation: str checksums: list[Checksum] primaryPackagePurpose: str packageVerificationCode: NotRequired[PackageVerificationCode] externalRefs: list[Ref] filesAnalyzed: NotRequired[bool] class File(TypedDict): SPDXID: str fileName: str checksums: list[Checksum] class Relationship(TypedDict): spdxElementId: str relatedSpdxElement: str relationshipType: str class Checksum(TypedDict): algorithm: str checksumValue: str class PackageVerificationCode(TypedDict): packageVerificationCodeValue: str class Ref(TypedDict): referenceCategory: str referenceLocator: str referenceType: str class CreationInfo(TypedDict): created: str # timestamp creators: list[str] licenseListVersion: str # Cache of values that we've seen already. We use this # to de-duplicate values and their corresponding SPDX ID. _SPDX_IDS_TO_VALUES: dict[str, Any] = {} @cache def spdx_id(value: LiteralString) -> str: """Encode a value into characters that are valid in an SPDX ID""" value_as_spdx_id = re.sub(r"[^a-zA-Z0-9.\-]+", "-", value) # The happy path is there are no collisions. # But collisions can happen, especially in file paths. # We append a hash suffix in those cases. if _SPDX_IDS_TO_VALUES.setdefault(value_as_spdx_id, value) != value: suffix = hashlib.sha256(value.encode()).hexdigest()[:8] value_as_spdx_id = f"{value_as_spdx_id}-{suffix}" assert _SPDX_IDS_TO_VALUES.setdefault(value_as_spdx_id, value) == value return value_as_spdx_id def calculate_package_verification_codes(sbom: SBOM) -> None: """ Calculate SPDX 'packageVerificationCode' values for each package with 'filesAnalyzed' set to 'true'. Mutates the values within the passed structure. The code is SHA1 of a concatenated and sorted list of file SHA1s. """ # Find all packages which we need to calculate package verification codes for. sbom_file_id_to_package_id = {} sbom_package_id_to_file_sha1s: dict[str, list[bytes]] = {} for sbom_package in sbom["packages"]: # If this value is 'false' we skip calculating. if sbom_package.get("filesAnalyzed", False): sbom_package_id = sbom_package["SPDXID"] sbom_package_id_to_file_sha1s[sbom_package_id] = [] # Next pass we do is over relationships, # we need to find all files that belong to each package. for sbom_relationship in sbom["relationships"]: sbom_relationship_type = sbom_relationship["relationshipType"] sbom_element_id = sbom_relationship["spdxElementId"] sbom_related_element_id = sbom_relationship["relatedSpdxElement"] # We're looking for ' CONTAINS ' relationships if ( sbom_relationship_type != "CONTAINS" or sbom_element_id not in sbom_package_id_to_file_sha1s or not sbom_related_element_id.startswith("SPDXRef-FILE-") ): continue # Found one! Add it to our mapping. sbom_file_id_to_package_id[sbom_related_element_id] = sbom_element_id # Now we do a single pass on files, appending all SHA1 values along the way. for sbom_file in sbom["files"]: # Attempt to match this file to a package. sbom_file_id = sbom_file["SPDXID"] if sbom_file_id not in sbom_file_id_to_package_id: continue sbom_package_id = sbom_file_id_to_package_id[sbom_file_id] # Find the SHA1 checksum for the file. for sbom_file_checksum in sbom_file["checksums"]: if sbom_file_checksum["algorithm"] == "SHA1": # We lowercase the value as that's what's required by the algorithm. sbom_file_checksum_sha1 = ( sbom_file_checksum["checksumValue"].lower().encode("ascii") ) break else: raise ValueError(f"Can't find SHA1 checksum for '{sbom_file_id}'") sbom_package_id_to_file_sha1s[sbom_package_id].append(sbom_file_checksum_sha1) # Finally we iterate over the packages again and calculate the final package verification code values. for sbom_package in sbom["packages"]: sbom_package_id = sbom_package["SPDXID"] if sbom_package_id not in sbom_package_id_to_file_sha1s: continue # Package verification code is the SHA1 of ASCII values ascending-sorted. sbom_package_verification_code = hashlib.sha1( b"".join(sorted(sbom_package_id_to_file_sha1s[sbom_package_id])) ).hexdigest() sbom_package["packageVerificationCode"] = { "packageVerificationCodeValue": sbom_package_verification_code } def get_release_tools_commit_sha() -> str: """Gets the git commit SHA of the release-tools repository""" git_prefix = os.path.abspath(os.path.dirname(__file__)) stdout = ( subprocess.check_output( ["git", "rev-parse", "--prefix", git_prefix, "HEAD"], cwd=git_prefix ) .decode("ascii") .strip() ) assert re.fullmatch(r"^[a-f0-9]{40,}$", stdout) return stdout def normalize_sbom_data(sbom_data: SBOM) -> None: """ Normalize SBOM data in-place by recursion and sorting lists by some repeatable key. """ def recursive_sort_in_place(value: list[Any] | dict[str, Any]) -> None: if isinstance(value, list): # We need to recurse first so bottom-most elements are sorted first. for item in value: recursive_sort_in_place(item) # Otherwise this key might change depending on the unsorted order of items. value.sort(key=lambda item: json.dumps(item, sort_keys=True)) # Dictionaries are the only other containers and keys # are already handled by json.dumps(sort_keys=True). elif isinstance(value, dict): for dict_val in value.values(): recursive_sort_in_place(dict_val) recursive_sort_in_place(cast(dict[str, Any], sbom_data)) def check_sbom_data(sbom_data: SBOM) -> None: """Check SBOM data for common issues""" def check_id_duplicates(sbom_components: list[Package] | list[File]) -> set[str]: all_ids = set() for sbom_component in sbom_components: sbom_component_id = sbom_component["SPDXID"] assert sbom_component_id not in all_ids all_ids.add(sbom_component_id) return all_ids all_package_ids = check_id_duplicates(sbom_data["packages"]) all_file_ids = check_id_duplicates(sbom_data["files"]) # Check that no files and packages have the same ID. assert not all_package_ids.intersection(all_file_ids) all_sbom_ids = all_package_ids | all_file_ids # Check that all relationships use existing IDs. for sbom_relationship in sbom_data["relationships"]: # The exception being 'DESCRIBES' with the meta 'document' ID if ( sbom_relationship["spdxElementId"] == "SPDXRef-DOCUMENT" and sbom_relationship["relationshipType"] == "DESCRIBES" ): continue assert sbom_relationship["spdxElementId"] in all_sbom_ids assert sbom_relationship["relatedSpdxElement"] in all_sbom_ids def fetch_package_metadata_from_pypi( project: str, version: str, filename: str | None = None ) -> tuple[str, str]: """ Fetches the SHA256 checksum and download location from PyPI. If we're given a filename then we match with that, otherwise we use wheels. """ # Get the package download URL from PyPI. try: raw_text = urlopen(f"https://pypi.org/pypi/{project}/{version}/json").read() release_metadata = json.loads(raw_text) url: dict[str, typing.Any] # Look for a matching artifact filename and then check # its remote checksum to the local one. for url in release_metadata["urls"]: # pip can only use Python-only dependencies, so there's # no risk of picking the 'incorrect' wheel here. if (filename is None and url["packagetype"] == "bdist_wheel") or ( filename is not None and url["filename"] == filename ): break else: raise ValueError(f"No matching filename on PyPI for '{filename}'") # Successfully found the download URL for the matching artifact. download_url = url["url"] checksum_sha256 = url["digests"]["sha256"] return download_url, checksum_sha256 except Exception as e: raise ValueError( f"Couldn't fetch metadata for project '{project}' from PyPI: {e}" ) def remove_pip_from_sbom(sbom_data: SBOM) -> None: """ Removes pip and its dependencies from the SBOM data. This is only necessary if there's potential we get pip SBOM data from the CPython source SBOM. """ sbom_pip_spdx_id = spdx_id("SPDXRef-PACKAGE-pip") sbom_spdx_ids_to_remove = {sbom_pip_spdx_id} # Find all package SPDXIDs that pip depends on. for sbom_relationship in sbom_data["relationships"]: if ( sbom_relationship["relationshipType"] == "DEPENDS_ON" and sbom_relationship["spdxElementId"] == sbom_pip_spdx_id ): sbom_spdx_ids_to_remove.add(sbom_relationship["relatedSpdxElement"]) # Remove all the packages and relationships. sbom_data["packages"] = [ sbom_package for sbom_package in sbom_data["packages"] if sbom_package["SPDXID"] not in sbom_spdx_ids_to_remove ] sbom_data["relationships"] = [ sbom_relationship for sbom_relationship in sbom_data["relationships"] if sbom_relationship["relatedSpdxElement"] not in sbom_spdx_ids_to_remove ] def create_pip_sbom_from_wheel( sbom_data: SBOM, pip_wheel_filename: str, pip_wheel_bytes: bytes ) -> None: """ pip is a part of a packaging ecosystem (Python, surprise!) so it's actually automatable to discover the metadata we need like the version and checksums so let's do that on behalf of our friends at the PyPA. This function also discovers vendored packages within pip and fetches their metadata. """ # Remove pip from the SBOM in case it's included in the CPython source code SBOM. remove_pip_from_sbom(sbom_data) # Wheel filename format puts the version right after the project name. pip_version = pip_wheel_filename.split("-")[1] pip_checksum_sha256 = hashlib.sha256(pip_wheel_bytes).hexdigest() pip_download_url, pip_actual_sha256 = fetch_package_metadata_from_pypi( project="pip", version=pip_version, filename=pip_wheel_filename, ) if pip_actual_sha256 != pip_checksum_sha256: raise ValueError("pip wheel checksum doesn't match PyPI") # Parse 'pip/_vendor/vendor.txt' from the wheel for sub-dependencies. with zipfile.ZipFile(io.BytesIO(pip_wheel_bytes)) as whl: vendor_txt_data = whl.read("pip/_vendor/vendor.txt").decode() # With this version regex we're assuming that pip isn't using pre-releases. # If any version doesn't match we get a failure below, so we're safe doing this. version_pin_re = re.compile(r"^([a-zA-Z0-9_.-]+)==([0-9.]*[0-9])$") sbom_pip_dependency_spdx_ids = set() for line in vendor_txt_data.splitlines(): line = line.partition("#")[0].strip() # Strip comments and whitespace. if not line: # Skip empty lines. continue # Non-empty lines we must be able to match. match = version_pin_re.match(line) assert ( match is not None ), f"Unparseable line in vendor.txt: {line!r}" # Make mypy happy. # Parse out and normalize the project name. project_name, project_version = match.groups() project_name = project_name.lower() # Fetch the metadata from PyPI project_download_url, project_checksum_sha256 = ( fetch_package_metadata_from_pypi(project_name, project_version) ) # Update our SBOM data with what we received from PyPI. sbom_project_spdx_id = spdx_id(f"SPDXRef-PACKAGE-{project_name}") sbom_pip_dependency_spdx_ids.add(sbom_project_spdx_id) sbom_data["packages"].append( { "SPDXID": sbom_project_spdx_id, "name": project_name, "versionInfo": project_version, "downloadLocation": project_download_url, "checksums": [ { "algorithm": "SHA256", "checksumValue": project_checksum_sha256, } ], "externalRefs": [ { "referenceCategory": "PACKAGE_MANAGER", "referenceLocator": f"pkg:pypi/{project_name}@{project_version}", "referenceType": "purl", }, ], "primaryPackagePurpose": "SOURCE", "licenseConcluded": "NOASSERTION", } ) # Now we add pip to the SBOM and dependency relationships sbom_pip_spdx_id = spdx_id("SPDXRef-PACKAGE-pip") sbom_data["packages"].append( { "SPDXID": sbom_pip_spdx_id, "name": "pip", "versionInfo": pip_version, "originator": "Organization: Python Packaging Authority", "licenseConcluded": "NOASSERTION", "downloadLocation": pip_download_url, "checksums": [ {"algorithm": "SHA256", "checksumValue": pip_checksum_sha256} ], "externalRefs": [ { "referenceCategory": "SECURITY", "referenceLocator": f"cpe:2.3:a:pypa:pip:{pip_version}:*:*:*:*:*:*:*", "referenceType": "cpe23Type", }, { "referenceCategory": "PACKAGE_MANAGER", "referenceLocator": f"pkg:pypi/pip@{pip_version}", "referenceType": "purl", }, ], "primaryPackagePurpose": "SOURCE", } ) for sbom_dep_spdx_id in sorted(sbom_pip_dependency_spdx_ids): sbom_data["relationships"].append( { "spdxElementId": sbom_pip_spdx_id, "relatedSpdxElement": sbom_dep_spdx_id, "relationshipType": "DEPENDS_ON", } ) # Finally, CPython depends on pip. sbom_data["relationships"].append( { "spdxElementId": "SPDXRef-PACKAGE-cpython", "relatedSpdxElement": sbom_pip_spdx_id, "relationshipType": "DEPENDS_ON", } ) def create_cpython_sbom( sbom_data: SBOM, cpython_version: str, artifact_path: str, ) -> None: """Creates the top-level SBOM metadata and the CPython SBOM package.""" if m := re.match(pat := r"^([0-9.]+)", cpython_version): cpython_version_without_suffix = m.group(1) else: raise ValueError(f"Invalid {cpython_version=}, expected {pat!r}") artifact_name = os.path.basename(artifact_path) artifact_download_location = f"https://www.python.org/ftp/python/{cpython_version_without_suffix}/{artifact_name}" # Take a hash of the artifact with open(artifact_path, mode="rb") as f: artifact_checksum_sha256 = hashlib.sha256(f.read()).hexdigest() sbom_data.update( { "SPDXID": "SPDXRef-DOCUMENT", "spdxVersion": "SPDX-2.3", "name": "CPython SBOM", "dataLicense": "CC0-1.0", # Naming done according to OpenSSF SBOM WG recommendations. # See: https://github.com/ossf/sbom-everywhere/blob/main/reference/sbom_naming.md "documentNamespace": f"{artifact_download_location}.spdx.json", "creationInfo": { "created": ( datetime.datetime.now(tz=datetime.timezone.utc).strftime( "%Y-%m-%dT%H:%M:%SZ" ) ), "creators": [ "Person: Python Release Managers", f"Tool: ReleaseTools-{get_release_tools_commit_sha()}", ], # Version of the SPDX License ID list. # This shouldn't need to be updated often, if ever. "licenseListVersion": "3.22", }, } ) # Create the SBOM entry for the CPython package. We use # the SPDXID later on for creating relationships to files. sbom_cpython_package: Package = { "SPDXID": "SPDXRef-PACKAGE-cpython", "name": "CPython", "versionInfo": cpython_version, "licenseConcluded": "PSF-2.0", "originator": "Organization: Python Software Foundation", "supplier": "Organization: Python Software Foundation", "packageFileName": artifact_name, "externalRefs": [ { "referenceCategory": "SECURITY", "referenceLocator": f"cpe:2.3:a:python:python:{cpython_version}:*:*:*:*:*:*:*", "referenceType": "cpe23Type", } ], "primaryPackagePurpose": "SOURCE", "downloadLocation": artifact_download_location, "checksums": [ {"algorithm": "SHA256", "checksumValue": artifact_checksum_sha256} ], } # The top-level CPython package depends on every vendored sub-package. for sbom_package in sbom_data["packages"]: sbom_data["relationships"].append( { "spdxElementId": sbom_cpython_package["SPDXID"], "relatedSpdxElement": sbom_package["SPDXID"], "relationshipType": "DEPENDS_ON", } ) sbom_data["packages"].append(sbom_cpython_package) def create_sbom_for_source_tarball(tarball_path: str) -> SBOM: """Stitches together an SBOM for a source tarball""" tarball_name = os.path.basename(tarball_path) # Open the tarball with known compression settings. if tarball_name.endswith(".tgz"): tarball = tarfile.open(tarball_path, mode="r:gz") elif tarball_name.endswith(".tar.xz"): tarball = tarfile.open(tarball_path, mode="r:xz") else: raise ValueError(f"Unknown tarball format: '{tarball_name}'") # Parse the CPython version from the tarball. # Calculate the download locations from the CPython version and tarball name. if m := re.match(pat := r"^Python-([0-9abrc.]+)\.t", tarball_name): cpython_version = m.group(1) else: raise ValueError(f"Invalid {tarball_name=}, expected {pat!r}") # There should be an SBOM included in the tarball. # If there's not we can't create an SBOM. try: sbom_tarball_member = tarball.getmember( f"Python-{cpython_version}/Misc/sbom.spdx.json" ) except KeyError: raise ValueError( "Tarball doesn't contain an SBOM at 'Misc/sbom.spdx.json'" ) from None reader = tarball.extractfile(sbom_tarball_member) assert reader, f"{sbom_tarball_member} is not a file in {tarball_path}" sbom_bytes = reader.read() sbom_data: SBOM = json.loads(sbom_bytes) create_cpython_sbom( sbom_data, cpython_version=cpython_version, artifact_path=tarball_path ) sbom_cpython_package_spdx_id = spdx_id("SPDXRef-PACKAGE-cpython") # Find the pip wheel in ensurepip in the tarball for member in tarball.getmembers(): if match := re.match( rf"^Python-{cpython_version}/Lib/ensurepip/_bundled/(pip-.*\.whl)$", member.name, ): pip_wheel_filename = match.group(1) reader = tarball.extractfile(member) assert reader, f"{member} is not a file in {tarball_path}" pip_wheel_bytes = reader.read() break else: raise ValueError("Could not find pip wheel in 'Lib/ensurepip/_bundled/...'") # Now add pip to the SBOM. We do this after the above step to avoid # CPython being dependent on packages that pip is dependent on. create_pip_sbom_from_wheel( sbom_data=sbom_data, pip_wheel_filename=pip_wheel_filename, pip_wheel_bytes=pip_wheel_bytes, ) # Extract all currently known files from the SBOM with their checksums. known_sbom_files = {} for sbom_file in sbom_data["files"]: sbom_filename = sbom_file["fileName"] # Look for the expected SHA256 checksum. for sbom_file_checksum in sbom_file["checksums"]: if sbom_file_checksum["algorithm"] == "SHA256": known_sbom_files[sbom_filename] = sbom_file_checksum["checksumValue"] break else: raise ValueError( f"Couldn't find expected SHA256 checksum in SBOM for file '{sbom_filename}'" ) # Now we walk the tarball and compare known files to our expected checksums in the SBOM. # All files that aren't already in the SBOM can be added as "CPython" files. for member in tarball.getmembers(): if not member.isfile(): # Only keep files (no symlinks) continue # Get the member from the tarball. CPython prefixes all of its # source code with 'Python-{version}/...'. assert member.name.startswith(f"Python-{cpython_version}/") # Calculate the hashes, either for comparison with a known value # or to embed in the SBOM as a new file. SHA1 is only used because # SPDX requires it for all file entries. reader = tarball.extractfile(member) assert reader, f"{member} is not a file in {tarball_path}" file_bytes = reader.read() actual_file_checksum_sha1 = hashlib.sha1(file_bytes).hexdigest() actual_file_checksum_sha256 = hashlib.sha256(file_bytes).hexdigest() # Remove the 'Python-{version}/...' prefix for the SPDXID and fileName. member_name_no_prefix = member.name.split("/", 1)[1] # We've already seen this file, so we check it hasn't been modified and continue on. if member_name_no_prefix in known_sbom_files: # If there's a hash mismatch we raise an error, something isn't right! expected_file_checksum_sha256 = known_sbom_files.pop(member_name_no_prefix) if expected_file_checksum_sha256 != actual_file_checksum_sha256: raise ValueError( f"Mismatched checksum for file '{member_name_no_prefix}'" ) # If this is a new file, then it's a part of the 'CPython' SBOM package. else: sbom_file_spdx_id = spdx_id(f"SPDXRef-FILE-{member_name_no_prefix}") sbom_data["files"].append( { "SPDXID": sbom_file_spdx_id, "fileName": member_name_no_prefix, "checksums": [ { "algorithm": "SHA1", "checksumValue": actual_file_checksum_sha1, }, { "algorithm": "SHA256", "checksumValue": actual_file_checksum_sha256, }, ], } ) sbom_data["relationships"].append( { "spdxElementId": sbom_cpython_package_spdx_id, "relatedSpdxElement": sbom_file_spdx_id, "relationshipType": "CONTAINS", } ) # If there are any known files that weren't found in the # source tarball we want to raise an error. if known_sbom_files: raise ValueError( f"Some files from source SBOM aren't accounted for " f"in source tarball: {sorted(known_sbom_files)!r}" ) # Final relationship, this SBOM describes the CPython package. sbom_data["relationships"].append( { "spdxElementId": "SPDXRef-DOCUMENT", "relatedSpdxElement": sbom_cpython_package_spdx_id, "relationshipType": "DESCRIBES", } ) # Apply the 'supplier' tag to every package since we're shipping # the package in the tarball itself. Originator field is used for maintainers. for sbom_package in sbom_data["packages"]: sbom_package["supplier"] = "Organization: Python Software Foundation" sbom_package["filesAnalyzed"] = True # Calculate the 'packageVerificationCode' values for files in packages. calculate_package_verification_codes(sbom_data) return sbom_data def create_sbom_for_windows_artifact( artifact_path: str, cpython_source_dir: Path | str ) -> SBOM: artifact_name = os.path.basename(artifact_path) if m := re.match(pat := r"^python-([0-9abrc.]+)t?(?:-|\.exe|\.zip)", artifact_name): cpython_version = m.group(1) else: raise ValueError(f"Invalid {artifact_name=}, expected {pat!r}") if not cpython_source_dir: raise ValueError("Must specify --cpython-source-dir for Windows artifacts") cpython_source_dir = Path(cpython_source_dir) # Start with the CPython source SBOM as a base with (cpython_source_dir / "Misc/externals.spdx.json").open() as f: sbom_data: SBOM = json.loads(f.read()) sbom_data["relationships"] = [] sbom_data["files"] = [] # Add all the packages from the source SBOM # We want to skip the file information because # the files aren't available in Windows artifacts. with (cpython_source_dir / "Misc/sbom.spdx.json").open() as f: source_sbom_data = json.loads(f.read()) for sbom_package in source_sbom_data["packages"]: # Update the SPDX ID to avoid collisions with # the 'externals' SBOM. sbom_package["SPDXID"] = spdx_id( f"SPDXRef-PACKAGE-{sbom_package['name']}-{sbom_package['versionInfo']}" ) sbom_data["packages"].append(sbom_package) create_cpython_sbom( sbom_data, cpython_version=cpython_version, artifact_path=artifact_path ) sbom_cpython_package_spdx_id = spdx_id("SPDXRef-PACKAGE-cpython") # The Windows embed artifacts don't contain pip/ensurepip, # but the others do. if "-embed" not in artifact_name: # Find the pip wheel in ensurepip in the source code for pathname in os.listdir(cpython_source_dir / "Lib/ensurepip/_bundled"): if pathname.startswith("pip-") and pathname.endswith(".whl"): pip_wheel_filename = pathname pip_wheel_bytes = ( cpython_source_dir / f"Lib/ensurepip/_bundled/{pathname}" ).read_bytes() break else: raise ValueError("Could not find pip wheel in 'Lib/ensurepip/_bundled/...'") create_pip_sbom_from_wheel( sbom_data, pip_wheel_filename=pip_wheel_filename, pip_wheel_bytes=pip_wheel_bytes, ) # Final relationship, this SBOM describes the CPython package. sbom_data["relationships"].append( { "spdxElementId": "SPDXRef-DOCUMENT", "relatedSpdxElement": sbom_cpython_package_spdx_id, "relationshipType": "DESCRIBES", } ) # Apply the 'supplier' tag to every package since we're shipping # the package in the artifact itself. Originator field is used for maintainers. for sbom_package in sbom_data["packages"]: sbom_package["supplier"] = "Organization: Python Software Foundation" # Source packages have been compiled. if sbom_package["primaryPackagePurpose"] == "SOURCE": sbom_package["primaryPackagePurpose"] = "LIBRARY" return sbom_data def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--cpython-source-dir", default=None) parser.add_argument("artifacts", nargs="+") parsed_args = parser.parse_args(sys.argv[1:]) artifact_paths = parsed_args.artifacts cpython_source_dir = parsed_args.cpython_source_dir for artifact_path in artifact_paths: # Windows MSI and Embed artifacts if artifact_path.endswith(".exe") or artifact_path.endswith(".zip"): sbom_data = create_sbom_for_windows_artifact( artifact_path, cpython_source_dir=cpython_source_dir ) # Source artifacts else: sbom_data = create_sbom_for_source_tarball(artifact_path) # Normalize SBOM data for reproducibility. normalize_sbom_data(sbom_data) # Check SBOM for validity. check_sbom_data(sbom_data) with open(artifact_path + ".spdx.json", mode="w") as f: f.truncate() f.write(json.dumps(sbom_data, indent=2, sort_keys=True)) if __name__ == "__main__": main() ================================================ FILE: select_jobs.py ================================================ #!/usr/bin/env python3 import argparse from release import Tag def output(key: str, value: bool) -> None: print(f"{key}={str(value).lower()}") def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("version", type=Tag) parser.add_argument( "--test", action="store_true", help="Enable all jobs for testing", ) args = parser.parse_args() version = args.version if args.test: # When testing the workflow itself (push/PR), # enable all jobs for full coverage. output("docs", True) output("android", True) output("ios", True) return # Docs are only built for stable releases or release candidates. output("docs", version.level in ["rc", "f"]) # Android binary releases began in Python 3.14. output("android", version.as_tuple() >= (3, 14)) # iOS binary releases began in Python 3.15. output("ios", version.as_tuple() >= (3, 15)) if __name__ == "__main__": main() ================================================ FILE: tests/README.rst ================================================ This is Python version 3.14.0 alpha 3 ===================================== .. image:: https://github.com/python/cpython/actions/workflows/build.yml/badge.svg?branch=main&event=push :alt: CPython build status on GitHub Actions :target: https://github.com/python/cpython/actions .. image:: https://dev.azure.com/python/cpython/_apis/build/status/Azure%20Pipelines%20CI?branchName=main :alt: CPython build status on Azure DevOps :target: https://dev.azure.com/python/cpython/_build/latest?definitionId=4&branchName=main .. image:: https://img.shields.io/badge/discourse-join_chat-brightgreen.svg :alt: Python Discourse chat :target: https://discuss.python.org/ Copyright © 2001 Python Software Foundation. All rights reserved. See the end of this file for further copyright and license information. .. contents:: General Information ------------------- - Website: https://www.python.org - Source code: https://github.com/python/cpython - Issue tracker: https://github.com/python/cpython/issues - Documentation: https://docs.python.org - Developer's Guide: https://devguide.python.org/ Contributing to CPython ----------------------- For more complete instructions on contributing to CPython development, see the `Developer Guide`_. .. _Developer Guide: https://devguide.python.org/ Using Python ------------ Installable Python kits, and information about using Python, are available at `python.org`_. .. _python.org: https://www.python.org/ Build Instructions ------------------ On Unix, Linux, BSD, macOS, and Cygwin:: ./configure make make test sudo make install This will install Python as ``python3``. You can pass many options to the configure script; run ``./configure --help`` to find out more. On macOS case-insensitive file systems and on Cygwin, the executable is called ``python.exe``; elsewhere it's just ``python``. Building a complete Python installation requires the use of various additional third-party libraries, depending on your build platform and configure options. Not all standard library modules are buildable or usable on all platforms. Refer to the `Install dependencies `_ section of the `Developer Guide`_ for current detailed information on dependencies for various Linux distributions and macOS. On macOS, there are additional configure and build options related to macOS framework and universal builds. Refer to `Mac/README.rst `_. On Windows, see `PCbuild/readme.txt `_. To build Windows installer, see `Tools/msi/README.txt `_. If you wish, you can create a subdirectory and invoke configure from there. For example:: mkdir debug cd debug ../configure --with-pydebug make make test (This will fail if you *also* built at the top-level directory. You should do a ``make clean`` at the top-level first.) To get an optimized build of Python, ``configure --enable-optimizations`` before you run ``make``. This sets the default make targets up to enable Profile Guided Optimization (PGO) and may be used to auto-enable Link Time Optimization (LTO) on some platforms. For more details, see the sections below. Profile Guided Optimization ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGO takes advantage of recent versions of the GCC or Clang compilers. If used, either via ``configure --enable-optimizations`` or by manually running ``make profile-opt`` regardless of configure flags, the optimized build process will perform the following steps: The entire Python directory is cleaned of temporary files that may have resulted from a previous compilation. An instrumented version of the interpreter is built, using suitable compiler flags for each flavor. Note that this is just an intermediary step. The binary resulting from this step is not good for real-life workloads as it has profiling instructions embedded inside. After the instrumented interpreter is built, the Makefile will run a training workload. This is necessary in order to profile the interpreter's execution. Note also that any output, both stdout and stderr, that may appear at this step is suppressed. The final step is to build the actual interpreter, using the information collected from the instrumented one. The end result will be a Python binary that is optimized; suitable for distribution or production installation. Link Time Optimization ^^^^^^^^^^^^^^^^^^^^^^ Enabled via configure's ``--with-lto`` flag. LTO takes advantage of the ability of recent compiler toolchains to optimize across the otherwise arbitrary ``.o`` file boundary when building final executables or shared libraries for additional performance gains. What's New ---------- We have a comprehensive overview of the changes in the `What's New in Python 3.14 `_ document. For a more detailed change log, read `Misc/NEWS `_, but a full accounting of changes can only be gleaned from the `commit history `_. If you want to install multiple versions of Python, see the section below entitled "Installing multiple versions". Documentation ------------- `Documentation for Python 3.14 `_ is online, updated daily. It can also be downloaded in many formats for faster access. The documentation is downloadable in HTML, EPUB, and reStructuredText formats; the latter version is primarily for documentation authors, translators, and people with special formatting requirements. For information about building Python's documentation, refer to `Doc/README.rst `_. Testing ------- To test the interpreter, type ``make test`` in the top-level directory. The test set produces some output. You can generally ignore the messages about skipped tests due to optional features which can't be imported. If a message is printed about a failed test or a traceback or core dump is produced, something is wrong. By default, tests are prevented from overusing resources like disk space and memory. To enable these tests, run ``make buildbottest``. If any tests fail, you can re-run the failing test(s) in verbose mode. For example, if ``test_os`` and ``test_gdb`` failed, you can run:: make test TESTOPTS="-v test_os test_gdb" If the failure persists and appears to be a problem with Python rather than your environment, you can `file a bug report `_ and include relevant output from that command to show the issue. See `Running & Writing Tests `_ for more on running tests. Installing multiple versions ---------------------------- On Unix and Mac systems if you intend to install multiple versions of Python using the same installation prefix (``--prefix`` argument to the configure script) you must take care that your primary python executable is not overwritten by the installation of a different version. All files and directories installed using ``make altinstall`` contain the major and minor version and can thus live side-by-side. ``make install`` also creates ``${prefix}/bin/python3`` which refers to ``${prefix}/bin/python3.X``. If you intend to install multiple versions using the same prefix you must decide which version (if any) is your "primary" version. Install that version using ``make install``. Install all other versions using ``make altinstall``. For example, if you want to install Python 2.7, 3.6, and 3.14 with 3.14 being the primary version, you would execute ``make install`` in your 3.14 build directory and ``make altinstall`` in the others. Release Schedule ---------------- See `PEP 745 `__ for Python 3.14 release details. Copyright and License Information --------------------------------- Copyright © 2001 Python Software Foundation. All rights reserved. Copyright © 2000 BeOpen.com. All rights reserved. Copyright © 1995-2001 Corporation for National Research Initiatives. All rights reserved. Copyright © 1991-1995 Stichting Mathematisch Centrum. All rights reserved. See the `LICENSE `_ for information on the history of this software, terms & conditions for usage, and a DISCLAIMER OF ALL WARRANTIES. This Python distribution contains *no* GNU General Public License (GPL) code, so it may be used in proprietary projects. There are interfaces to some GNU code but these are entirely optional. All trademarks referenced herein are property of their respective holders. ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/buildbotapi/builders.json ================================================ { "builders": [ { "builderid": 3, "description": null, "description_format": null, "description_html": null, "masterids": [ 1 ], "name": "AMD64 RHEL8 LTO 3.13", "projectid": null, "tags": [ "3.13", "stable", "lto", "nondebug", "tier-1" ] }, { "builderid": 1623, "description": null, "description_format": null, "description_html": null, "masterids": [ 1 ], "name": "AMD64 Windows PGO NoGIL PR", "projectid": null, "tags": [ "PullRequest", "unstable", "win64", "nogil", "nondebug", "pgo", "tier-1" ] } ], "meta": { "total": 3 } } ================================================ FILE: tests/buildbotapi/failure.json ================================================ { "builds": [ { "builderid": 3, "buildid": 1732278, "buildrequestid": 2341889, "complete": true, "complete_at": 1734198808, "locks_duration_s": 0, "masterid": 1, "number": 228, "properties": {}, "results": 2, "started_at": 1734197714, "state_string": "failed test (failure)", "workerid": 28 } ], "meta": { "total": 1 } } ================================================ FILE: tests/buildbotapi/no-builds.json ================================================ { "builds": [ ], "meta": { "total": 0 } } ================================================ FILE: tests/buildbotapi/success.json ================================================ { "builds": [ { "builderid": 3, "buildid": 1645411, "buildrequestid": 2211085, "complete": true, "complete_at": 1728312495, "locks_duration_s": 531, "masterid": 1, "number": 6844, "properties": {}, "results": 0, "started_at": 1728311538, "state_string": "build successful", "workerid": 27 } ], "meta": { "total": 1 } } ================================================ FILE: tests/fake-artifact.txt ================================================ ================================================ FILE: tests/fake-ftp-files.txt ================================================ # Test data only, doesn't need to be updated for each release Python-3.14.0a1.tar.xz Python-3.14.0a1.tar.xz.crt Python-3.14.0a1.tar.xz.sig Python-3.14.0a1.tar.xz.sigstore Python-3.14.0a1.tar.xz.spdx.json Python-3.14.0a1.tgz Python-3.14.0a1.tgz.crt Python-3.14.0a1.tgz.sig Python-3.14.0a1.tgz.sigstore Python-3.14.0a1.tgz.spdx.json Python-3.14.0a2.tar.xz Python-3.14.0a2.tar.xz.crt Python-3.14.0a2.tar.xz.sig Python-3.14.0a2.tar.xz.sigstore Python-3.14.0a2.tar.xz.spdx.json Python-3.14.0a2.tgz Python-3.14.0a2.tgz.crt Python-3.14.0a2.tgz.sig Python-3.14.0a2.tgz.sigstore Python-3.14.0a2.tgz.spdx.json Python-3.14.0a3.tar.xz Python-3.14.0a3.tar.xz.crt Python-3.14.0a3.tar.xz.sig Python-3.14.0a3.tar.xz.sigstore Python-3.14.0a3.tar.xz.spdx.json Python-3.14.0a3.tgz Python-3.14.0a3.tgz.crt Python-3.14.0a3.tgz.sig Python-3.14.0a3.tgz.sigstore Python-3.14.0a3.tgz.spdx.json Python-3.14.0a4.tar.xz Python-3.14.0a4.tar.xz.crt Python-3.14.0a4.tar.xz.sig Python-3.14.0a4.tar.xz.sigstore Python-3.14.0a4.tar.xz.spdx.json Python-3.14.0a4.tgz Python-3.14.0a4.tgz.crt Python-3.14.0a4.tgz.sig Python-3.14.0a4.tgz.sigstore Python-3.14.0a4.tgz.spdx.json Python-3.14.0a5.tar.xz Python-3.14.0a5.tar.xz.crt Python-3.14.0a5.tar.xz.sig Python-3.14.0a5.tar.xz.sigstore Python-3.14.0a5.tar.xz.spdx.json Python-3.14.0a5.tgz Python-3.14.0a5.tgz.crt Python-3.14.0a5.tgz.sig Python-3.14.0a5.tgz.sigstore Python-3.14.0a5.tgz.spdx.json Python-3.14.0a6.tar.xz Python-3.14.0a6.tar.xz.crt Python-3.14.0a6.tar.xz.sig Python-3.14.0a6.tar.xz.sigstore Python-3.14.0a6.tar.xz.spdx.json Python-3.14.0a6.tgz Python-3.14.0a6.tgz.crt Python-3.14.0a6.tgz.sig Python-3.14.0a6.tgz.sigstore Python-3.14.0a6.tgz.spdx.json Python-3.14.0a7.tar.xz Python-3.14.0a7.tar.xz.crt Python-3.14.0a7.tar.xz.sig Python-3.14.0a7.tar.xz.sigstore Python-3.14.0a7.tar.xz.spdx.json Python-3.14.0a7.tgz Python-3.14.0a7.tgz.crt Python-3.14.0a7.tgz.sig Python-3.14.0a7.tgz.sigstore Python-3.14.0a7.tgz.spdx.json Python-3.14.0b1.tar.xz Python-3.14.0b1.tar.xz.crt Python-3.14.0b1.tar.xz.sig Python-3.14.0b1.tar.xz.sigstore Python-3.14.0b1.tar.xz.spdx.json Python-3.14.0b1.tgz Python-3.14.0b1.tgz.crt Python-3.14.0b1.tgz.sig Python-3.14.0b1.tgz.sigstore Python-3.14.0b1.tgz.spdx.json Python-3.14.0b2.tar.xz Python-3.14.0b2.tar.xz.crt Python-3.14.0b2.tar.xz.sig Python-3.14.0b2.tar.xz.sigstore Python-3.14.0b2.tar.xz.spdx.json Python-3.14.0b2.tgz Python-3.14.0b2.tgz.crt Python-3.14.0b2.tgz.sig Python-3.14.0b2.tgz.sigstore Python-3.14.0b2.tgz.spdx.json Python-3.14.0b3.tar.xz Python-3.14.0b3.tar.xz.crt Python-3.14.0b3.tar.xz.sig Python-3.14.0b3.tar.xz.sigstore Python-3.14.0b3.tar.xz.spdx.json Python-3.14.0b3.tgz Python-3.14.0b3.tgz.crt Python-3.14.0b3.tgz.sig Python-3.14.0b3.tgz.sigstore Python-3.14.0b3.tgz.spdx.json amd64a1/ amd64a2/ amd64a3/ amd64a4/ amd64a5/ amd64a6/ amd64a7/ amd64b1/ amd64b2/ amd64b3/ arm64a1/ arm64a2/ arm64a3/ arm64a4/ arm64a5/ arm64a6/ arm64a7/ arm64b1/ arm64b2/ arm64b3/ python-3.14.0a1-amd64.exe python-3.14.0a1-amd64.exe.crt python-3.14.0a1-amd64.exe.sig python-3.14.0a1-amd64.exe.sigstore python-3.14.0a1-amd64.exe.spdx.json python-3.14.0a1-amd64.zip python-3.14.0a1-arm64.exe python-3.14.0a1-arm64.exe.crt python-3.14.0a1-arm64.exe.sig python-3.14.0a1-arm64.exe.sigstore python-3.14.0a1-arm64.exe.spdx.json python-3.14.0a1-arm64.zip python-3.14.0a1-embed-amd64.zip python-3.14.0a1-embed-amd64.zip.crt python-3.14.0a1-embed-amd64.zip.sig python-3.14.0a1-embed-amd64.zip.sigstore python-3.14.0a1-embed-amd64.zip.spdx.json python-3.14.0a1-embed-arm64.zip python-3.14.0a1-embed-arm64.zip.crt python-3.14.0a1-embed-arm64.zip.sig python-3.14.0a1-embed-arm64.zip.sigstore python-3.14.0a1-embed-arm64.zip.spdx.json python-3.14.0a1-embed-win32.zip python-3.14.0a1-embed-win32.zip.crt python-3.14.0a1-embed-win32.zip.sig python-3.14.0a1-embed-win32.zip.sigstore python-3.14.0a1-embed-win32.zip.spdx.json python-3.14.0a1-embeddable-amd64.zip python-3.14.0a1-embeddable-arm64.zip python-3.14.0a1-embeddable-win32.zip python-3.14.0a1-macos11.pkg python-3.14.0a1-macos11.pkg.crt python-3.14.0a1-macos11.pkg.sig python-3.14.0a1-macos11.pkg.sigstore python-3.14.0a1-test-amd64.zip python-3.14.0a1-test-arm64.zip python-3.14.0a1-test-win32.zip python-3.14.0a1-win32.zip python-3.14.0a1.exe python-3.14.0a1.exe.crt python-3.14.0a1.exe.sig python-3.14.0a1.exe.sigstore python-3.14.0a1.exe.spdx.json python-3.14.0a1t-amd64.zip python-3.14.0a1t-arm64.zip python-3.14.0a1t-win32.zip python-3.14.0a2-amd64.exe python-3.14.0a2-amd64.exe.crt python-3.14.0a2-amd64.exe.sig python-3.14.0a2-amd64.exe.sigstore python-3.14.0a2-amd64.exe.spdx.json python-3.14.0a2-amd64.zip python-3.14.0a2-arm64.exe python-3.14.0a2-arm64.exe.crt python-3.14.0a2-arm64.exe.sig python-3.14.0a2-arm64.exe.sigstore python-3.14.0a2-arm64.exe.spdx.json python-3.14.0a2-arm64.zip python-3.14.0a2-embed-amd64.zip python-3.14.0a2-embed-amd64.zip.crt python-3.14.0a2-embed-amd64.zip.sig python-3.14.0a2-embed-amd64.zip.sigstore python-3.14.0a2-embed-amd64.zip.spdx.json python-3.14.0a2-embed-arm64.zip python-3.14.0a2-embed-arm64.zip.crt python-3.14.0a2-embed-arm64.zip.sig python-3.14.0a2-embed-arm64.zip.sigstore python-3.14.0a2-embed-arm64.zip.spdx.json python-3.14.0a2-embed-win32.zip python-3.14.0a2-embed-win32.zip.crt python-3.14.0a2-embed-win32.zip.sig python-3.14.0a2-embed-win32.zip.sigstore python-3.14.0a2-embed-win32.zip.spdx.json python-3.14.0a2-embeddable-amd64.zip python-3.14.0a2-embeddable-arm64.zip python-3.14.0a2-embeddable-win32.zip python-3.14.0a2-macos11.pkg python-3.14.0a2-macos11.pkg.crt python-3.14.0a2-macos11.pkg.sig python-3.14.0a2-macos11.pkg.sigstore python-3.14.0a2-test-amd64.zip python-3.14.0a2-test-arm64.zip python-3.14.0a2-test-win32.zip python-3.14.0a2-win32.zip python-3.14.0a2.exe python-3.14.0a2.exe.crt python-3.14.0a2.exe.sig python-3.14.0a2.exe.sigstore python-3.14.0a2.exe.spdx.json python-3.14.0a2t-amd64.zip python-3.14.0a2t-arm64.zip python-3.14.0a2t-win32.zip python-3.14.0a3-amd64.exe python-3.14.0a3-amd64.exe.crt python-3.14.0a3-amd64.exe.sig python-3.14.0a3-amd64.exe.sigstore python-3.14.0a3-amd64.exe.spdx.json python-3.14.0a3-amd64.zip python-3.14.0a3-arm64.exe python-3.14.0a3-arm64.exe.crt python-3.14.0a3-arm64.exe.sig python-3.14.0a3-arm64.exe.sigstore python-3.14.0a3-arm64.exe.spdx.json python-3.14.0a3-arm64.zip python-3.14.0a3-embed-amd64.zip python-3.14.0a3-embed-amd64.zip.crt python-3.14.0a3-embed-amd64.zip.sig python-3.14.0a3-embed-amd64.zip.sigstore python-3.14.0a3-embed-amd64.zip.spdx.json python-3.14.0a3-embed-arm64.zip python-3.14.0a3-embed-arm64.zip.crt python-3.14.0a3-embed-arm64.zip.sig python-3.14.0a3-embed-arm64.zip.sigstore python-3.14.0a3-embed-arm64.zip.spdx.json python-3.14.0a3-embed-win32.zip python-3.14.0a3-embed-win32.zip.crt python-3.14.0a3-embed-win32.zip.sig python-3.14.0a3-embed-win32.zip.sigstore python-3.14.0a3-embed-win32.zip.spdx.json python-3.14.0a3-embeddable-amd64.zip python-3.14.0a3-embeddable-arm64.zip python-3.14.0a3-embeddable-win32.zip python-3.14.0a3-macos11.pkg python-3.14.0a3-macos11.pkg.crt python-3.14.0a3-macos11.pkg.sig python-3.14.0a3-macos11.pkg.sigstore python-3.14.0a3-test-amd64.zip python-3.14.0a3-test-arm64.zip python-3.14.0a3-test-win32.zip python-3.14.0a3-win32.zip python-3.14.0a3.exe python-3.14.0a3.exe.crt python-3.14.0a3.exe.sig python-3.14.0a3.exe.sigstore python-3.14.0a3.exe.spdx.json python-3.14.0a3t-amd64.zip python-3.14.0a3t-arm64.zip python-3.14.0a3t-win32.zip python-3.14.0a4-amd64.exe python-3.14.0a4-amd64.exe.crt python-3.14.0a4-amd64.exe.sig python-3.14.0a4-amd64.exe.sigstore python-3.14.0a4-amd64.exe.spdx.json python-3.14.0a4-amd64.zip python-3.14.0a4-arm64.exe python-3.14.0a4-arm64.exe.crt python-3.14.0a4-arm64.exe.sig python-3.14.0a4-arm64.exe.sigstore python-3.14.0a4-arm64.exe.spdx.json python-3.14.0a4-arm64.zip python-3.14.0a4-embed-amd64.zip python-3.14.0a4-embed-amd64.zip.crt python-3.14.0a4-embed-amd64.zip.sig python-3.14.0a4-embed-amd64.zip.sigstore python-3.14.0a4-embed-amd64.zip.spdx.json python-3.14.0a4-embed-arm64.zip python-3.14.0a4-embed-arm64.zip.crt python-3.14.0a4-embed-arm64.zip.sig python-3.14.0a4-embed-arm64.zip.sigstore python-3.14.0a4-embed-arm64.zip.spdx.json python-3.14.0a4-embed-win32.zip python-3.14.0a4-embed-win32.zip.crt python-3.14.0a4-embed-win32.zip.sig python-3.14.0a4-embed-win32.zip.sigstore python-3.14.0a4-embed-win32.zip.spdx.json python-3.14.0a4-embeddable-amd64.zip python-3.14.0a4-embeddable-arm64.zip python-3.14.0a4-embeddable-win32.zip python-3.14.0a4-macos11.pkg python-3.14.0a4-macos11.pkg.crt python-3.14.0a4-macos11.pkg.sig python-3.14.0a4-macos11.pkg.sigstore python-3.14.0a4-test-amd64.zip python-3.14.0a4-test-arm64.zip python-3.14.0a4-test-win32.zip python-3.14.0a4-win32.zip python-3.14.0a4.exe python-3.14.0a4.exe.crt python-3.14.0a4.exe.sig python-3.14.0a4.exe.sigstore python-3.14.0a4.exe.spdx.json python-3.14.0a4t-amd64.zip python-3.14.0a4t-arm64.zip python-3.14.0a4t-win32.zip python-3.14.0a5-amd64.exe python-3.14.0a5-amd64.exe.crt python-3.14.0a5-amd64.exe.sig python-3.14.0a5-amd64.exe.sigstore python-3.14.0a5-amd64.exe.spdx.json python-3.14.0a5-arm64.exe python-3.14.0a5-arm64.exe.crt python-3.14.0a5-arm64.exe.sig python-3.14.0a5-arm64.exe.sigstore python-3.14.0a5-arm64.exe.spdx.json python-3.14.0a5-embed-amd64.zip python-3.14.0a5-embed-amd64.zip.crt python-3.14.0a5-embed-amd64.zip.sig python-3.14.0a5-embed-amd64.zip.sigstore python-3.14.0a5-embed-amd64.zip.spdx.json python-3.14.0a5-embed-arm64.zip python-3.14.0a5-embed-arm64.zip.crt python-3.14.0a5-embed-arm64.zip.sig python-3.14.0a5-embed-arm64.zip.sigstore python-3.14.0a5-embed-arm64.zip.spdx.json python-3.14.0a5-embed-win32.zip python-3.14.0a5-embed-win32.zip.crt python-3.14.0a5-embed-win32.zip.sig python-3.14.0a5-embed-win32.zip.sigstore python-3.14.0a5-embed-win32.zip.spdx.json python-3.14.0a5-macos11.pkg python-3.14.0a5-macos11.pkg.crt python-3.14.0a5-macos11.pkg.sig python-3.14.0a5-macos11.pkg.sigstore python-3.14.0a5.exe python-3.14.0a5.exe.crt python-3.14.0a5.exe.sig python-3.14.0a5.exe.sigstore python-3.14.0a5.exe.spdx.json python-3.14.0a6-amd64.exe python-3.14.0a6-amd64.exe.crt python-3.14.0a6-amd64.exe.sig python-3.14.0a6-amd64.exe.sigstore python-3.14.0a6-amd64.exe.spdx.json python-3.14.0a6-amd64.zip python-3.14.0a6-arm64.exe python-3.14.0a6-arm64.exe.crt python-3.14.0a6-arm64.exe.sig python-3.14.0a6-arm64.exe.sigstore python-3.14.0a6-arm64.exe.spdx.json python-3.14.0a6-arm64.zip python-3.14.0a6-embed-amd64.zip python-3.14.0a6-embed-amd64.zip.crt python-3.14.0a6-embed-amd64.zip.sig python-3.14.0a6-embed-amd64.zip.sigstore python-3.14.0a6-embed-amd64.zip.spdx.json python-3.14.0a6-embed-arm64.zip python-3.14.0a6-embed-arm64.zip.crt python-3.14.0a6-embed-arm64.zip.sig python-3.14.0a6-embed-arm64.zip.sigstore python-3.14.0a6-embed-arm64.zip.spdx.json python-3.14.0a6-embed-win32.zip python-3.14.0a6-embed-win32.zip.crt python-3.14.0a6-embed-win32.zip.sig python-3.14.0a6-embed-win32.zip.sigstore python-3.14.0a6-embed-win32.zip.spdx.json python-3.14.0a6-embeddable-amd64.zip python-3.14.0a6-embeddable-arm64.zip python-3.14.0a6-embeddable-win32.zip python-3.14.0a6-macos11.pkg python-3.14.0a6-macos11.pkg.crt python-3.14.0a6-macos11.pkg.sig python-3.14.0a6-macos11.pkg.sigstore python-3.14.0a6-test-amd64.zip python-3.14.0a6-test-arm64.zip python-3.14.0a6-test-win32.zip python-3.14.0a6-win32.zip python-3.14.0a6.exe python-3.14.0a6.exe.crt python-3.14.0a6.exe.sig python-3.14.0a6.exe.sigstore python-3.14.0a6.exe.spdx.json python-3.14.0a6t-amd64.zip python-3.14.0a6t-arm64.zip python-3.14.0a6t-win32.zip python-3.14.0a7-amd64.exe python-3.14.0a7-amd64.exe.crt python-3.14.0a7-amd64.exe.sig python-3.14.0a7-amd64.exe.sigstore python-3.14.0a7-amd64.exe.spdx.json python-3.14.0a7-amd64.zip python-3.14.0a7-arm64.exe python-3.14.0a7-arm64.exe.crt python-3.14.0a7-arm64.exe.sig python-3.14.0a7-arm64.exe.sigstore python-3.14.0a7-arm64.exe.spdx.json python-3.14.0a7-arm64.zip python-3.14.0a7-embed-amd64.zip python-3.14.0a7-embed-amd64.zip.crt python-3.14.0a7-embed-amd64.zip.sig python-3.14.0a7-embed-amd64.zip.sigstore python-3.14.0a7-embed-amd64.zip.spdx.json python-3.14.0a7-embed-arm64.zip python-3.14.0a7-embed-arm64.zip.crt python-3.14.0a7-embed-arm64.zip.sig python-3.14.0a7-embed-arm64.zip.sigstore python-3.14.0a7-embed-arm64.zip.spdx.json python-3.14.0a7-embed-win32.zip python-3.14.0a7-embed-win32.zip.crt python-3.14.0a7-embed-win32.zip.sig python-3.14.0a7-embed-win32.zip.sigstore python-3.14.0a7-embed-win32.zip.spdx.json python-3.14.0a7-embeddable-amd64.zip python-3.14.0a7-embeddable-arm64.zip python-3.14.0a7-embeddable-win32.zip python-3.14.0a7-macos11.pkg python-3.14.0a7-macos11.pkg.crt python-3.14.0a7-macos11.pkg.sig python-3.14.0a7-macos11.pkg.sigstore python-3.14.0a7-test-amd64.zip python-3.14.0a7-test-arm64.zip python-3.14.0a7-test-win32.zip python-3.14.0a7-win32.zip python-3.14.0a7.exe python-3.14.0a7.exe.crt python-3.14.0a7.exe.sig python-3.14.0a7.exe.sigstore python-3.14.0a7.exe.spdx.json python-3.14.0a7t-amd64.zip python-3.14.0a7t-arm64.zip python-3.14.0a7t-win32.zip python-3.14.0b1-amd64.exe python-3.14.0b1-amd64.exe.crt python-3.14.0b1-amd64.exe.sig python-3.14.0b1-amd64.exe.sigstore python-3.14.0b1-amd64.exe.spdx.json python-3.14.0b1-amd64.zip python-3.14.0b1-arm64.exe python-3.14.0b1-arm64.exe.crt python-3.14.0b1-arm64.exe.sig python-3.14.0b1-arm64.exe.sigstore python-3.14.0b1-arm64.exe.spdx.json python-3.14.0b1-arm64.zip python-3.14.0b1-embed-amd64.zip python-3.14.0b1-embed-amd64.zip.crt python-3.14.0b1-embed-amd64.zip.sig python-3.14.0b1-embed-amd64.zip.sigstore python-3.14.0b1-embed-amd64.zip.spdx.json python-3.14.0b1-embed-arm64.zip python-3.14.0b1-embed-arm64.zip.crt python-3.14.0b1-embed-arm64.zip.sig python-3.14.0b1-embed-arm64.zip.sigstore python-3.14.0b1-embed-arm64.zip.spdx.json python-3.14.0b1-embed-win32.zip python-3.14.0b1-embed-win32.zip.crt python-3.14.0b1-embed-win32.zip.sig python-3.14.0b1-embed-win32.zip.sigstore python-3.14.0b1-embed-win32.zip.spdx.json python-3.14.0b1-embeddable-amd64.zip python-3.14.0b1-embeddable-arm64.zip python-3.14.0b1-embeddable-win32.zip python-3.14.0b1-macos11.pkg python-3.14.0b1-macos11.pkg.crt python-3.14.0b1-macos11.pkg.sig python-3.14.0b1-macos11.pkg.sigstore python-3.14.0b1-test-amd64.zip python-3.14.0b1-test-arm64.zip python-3.14.0b1-test-win32.zip python-3.14.0b1-win32.zip python-3.14.0b1.exe python-3.14.0b1.exe.crt python-3.14.0b1.exe.sig python-3.14.0b1.exe.sigstore python-3.14.0b1.exe.spdx.json python-3.14.0b1t-amd64.zip python-3.14.0b1t-arm64.zip python-3.14.0b1t-win32.zip python-3.14.0b2-amd64.exe python-3.14.0b2-amd64.exe.crt python-3.14.0b2-amd64.exe.sig python-3.14.0b2-amd64.exe.sigstore python-3.14.0b2-amd64.exe.spdx.json python-3.14.0b2-amd64.zip python-3.14.0b2-arm64.exe python-3.14.0b2-arm64.exe.crt python-3.14.0b2-arm64.exe.sig python-3.14.0b2-arm64.exe.sigstore python-3.14.0b2-arm64.exe.spdx.json python-3.14.0b2-arm64.zip python-3.14.0b2-embed-amd64.zip python-3.14.0b2-embed-amd64.zip.crt python-3.14.0b2-embed-amd64.zip.sig python-3.14.0b2-embed-amd64.zip.sigstore python-3.14.0b2-embed-amd64.zip.spdx.json python-3.14.0b2-embed-arm64.zip python-3.14.0b2-embed-arm64.zip.crt python-3.14.0b2-embed-arm64.zip.sig python-3.14.0b2-embed-arm64.zip.sigstore python-3.14.0b2-embed-arm64.zip.spdx.json python-3.14.0b2-embed-win32.zip python-3.14.0b2-embed-win32.zip.crt python-3.14.0b2-embed-win32.zip.sig python-3.14.0b2-embed-win32.zip.sigstore python-3.14.0b2-embed-win32.zip.spdx.json python-3.14.0b2-embeddable-amd64.zip python-3.14.0b2-embeddable-arm64.zip python-3.14.0b2-embeddable-win32.zip python-3.14.0b2-macos11.pkg python-3.14.0b2-macos11.pkg.crt python-3.14.0b2-macos11.pkg.sig python-3.14.0b2-macos11.pkg.sigstore python-3.14.0b2-test-amd64.zip python-3.14.0b2-test-arm64.zip python-3.14.0b2-test-win32.zip python-3.14.0b2-win32.zip python-3.14.0b2.exe python-3.14.0b2.exe.crt python-3.14.0b2.exe.sig python-3.14.0b2.exe.sigstore python-3.14.0b2.exe.spdx.json python-3.14.0b2t-amd64.zip python-3.14.0b2t-arm64.zip python-3.14.0b2t-win32.zip python-3.14.0b3-aarch64-linux-android.tar.gz Python-3.14.0b3-iOS-XCframework.tar.gz python-3.14.0b3-amd64.exe python-3.14.0b3-amd64.exe.crt python-3.14.0b3-amd64.exe.sig python-3.14.0b3-amd64.exe.sigstore python-3.14.0b3-amd64.exe.spdx.json python-3.14.0b3-amd64.zip python-3.14.0b3-arm64.exe python-3.14.0b3-arm64.exe.crt python-3.14.0b3-arm64.exe.sig python-3.14.0b3-arm64.exe.sigstore python-3.14.0b3-arm64.exe.spdx.json python-3.14.0b3-arm64.zip python-3.14.0b3-embed-amd64.zip python-3.14.0b3-embed-amd64.zip.crt python-3.14.0b3-embed-amd64.zip.sig python-3.14.0b3-embed-amd64.zip.sigstore python-3.14.0b3-embed-amd64.zip.spdx.json python-3.14.0b3-embed-arm64.zip python-3.14.0b3-embed-arm64.zip.crt python-3.14.0b3-embed-arm64.zip.sig python-3.14.0b3-embed-arm64.zip.sigstore python-3.14.0b3-embed-arm64.zip.spdx.json python-3.14.0b3-embed-win32.zip python-3.14.0b3-embed-win32.zip.crt python-3.14.0b3-embed-win32.zip.sig python-3.14.0b3-embed-win32.zip.sigstore python-3.14.0b3-embed-win32.zip.spdx.json python-3.14.0b3-embeddable-amd64.zip python-3.14.0b3-embeddable-arm64.zip python-3.14.0b3-embeddable-win32.zip python-3.14.0b3-macos11.pkg python-3.14.0b3-macos11.pkg.crt python-3.14.0b3-macos11.pkg.sig python-3.14.0b3-macos11.pkg.sigstore python-3.14.0b3-test-amd64.zip python-3.14.0b3-test-arm64.zip python-3.14.0b3-test-win32.zip python-3.14.0b3-win32.zip python-3.14.0b3-x86_64-linux-android.tar.gz python-3.14.0b3.exe python-3.14.0b3.exe.crt python-3.14.0b3.exe.sig python-3.14.0b3.exe.sigstore python-3.14.0b3.exe.spdx.json python-3.14.0b3t-amd64.zip python-3.14.0b3t-arm64.zip python-3.14.0b3t-win32.zip win32a1/ win32a2/ win32a3/ win32a4/ win32a5/ win32a6/ win32a7/ win32b1/ win32b2/ win32b3/ windows-3.14.0a1.json windows-3.14.0a2.json windows-3.14.0a3.json windows-3.14.0a4.json windows-3.14.0a6.json windows-3.14.0a7.json windows-3.14.0b1.json windows-3.14.0b2.json windows-3.14.0b3.json ================================================ FILE: tests/magicdata/Include/internal/pycore_magic_number.h ================================================ // copied from cpython bd3d31f380cd451a4ab6da5fbfde463fed95b5b5 // ... #ifndef Py_INTERNAL_MAGIC_NUMBER_H #define Py_INTERNAL_MAGIC_NUMBER_H #define PYC_MAGIC_NUMBER 3603 #endif // !Py_INTERNAL_MAGIC_NUMBER_H // ... ================================================ FILE: tests/magicdata/Lib/test/test_importlib/test_util.py ================================================ # copied from cpython bd3d31f380cd451a4ab6da5fbfde463fed95b5b5 class SomeClass: def some_method(self) -> None: EXPECTED_MAGIC_NUMBER = 3495 print(EXPECTED_MAGIC_NUMBER) ================================================ FILE: tests/patchlevel.h ================================================ /* Python version identification scheme. When the major or minor version changes, the VERSION variable in configure.ac must also be changed. There is also (independent) API version information in modsupport.h. */ /* Values for PY_RELEASE_LEVEL */ #define PY_RELEASE_LEVEL_ALPHA 0xA #define PY_RELEASE_LEVEL_BETA 0xB #define PY_RELEASE_LEVEL_GAMMA 0xC /* For release candidates */ #define PY_RELEASE_LEVEL_FINAL 0xF /* Serial should be 0 here */ /* Higher for patch releases */ /* Version parsed out into numeric values */ /*--start constants--*/ #define PY_MAJOR_VERSION 3 #define PY_MINOR_VERSION 14 #define PY_MICRO_VERSION 0 #define PY_RELEASE_LEVEL PY_RELEASE_LEVEL_ALPHA #define PY_RELEASE_SERIAL 1 /* Version as a string */ #define PY_VERSION "3.14.0a1+" /*--end constants--*/ /* Version as a single 4-byte hex number, e.g. 0x010502B2 == 1.5.2b2. Use this for numeric comparisons, e.g. #if PY_VERSION_HEX >= ... */ #define PY_VERSION_HEX ((PY_MAJOR_VERSION << 24) | \ (PY_MINOR_VERSION << 16) | \ (PY_MICRO_VERSION << 8) | \ (PY_RELEASE_LEVEL << 4) | \ (PY_RELEASE_SERIAL << 0)) ================================================ FILE: tests/sbom/sbom-with-pip-removed.json ================================================ { "SPDXID": "SPDXRef-DOCUMENT", "name": "CPython SBOM", "spdxVersion": "SPDX-2.3", "dataLicense": "CC0-1.0", "documentNamespace": "https://www.python.org/ftp/python/3.13.0/fake-artifact.txt.spdx.json", "creationInfo": { "created": "2024-10-15T20:11:52Z", "creators": [], "licenseListVersion": "3.22" }, "files": [], "packages": [], "relationships": [ { "relatedSpdxElement": "SPDXRef-FILE-Modules-expat-COPYING", "relationshipType": "CONTAINS", "spdxElementId": "SPDXRef-PACKAGE-expat" } ] } ================================================ FILE: tests/sbom/sbom-with-pip.json ================================================ { "SPDXID": "SPDXRef-DOCUMENT", "name": "CPython SBOM", "spdxVersion": "SPDX-2.3", "dataLicense": "CC0-1.0", "documentNamespace": "https://www.python.org/ftp/python/3.13.0/fake-artifact.txt.spdx.json", "creationInfo": { "created": "2024-10-15T20:11:52Z", "creators": [], "licenseListVersion": "3.22" }, "files": [], "packages": [ { "SPDXID": "SPDXRef-PACKAGE-pip", "name": "pip", "versionInfo": "24.0", "licenseConcluded": "MIT", "originator": "Organization: Python Software Foundation", "supplier": "Organization: Python Software Foundation", "packageFileName": "pip-24.0-py3-none-any.whl", "externalRefs": [ { "referenceCategory": "SECURITY", "referenceLocator": "cpe:2.3:a:pypa:pip:24.0:*:*:*:*:*:*:*", "referenceType": "cpe23Type" } ], "primaryPackagePurpose": "RUNTIME", "downloadLocation": "https://files.pythonhosted.org/packages/.../pip-24.0-py3-none-any.whl", "checksums": [ { "algorithm": "SHA256", "checksumValue": "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc" } ] } ], "relationships": [ { "relatedSpdxElement": "SPDXRef-FILE-Modules-expat-COPYING", "relationshipType": "CONTAINS", "spdxElementId": "SPDXRef-PACKAGE-expat" }, { "relatedSpdxElement": "SPDXRef-PACKAGE-urllib3", "relationshipType": "DEPENDS_ON", "spdxElementId": "SPDXRef-PACKAGE-pip" }, { "relatedSpdxElement": "SPDXRef-PACKAGE-pip", "relationshipType": "DEPENDS_ON", "spdxElementId": "SPDXRef-PACKAGE-cpython" } ] } ================================================ FILE: tests/test_add_to_pydotorg.py ================================================ import os from pathlib import Path import pytest from pyfakefs.fake_filesystem import FakeFilesystem os.environ["AUTH_INFO"] = "test_username:test_api_key" import add_to_pydotorg @pytest.mark.parametrize( ["release", "expected"], [ ("3.9.0a0", "390-a0"), ("3.10.0b3", "3100-b3"), ("3.11.0rc2", "3110-rc2"), ("3.12.15", "31215"), ], ) def test_slug_for(release: str, expected: str) -> None: assert add_to_pydotorg.slug_for(release) == expected def test_sigfile_for() -> None: assert ( add_to_pydotorg.sigfile_for("3.14.0", "Python-3.13.0.tgz") == "https://www.python.org/ftp/python/3.14.0/Python-3.13.0.tgz.asc" ) @pytest.mark.parametrize( ["text", "expected"], [ ("3.9.0a0", "390a0"), ("3.10.0b3", "3100b3"), ("3.11.0rc2", "3110rc2"), ("3.12.15", "31215"), ("Hello, world!", "Hello-world"), ], ) def test_make_slug(text: str, expected: str) -> None: assert add_to_pydotorg.make_slug(text) == expected def test_build_file_dict(tmp_path: Path) -> None: release = "3.14.0rc2" release_url = "https://www.python.org/ftp/python/3.14.0" release_dir = tmp_path / "3.14.0" release_dir.mkdir() rfile = "test-artifact.txt" (release_dir / rfile).write_text("Hello world") (release_dir / f"{rfile}.sigstore").touch() assert add_to_pydotorg.build_file_dict( str(tmp_path), release, rfile, 12, "Test artifact", 34, True, "Test description", ) == { "name": "Test artifact", "slug": "3140-rc2-Test-artifact", "os": "/api/v1/downloads/os/34/", "release": "/api/v1/downloads/release/12/", "description": "Test description", "is_source": False, "url": f"{release_url}/test-artifact.txt", "sha256_sum": "64ec88ca00b268e5ba1a35678a1b5316d212f4f366b2477232534a8aeca37f3c", "filesize": 11, "download_button": True, "sigstore_bundle_file": f"{release_url}/test-artifact.txt.sigstore", } @pytest.mark.parametrize( ["release", "expected"], [ ("3.9.0a0", "3.9.0"), ("3.10.0b3", "3.10.0"), ("3.11.0rc2", "3.11.0"), ("3.12.15", "3.12.15"), ], ) def test_base_version(release: str, expected: str) -> None: assert add_to_pydotorg.base_version(release) == expected @pytest.mark.parametrize( ["release", "expected"], [ ("3.9.0a0", (3, 9, 0)), ("3.10.0b3", (3, 10, 0)), ("3.11.0rc2", (3, 11, 0)), ("3.12.15", (3, 12, 15)), ], ) def test_base_version_tuple(release: str, expected: tuple[int, int, int]) -> None: assert add_to_pydotorg.base_version_tuple(release) == expected @pytest.mark.parametrize( ["release", "expected"], [ ("3.9.0a0", "3.9"), ("3.10.0b3", "3.10"), ("3.11.0rc2", "3.11"), ("3.12.15", "3.12"), ], ) def test_minor_version(release: str, expected: str) -> None: assert add_to_pydotorg.minor_version(release) == expected @pytest.mark.parametrize( ["release", "expected"], [ ("3.9.0a0", (3, 9)), ("3.10.0b3", (3, 10)), ("3.11.0rc2", (3, 11)), ("3.12.15", (3, 12)), ], ) def test_minor_version_tuple(release: str, expected: tuple[int, int]) -> None: assert add_to_pydotorg.minor_version_tuple(release) == expected @pytest.mark.parametrize( ["release", "expected"], [ ((3, 13, 0), "for macOS 10.13 and later"), ((3, 14, 0), "for macOS 10.15 and later"), ], ) def test_macos_description(release: tuple[int, int, int], expected: str) -> None: assert add_to_pydotorg.macos_description(release) == expected def test_list_files(fs: FakeFilesystem) -> None: # Arrange fake_ftp_root = "/fake_ftp_root" fs.add_real_file("tests/fake-ftp-files.txt") fake_files = Path("tests/fake-ftp-files.txt").read_text().splitlines() for fn in fake_files: if fn.startswith("#"): # comment continue file_path = Path(fake_ftp_root) / "3.14.0" / fn if fn.endswith("/"): fs.create_dir(file_path) else: fs.create_file(file_path) # Act files = list(add_to_pydotorg.list_files(fake_ftp_root, "3.14.0b3")) # Assert assert files == [ ( "Python-3.14.0b3-iOS-XCframework.tar.gz", "iOS XCframework", "ios", False, "", ), ("Python-3.14.0b3.tar.xz", "XZ compressed source tarball", "source", True, ""), ("Python-3.14.0b3.tgz", "Gzipped source tarball", "source", False, ""), ( "python-3.14.0b3-aarch64-linux-android.tar.gz", "Android embeddable package (aarch64)", "android", False, "", ), ( "python-3.14.0b3-amd64.exe", "Windows installer (64-bit)", "windows", True, "Recommended", ), ( "python-3.14.0b3-arm64.exe", "Windows installer (ARM64)", "windows", False, "Experimental", ), ( "python-3.14.0b3-embed-amd64.zip", "Windows embeddable package (64-bit)", "windows", False, "", ), ( "python-3.14.0b3-embed-arm64.zip", "Windows embeddable package (ARM64)", "windows", False, "", ), ( "python-3.14.0b3-embed-win32.zip", "Windows embeddable package (32-bit)", "windows", False, "", ), ( "python-3.14.0b3-macos11.pkg", "macOS installer", "macos", True, "for macOS 10.15 and later", ), ( "python-3.14.0b3-x86_64-linux-android.tar.gz", "Android embeddable package (x86_64)", "android", False, "", ), ("python-3.14.0b3.exe", "Windows installer (32-bit)", "windows", False, ""), ( "windows-3.14.0b3.json", "Windows release manifest", "windows", False, "Install with 'py install 3.14'", ), ] ================================================ FILE: tests/test_buildbotapi.py ================================================ from functools import cache from unittest.mock import AsyncMock import aiohttp import pytest import buildbotapi def test_builder_class() -> None: # Arrange / Act builder = buildbotapi.Builder( builderid=123, description="my description", name="my name", tags=["tag1", "tag2"], ) # Assert assert builder.builderid == 123 assert builder.description == "my description" assert builder.name == "my name" assert builder.tags == ["tag1", "tag2"] assert hash(builder) == 123 @cache def load(filename: str) -> str: with open(filename) as f: return f.read() @pytest.mark.asyncio async def test_buildbotapi_authenticate() -> None: # Arrange async with AsyncMock(aiohttp.ClientSession) as mock_session: api = buildbotapi.BuildBotAPI(mock_session) # Act await api.authenticate(token="") # Assert mock_session.get.assert_called_with( "https://buildbot.python.org/all/auth/login", params={"token": ""} ) @pytest.mark.asyncio async def test_buildbotapi_all_builders() -> None: # Arrange mock_session = AsyncMock(aiohttp.ClientSession) mock_session.get.return_value.__aenter__.return_value.status = 200 mock_session.get.return_value.__aenter__.return_value.text.return_value = load( "tests/buildbotapi/builders.json" ) api = buildbotapi.BuildBotAPI(mock_session) # Act all_builders = await api.all_builders() # Assert mock_session.get.assert_called_with( "https://buildbot.python.org/all/api/v2/builders" ) assert len(all_builders) == 2 assert all_builders[3].name == "AMD64 RHEL8 LTO 3.13" assert all_builders[1623].name == "AMD64 Windows PGO NoGIL PR" @pytest.mark.asyncio async def test_buildbotapi_all_builders_with_branch() -> None: # Arrange mock_session = AsyncMock(aiohttp.ClientSession) mock_session.get.return_value.__aenter__.return_value.status = 200 mock_session.get.return_value.__aenter__.return_value.text.return_value = load( "tests/buildbotapi/builders.json" ) api = buildbotapi.BuildBotAPI(mock_session) # Act await api.all_builders(branch="3.13") # Assert mock_session.get.assert_called_with( "https://buildbot.python.org/all/api/v2/builders?tags__contains=3.13" ) @pytest.mark.asyncio async def test_buildbotapi_stable_builders() -> None: # Arrange mock_session = AsyncMock(aiohttp.ClientSession) mock_session.get.return_value.__aenter__.return_value.status = 200 mock_session.get.return_value.__aenter__.return_value.text.return_value = load( "tests/buildbotapi/builders.json" ) api = buildbotapi.BuildBotAPI(mock_session) # Act all_builders = await api.stable_builders() # Assert mock_session.get.assert_called_with( "https://buildbot.python.org/all/api/v2/builders" ) assert len(all_builders) == 1 assert all_builders[3].name == "AMD64 RHEL8 LTO 3.13" assert "stable" in all_builders[3].tags @pytest.mark.asyncio @pytest.mark.parametrize( ["json_data", "expected"], [ ("tests/buildbotapi/success.json", False), ("tests/buildbotapi/failure.json", True), ("tests/buildbotapi/no-builds.json", False), ], ) async def test_buildbotapi_is_builder_failing_currently_yes( json_data: str, expected: bool ) -> None: # Arrange mock_session = AsyncMock(aiohttp.ClientSession) mock_session.get.return_value.__aenter__.return_value.status = 200 mock_session.get.return_value.__aenter__.return_value.text.return_value = load( json_data ) api = buildbotapi.BuildBotAPI(mock_session) builder = buildbotapi.Builder(builderid=3) # Act failing = await api.is_builder_failing_currently(builder=builder) # Assert mock_session.get.assert_called_with( "https://buildbot.python.org/all/api/v2/builds?complete__eq=true" "&&builderid__eq=3&&order=-complete_at&&limit=1" ) assert failing is expected ================================================ FILE: tests/test_release.py ================================================ from pathlib import Path from typing import cast import pytest from pytest_mock import MockerFixture import release @pytest.mark.parametrize( ["test_editor", "expected"], [ ("vim", ["vim", "README.rst"]), ("bbedit --wait", ["bbedit", "--wait", "README.rst"]), ], ) def test_manual_edit( mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, test_editor: str, expected: list[str], ) -> None: # Arrange monkeypatch.setenv("EDITOR", test_editor) mock_run_cmd = mocker.patch("release.run_cmd") # Act release.manual_edit("README.rst") # Assert mock_run_cmd.assert_called_once_with(expected) def test_task(mocker: MockerFixture) -> None: # Arrange db = {"mock": "mock"} my_task = mocker.Mock() task = release.Task(my_task, "My task") # Act task(cast(release.ReleaseShelf, db)) # Assert assert task.description == "My task" assert task.function == my_task my_task.assert_called_once_with(cast(release.ReleaseShelf, db)) @pytest.mark.parametrize( ["test_inputs", "expected"], [ (["yes"], True), (["no"], False), (["maybe", "yes"], True), (["maybe", "no"], False), (["", "nope", "y", "yes"], True), (["", "nope", "n", "no"], False), ], ) def test_ask_question( mocker: MockerFixture, capsys: pytest.CaptureFixture[str], test_inputs: list[str], expected: bool, ) -> None: # Arrange mocker.patch("release.input", side_effect=test_inputs) # Act result = release.ask_question("Do you want to proceed?") # Assert assert result is expected captured = capsys.readouterr() assert "Do you want to proceed?" in captured.out # All inputs except the last are invalid invalid_count = len(test_inputs) - 1 assert captured.out.count("Please enter yes or no.") == invalid_count def test_tweak_patchlevel(tmp_path: Path) -> None: # Arrange tag = release.Tag("3.14.0b2") original_patchlevel_file = Path(__file__).parent / "patchlevel.h" patchlevel_file = tmp_path / "patchlevel.h" patchlevel_file.write_text(original_patchlevel_file.read_text()) # Act release.tweak_patchlevel(tag, filename=str(patchlevel_file)) # Assert new_contents = patchlevel_file.read_text() for expected in ( "#define PY_MAJOR_VERSION 3", "#define PY_MINOR_VERSION 14", "#define PY_MICRO_VERSION 0", "#define PY_RELEASE_LEVEL PY_RELEASE_LEVEL_BETA", "#define PY_RELEASE_SERIAL 2", '#define PY_VERSION "3.14.0b2"', ): assert expected in new_contents @pytest.mark.parametrize( [ "test_tag", "expected_version", "expected_underline", "expected_whatsnew", "expected_docs", "expected_pep_line", ], [ ( "3.14.0a6", "This is Python version 3.14.0 alpha 6", "=====================================", "3.14 `_", "`Documentation for Python 3.14 `_", "`PEP 745 `__ for Python 3.14", ), ( "3.14.0b2", "This is Python version 3.14.0 beta 2", "====================================", "3.14 `_", "`Documentation for Python 3.14 `_", "`PEP 745 `__ for Python 3.14", ), ( "3.14.0rc2", "This is Python version 3.14.0 release candidate 2", "=================================================", "3.14 `_", "`Documentation for Python 3.14 `_", "`PEP 745 `__ for Python 3.14", ), ( "3.15.1", "This is Python version 3.15.1", "=============================", "3.15 `_", "`Documentation for Python 3.15 `_", "`PEP 790 `__ for Python 3.15", ), ], ) def test_tweak_readme( tmp_path: Path, test_tag: str, expected_version: str, expected_underline: str, expected_whatsnew: str, expected_docs: str, expected_pep_line: str, ) -> None: # Arrange tag = release.Tag(test_tag) original_readme_file = Path(__file__).parent / "README.rst" original_contents = original_readme_file.read_text() readme_file = tmp_path / "README.rst" readme_file.write_text(original_contents) # Act release.tweak_readme(tag, filename=str(readme_file)) # Assert new_contents = readme_file.read_text() new_lines = new_contents.split("\n") assert new_lines[0] == expected_version assert new_lines[1] == expected_underline assert expected_whatsnew in new_contents assert expected_docs in new_contents assert expected_pep_line in new_contents assert original_contents.endswith("\n") assert new_contents.endswith("\n") ================================================ FILE: tests/test_release_tag.py ================================================ import io from subprocess import CompletedProcess import pytest from pytest_mock import MockerFixture import release def test_tag() -> None: # Arrange tag_name = "3.12.2" # Act tag = release.Tag(tag_name) # Assert assert str(tag) == "3.12.2" assert str(tag.next_minor_release()) == "3.13.0a0" assert tag.as_tuple() == (3, 12, 2, "f", 0) assert tag.branch == "3.12" assert tag.gitname == "v3.12.2" assert tag.long_name == "3.12.2" assert tag.is_alpha_release is False assert tag.is_feature_freeze_release is False assert tag.is_release_candidate is False assert tag.nickname == "3122" assert tag.normalized() == "3.12.2" def test_tag_phase() -> None: # Arrange alpha = release.Tag("3.13.0a7") beta1 = release.Tag("3.13.0b1") beta4 = release.Tag("3.13.0b4") rc = release.Tag("3.13.0rc3") final = release.Tag("3.13.0") # Act / Assert assert alpha.is_alpha_release is True assert alpha.is_feature_freeze_release is False assert alpha.is_release_candidate is False assert alpha.is_final is False assert alpha.branch == "main" assert beta1.is_alpha_release is False assert beta1.is_feature_freeze_release is True assert beta1.is_release_candidate is False assert beta1.is_final is False assert beta1.branch == "main" assert beta4.is_alpha_release is False assert beta4.is_feature_freeze_release is False assert beta4.is_release_candidate is False assert beta4.is_final is False assert beta4.branch == "3.13" assert rc.is_alpha_release is False assert rc.is_feature_freeze_release is False assert rc.is_release_candidate is True assert rc.is_final is False assert rc.branch == "3.13" assert final.is_alpha_release is False assert final.is_feature_freeze_release is False assert final.is_release_candidate is False assert final.is_final is True assert final.branch == "3.13" def test_tag_committed_at_not_found() -> None: # Arrange tag = release.Tag("3.12.2") # Act / Assert with pytest.raises(SystemExit): tag.committed_at() def test_tag_committed(mocker: MockerFixture) -> None: # Arrange tag = release.Tag("3.12.2") proc = CompletedProcess([], 0) proc.stdout = b"1707250784" mocker.patch("subprocess.run", return_value=proc) # Act / Assert assert str(tag.committed_at) == "2024-02-06 20:19:44+00:00" def test_tag_dot(mocker: MockerFixture) -> None: # Arrange tag_name = "." mocker.patch("os.getcwd", return_value="/path/to/3.12.2") # Act tag = release.Tag(tag_name) # Assert assert str(tag) == "3.12.2" def test_tag_invalid() -> None: # Arrange tag_name = "bleep" # Act / Assert with pytest.raises(SystemExit): release.Tag(tag_name) def test_tag_docs_attributes() -> None: # Arrange alpha = release.Tag("3.13.0a7") beta = release.Tag("3.13.0b1") rc = release.Tag("3.13.0rc3") final_zero = release.Tag("3.13.0") final_3 = release.Tag("3.13.3") # Act / Assert assert alpha.includes_docs is False assert beta.includes_docs is False assert rc.includes_docs is True assert final_zero.includes_docs is True assert final_3.includes_docs is True assert alpha.doc_version == "3.13" assert beta.doc_version == "3.13" assert rc.doc_version == "3.13" assert final_zero.doc_version == "3.13" assert final_3.doc_version == "3.13.3" def test_tag_long_name() -> None: # Arrange alpha = release.Tag("3.13.0a7") beta = release.Tag("3.13.0b1") rc = release.Tag("3.13.0rc3") final_zero = release.Tag("3.13.0") final_3 = release.Tag("3.13.3") # Act / Assert assert alpha.long_name == "3.13.0 alpha 7" assert beta.long_name == "3.13.0 beta 1" assert rc.long_name == "3.13.0 release candidate 3" assert final_zero.long_name == "3.13.0" assert final_3.long_name == "3.13.3" @pytest.mark.parametrize( ["version", "expected"], [ ("3.12.10", True), ("3.13.3", False), ], ) def test_tag_is_security_release( version: str, expected: str, mocker: MockerFixture ) -> None: # Arrange mock_response = b""" { "3.13": { "status": "bugfix" }, "3.12": { "status": "security" } } """ mocker.patch("urllib.request.urlopen", return_value=io.BytesIO(mock_response)) # Act tag = release.Tag(version) # Assert assert tag.is_security_release is expected ================================================ FILE: tests/test_run_release.py ================================================ import builtins import contextlib import io import tarfile from contextlib import nullcontext as does_not_raise from pathlib import Path from typing import cast import pytest import run_release from release import ReleaseShelf, Tag from run_release import ReleaseException @pytest.mark.parametrize( "version", ["sigstore 3.6.2", "sigstore 3.6.6"], ) def test_check_sigstore_version_success(version) -> None: # Verify runs with no exceptions run_release.check_sigstore_version(version) @pytest.mark.parametrize( "version", ["sigstore 3.4.0", "sigstore 3.6.0", "sigstore 4.0.0", ""], ) def test_check_sigstore_version_exception(version) -> None: with pytest.raises( ReleaseException, match="Sigstore version not detected or not valid" ): run_release.check_sigstore_version(version) @pytest.mark.parametrize( ["url", "expected"], [ ("github.com/hugovk/cpython.git", "hugovk"), ("git@github.com:hugovk/cpython.git", "hugovk"), ("https://github.com/hugovk/cpython.git", "hugovk"), ], ) def test_extract_github_owner(url: str, expected: str) -> None: assert run_release.extract_github_owner(url) == expected def test_invalid_extract_github_owner() -> None: with pytest.raises( ReleaseException, match="Could not parse GitHub owner from 'origin' remote URL: " "https://example.com", ): run_release.extract_github_owner("https://example.com") @pytest.mark.parametrize( ["release_tag", "git_current_branch", "expectation"], [ # Success cases ("3.15.0rc1", "3.15\n", does_not_raise()), ("3.15.0b3", "3.15\n", does_not_raise()), ("3.15.0b2", "3.15\n", does_not_raise()), ("3.15.0b1", "main\n", does_not_raise()), ("3.15.0a6", "main\n", does_not_raise()), ("3.14.3", "3.14\n", does_not_raise()), ("3.13.12", "3.13\n", does_not_raise()), # Failure cases ( "3.15.0rc1", "main\n", pytest.raises(ReleaseException, match="on main branch, expected 3.15"), ), ( "3.15.0b1", "3.15\n", pytest.raises(ReleaseException, match="on 3.15 branch, expected main"), ), ( "3.15.0a6", "3.14\n", pytest.raises(ReleaseException, match="on 3.14 branch, expected main"), ), ( "3.14.3", "main\n", pytest.raises(ReleaseException, match="on main branch, expected 3.14"), ), ], ) def test_check_cpython_repo_branch( monkeypatch, release_tag: str, git_current_branch: str, expectation ) -> None: # Arrange db = {"release": Tag(release_tag), "git_repo": "/fake/repo"} monkeypatch.setattr( run_release.subprocess, "check_output", lambda *args, **kwargs: git_current_branch, ) # Act / Assert with expectation: run_release.check_cpython_repo_branch(cast(ReleaseShelf, db)) @pytest.mark.parametrize( ["age_seconds", "user_continues", "expectation"], [ # Recent repo (< 1 day) - no question asked (3600, None, does_not_raise()), # Old repo (> 1 day) + user says yes (90000, True, does_not_raise()), # Old repo (> 1 day) + user says no (90000, False, pytest.raises(ReleaseException, match="repository is old")), ], ) def test_check_cpython_repo_age( monkeypatch, age_seconds: int, user_continues: bool | None, expectation ) -> None: # Arrange db = {"release": Tag("3.15.0a6"), "git_repo": "/fake/repo"} current_time = 1700000000 commit_timestamp = current_time - age_seconds def fake_check_output(cmd, **kwargs): cmd_str = " ".join(cmd) if "%ct" in cmd_str: return f"{commit_timestamp}\n" if "%cr" in cmd_str: return "some time ago\n" return "" monkeypatch.setattr(run_release.subprocess, "check_output", fake_check_output) monkeypatch.setattr(run_release.time, "time", lambda: current_time) if user_continues is not None: monkeypatch.setattr(run_release, "ask_question", lambda _: user_continues) # Act / Assert with expectation: run_release.check_cpython_repo_age(cast(ReleaseShelf, db)) def test_check_magic_number() -> None: db = { "release": Tag("3.14.0rc1"), "git_repo": str(Path(__file__).parent / "magicdata"), } with pytest.raises(ReleaseException, match="Magic numbers in .* don't match"): run_release.check_magic_number(cast(ReleaseShelf, db)) def prepare_fake_docs(tmp_path: Path, content: str) -> None: docs_path = tmp_path / "3.13.0rc1/docs" docs_path.mkdir(parents=True) tarball = tarfile.open(docs_path / "python-3.13.0rc1-docs-html.tar.bz2", "w:bz2") with tarball: tarinfo = tarfile.TarInfo("index.html") tarinfo.size = len(content) tarball.addfile(tarinfo, io.BytesIO(content.encode())) @contextlib.contextmanager def fake_answers(monkeypatch: pytest.MonkeyPatch, answers: list[str]) -> None: """Monkey-patch input() to give the given answers. All must be consumed.""" answers_left = list(answers) def fake_input(question): print(question, "--", answers_left[0]) return answers_left.pop(0) with monkeypatch.context() as ctx: ctx.setattr(builtins, "input", fake_input) yield assert answers_left == [] def test_check_doc_unreleased_version_no_file(tmp_path: Path) -> None: db = { "release": Tag("3.13.0rc1"), "git_repo": str(tmp_path), } with pytest.raises(AssertionError): # There should be a docs artefact available run_release.check_doc_unreleased_version(cast(ReleaseShelf, db)) def test_check_doc_unreleased_version_no_file_alpha(tmp_path: Path) -> None: db = { "release": Tag("3.13.0a1"), "git_repo": str(tmp_path), } # No docs artefact needed for alphas run_release.check_doc_unreleased_version(cast(ReleaseShelf, db)) def test_check_doc_unreleased_version_ok(tmp_path: Path) -> None: prepare_fake_docs( tmp_path, "
New in 3.13
", ) db = { "release": Tag("3.13.0rc1"), "git_repo": str(tmp_path), } run_release.check_doc_unreleased_version(cast(ReleaseShelf, db)) def test_check_doc_unreleased_version_not_ok(monkeypatch, tmp_path: Path) -> None: prepare_fake_docs( tmp_path, "
New in 3.13.0rc1 (unreleased)
", ) db = { "release": Tag("3.13.0rc1"), "git_repo": str(tmp_path), } with fake_answers(monkeypatch, ["no"]), pytest.raises(AssertionError): run_release.check_doc_unreleased_version(cast(ReleaseShelf, db)) def test_check_doc_unreleased_version_waived(monkeypatch, tmp_path: Path) -> None: prepare_fake_docs( tmp_path, "
New in 3.13.0rc1 (unreleased)
", ) db = { "release": Tag("3.13.0rc1"), "git_repo": str(tmp_path), } with fake_answers(monkeypatch, ["yes"]): run_release.check_doc_unreleased_version(cast(ReleaseShelf, db)) def test_update_whatsnew_toctree(tmp_path: Path) -> None: # Arrange # Only first beta triggers update db = {"release": Tag("3.14.0b1")} original_toctree_file = Path(__file__).parent / "whatsnew_index.rst" toctree__file = tmp_path / "patchlevel.h" toctree__file.write_text(original_toctree_file.read_text()) # Act run_release.update_whatsnew_toctree(cast(ReleaseShelf, db), str(toctree__file)) # Assert new_contents = toctree__file.read_text() assert " 3.15.rst\n 3.14.rst\n" in new_contents ================================================ FILE: tests/test_sbom.py ================================================ import hashlib import json import pathlib import random import re import unittest.mock from pathlib import Path import pytest import sbom @pytest.mark.parametrize( ["value", "expected"], [ ("abc", "abc"), ("path/name", "path-name"), ("SPDXRef-PACKAGE-pip", "SPDXRef-PACKAGE-pip"), ("SPDXRef-PACKAGE-cpython", "SPDXRef-PACKAGE-cpython"), ("SPDXRef-PACKAGE-urllib3", "SPDXRef-PACKAGE-urllib3"), ], ) def test_spdx_id(value: str, expected: str) -> None: assert sbom.spdx_id(value) == expected # Check we get the same value next time assert sbom.spdx_id(value) == expected def test_spdx_id_collisions(): sbom._SPDX_IDS_TO_VALUES = {} # Reset the cache. assert ( sbom.spdx_id("SPDXRef-FILE-Lib/collections.py") == "SPDXRef-FILE-Lib-collections.py" ) assert ( sbom.spdx_id("SPDXRef-FILE-Lib/_collections.py") == "SPDXRef-FILE-Lib-collections.py-fc43043d" ) @pytest.mark.parametrize( ["package_sha1s", "package_verification_code"], [ # No files -> empty SHA1 ([], hashlib.sha1().hexdigest()), # One file -> SHA1(SHA1(file)) (["F" * 40], hashlib.sha1(b"f" * 40).hexdigest()), # Tests ordering and lowercasing of SHA1s ( ["0" * 40, "e" * 40, "F" * 40], hashlib.sha1((b"0" * 40) + (b"e" * 40) + (b"f" * 40)).hexdigest(), ), ], ) def test_calculate_package_verification_code(package_sha1s, package_verification_code): # Randomize because PackageVerificationCode is deterministic. random.shuffle(package_sha1s) input_sbom = { "files": [ { "SPDXID": f"SPDXRef-FILE-{package_sha1}", "checksums": [{"algorithm": "SHA1", "checksumValue": package_sha1}], } for package_sha1 in package_sha1s ], "packages": [{"SPDXID": "SPDXRef-PACKAGE", "filesAnalyzed": True}], "relationships": [ { "spdxElementId": "SPDXRef-PACKAGE", "relatedSpdxElement": f"SPDXRef-FILE-{package_sha1}", "relationshipType": "CONTAINS", } for package_sha1 in package_sha1s ], } sbom.calculate_package_verification_codes(input_sbom) assert input_sbom["packages"][0]["packageVerificationCode"] == { "packageVerificationCodeValue": package_verification_code } def test_normalization(): # Test that arbitrary JSON data can be normalized. # Normalization doesn't have to make too much sense, # only needs to be reproducible. data = { "a": [1, 2, 3, {"b": [4, "c", [7, True, "2", {}]]}], # This line tests that inner structures are sorted first. "b": [[1, 2, "b"], [2, 1, "a"]], } sbom.normalize_sbom_data(data) assert data == { "a": [1, 2, 3, {"b": ["c", 4, ["2", 7, True, {}]]}], "b": [["a", 1, 2], ["b", 1, 2]], } def test_fetch_project_metadata_from_pypi(mocker): mock_urlopen = mocker.patch("sbom.urlopen") mock_urlopen.return_value = unittest.mock.Mock() # This is only a partial response using the information # that this function uses. mock_urlopen.return_value.read.return_value = json.dumps( { "urls": [ { "digests": { "blake2b_256": "94596638090c25e9bc4ce0c42817b5a234e183872a1129735a9330c472cc2056", "sha256": "ea9bd1a847e8c5774a5777bb398c19e80bcd4e2aa16a4b301b718fe6f593aba2", }, "filename": "pip-24.0.tar.gz", "packagetype": "sdist", "url": "https://files.pythonhosted.org/packages/.../pip-24.0.tar.gz", }, { "digests": { "blake2b_256": "8a6a19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b", "sha256": "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc", }, "filename": "pip-24.0-py3-none-any.whl", "packagetype": "bdist_wheel", "url": "https://files.pythonhosted.org/packages/.../pip-24.0-py3-none-any.whl", }, ] } ).encode() # Default filename is the wheel download_url, checksum_sha256 = sbom.fetch_package_metadata_from_pypi( project="pip", version="24.0", ) mock_urlopen.assert_called_once_with("https://pypi.org/pypi/pip/24.0/json") assert ( download_url == "https://files.pythonhosted.org/packages/.../pip-24.0-py3-none-any.whl" ) assert ( checksum_sha256 == "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc" ) # If we ask for the sdist (which we don't do normally) # then it'll be returned instead. download_url, checksum_sha256 = sbom.fetch_package_metadata_from_pypi( project="pip", version="24.0", filename="pip-24.0.tar.gz" ) assert download_url == "https://files.pythonhosted.org/packages/.../pip-24.0.tar.gz" assert ( checksum_sha256 == "ea9bd1a847e8c5774a5777bb398c19e80bcd4e2aa16a4b301b718fe6f593aba2" ) def test_remove_pip_from_sbom() -> None: # Arrange with (Path(__file__).parent / "sbom" / "sbom-with-pip.json").open() as f: sbom_data = json.load(f) with (Path(__file__).parent / "sbom" / "sbom-with-pip-removed.json").open() as f: expected = json.load(f) # Act sbom.remove_pip_from_sbom(sbom_data) # Assert assert sbom_data == expected def test_create_cpython_sbom(): sbom_data = {"packages": []} artifact_path = str(pathlib.Path(__file__).parent / "fake-artifact.txt") sbom.create_cpython_sbom( sbom_data, cpython_version="3.13.0", artifact_path=artifact_path ) assert re.fullmatch( r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$", sbom_data["creationInfo"].pop("created"), ) assert re.fullmatch( r"^Tool: ReleaseTools-[a-f0-9]+$", sbom_data["creationInfo"]["creators"].pop(1) ) assert sbom_data == { "packages": [ { "SPDXID": "SPDXRef-PACKAGE-cpython", "name": "CPython", "versionInfo": "3.13.0", "licenseConcluded": "PSF-2.0", "originator": "Organization: Python Software Foundation", "supplier": "Organization: Python Software Foundation", "packageFileName": "fake-artifact.txt", "externalRefs": [ { "referenceCategory": "SECURITY", "referenceLocator": "cpe:2.3:a:python:python:3.13.0:*:*:*:*:*:*:*", "referenceType": "cpe23Type", } ], "primaryPackagePurpose": "SOURCE", "downloadLocation": "https://www.python.org/ftp/python/3.13.0/fake-artifact.txt", "checksums": [ { "algorithm": "SHA256", "checksumValue": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", } ], } ], "SPDXID": "SPDXRef-DOCUMENT", "spdxVersion": "SPDX-2.3", "name": "CPython SBOM", "dataLicense": "CC0-1.0", "documentNamespace": "https://www.python.org/ftp/python/3.13.0/fake-artifact.txt.spdx.json", "creationInfo": { "creators": [ "Person: Python Release Managers", ], "licenseListVersion": "3.22", }, } @pytest.mark.parametrize( ["cpython_version", "download_location"], [ ("3.13.0", "https://www.python.org/ftp/python/3.13.0/fake-artifact.txt"), ("3.11.0a1", "https://www.python.org/ftp/python/3.11.0/fake-artifact.txt"), ("3.12.0b2", "https://www.python.org/ftp/python/3.12.0/fake-artifact.txt"), ("3.13.0rc3", "https://www.python.org/ftp/python/3.13.0/fake-artifact.txt"), ], ) def test_create_cpython_sbom_pre_release_download_location( cpython_version, download_location ): sbom_data = {"packages": []} artifact_path = str(pathlib.Path(__file__).parent / "fake-artifact.txt") sbom.create_cpython_sbom( sbom_data, cpython_version=cpython_version, artifact_path=artifact_path ) assert sbom_data["packages"][0]["downloadLocation"] == download_location ================================================ FILE: tests/test_select_jobs.py ================================================ import sys from textwrap import dedent import pytest import select_jobs @pytest.mark.parametrize( ("version", "docs", "android", "ios"), [ ("3.13.0a1", "false", "false", "false"), ("3.13.0rc1", "true", "false", "false"), ("3.13.0", "true", "false", "false"), ("3.13.1", "true", "false", "false"), ("3.14.0b2", "false", "true", "false"), ("3.14.0rc1", "true", "true", "false"), ("3.14.0", "true", "true", "false"), ("3.14.1", "true", "true", "false"), ("3.15.0a1", "false", "true", "true"), ("3.15.0", "true", "true", "true"), ], ) def test_select_jobs( version: str, docs: str, android: str, ios: str, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: monkeypatch.setattr(sys, "argv", ["select_jobs.py", version]) select_jobs.main() assert capsys.readouterr().out == dedent( f"""\ docs={docs} android={android} ios={ios} """ ) @pytest.mark.parametrize( "version", [ "3.13.0a1", "3.13.0", "3.14.0b2", "3.15.0a1", ], ) def test_select_jobs_test_mode( version: str, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: monkeypatch.setattr(sys, "argv", ["select_jobs.py", "--test", version]) select_jobs.main() assert capsys.readouterr().out == dedent( """\ docs=true android=true ios=true """ ) ================================================ FILE: tests/test_update_version_next.py ================================================ """Tests for the update_version_next tool.""" from pathlib import Path import update_version_next TO_CHANGE = """ Directives to change -------------------- Here, all occurrences of NEXT (lowercase) should be changed: .. versionadded:: next .. versionchanged:: next .. deprecated:: next .. deprecated-removed:: next 4.0 whitespace: .. versionchanged:: next .. versionchanged :: next .. versionadded:: next arguments: .. versionadded:: next Foo bar .. versionadded:: next as ``previousname`` """ UNCHANGED = """ Unchanged --------- Here, the word "next" should NOT be changed: .. versionchanged:: NEXT ..versionchanged:: NEXT ... versionchanged:: next foo .. versionchanged:: next .. otherdirective:: next .. VERSIONCHANGED: next .. deprecated-removed: 3.0 next """ EXPECTED_CHANGED = TO_CHANGE.replace("next", "VER") def test_freeze_simple_script(tmp_path: Path) -> None: p = tmp_path.joinpath p("source.rst").write_text(TO_CHANGE + UNCHANGED) p("subdir").mkdir() p("subdir/change.rst").write_text(".. versionadded:: next") p("subdir/keep.not-rst").write_text(".. versionadded:: next") p("subdir/keep.rst").write_text("nothing to see here") args = ["VER", str(tmp_path)] update_version_next.main(args) assert p("source.rst").read_text() == EXPECTED_CHANGED + UNCHANGED assert p("subdir/change.rst").read_text() == ".. versionadded:: VER" assert p("subdir/keep.not-rst").read_text() == ".. versionadded:: next" assert p("subdir/keep.rst").read_text() == "nothing to see here" ================================================ FILE: tests/whatsnew_index.rst ================================================ .. _whatsnew-index: ###################### What's New in Python ###################### The "What's New in Python" series of essays takes tours through the most important changes between major Python versions. They are a "must read" for anyone wishing to stay up-to-date after a new release. .. toctree:: :maxdepth: 2 3.14.rst 3.13.rst 3.12.rst 3.11.rst 3.10.rst 3.9.rst 3.8.rst 3.7.rst 3.6.rst 3.5.rst 3.4.rst 3.3.rst 3.2.rst 3.1.rst 3.0.rst 2.7.rst 2.6.rst 2.5.rst 2.4.rst 2.3.rst 2.2.rst 2.1.rst 2.0.rst The "Changelog" is an HTML version of the :pypi:`file built` from the contents of the :source:`Misc/NEWS.d` directory tree, which contains *all* nontrivial changes to Python for the current version. .. toctree:: :maxdepth: 2 changelog.rst ================================================ FILE: tox.ini ================================================ [tox] requires = tox>=4.2 env_list = lint py{314, 313, 312} [testenv] skip_install = true deps = -r dev-requirements.txt -r requirements.txt commands = {envpython} -m pytest -vv \ tests/ \ --cov . \ --cov tests \ --cov-report html \ --cov-report term \ --cov-report xml \ {posargs} [testenv:lint] skip_install = true deps = pre-commit pass_env = PRE_COMMIT_COLOR commands = pre-commit run --all-files --show-diff-on-failure [testenv:mypy] skip_install = true deps = -r mypy-requirements.txt commands = mypy . {posargs} ================================================ FILE: update_version_next.py ================================================ #!/usr/bin/env python3 """ Replace `.. versionchanged:: next` lines in docs files by the given version. Run this at release time to replace `next` with the just-released version in the sources. No backups are made; add/commit to Git before running the script. Applies to all the VersionChange directives. For deprecated-removed, only handle the first argument (deprecation version, not the removal version). """ import argparse import re import sys from pathlib import Path DIRECTIVE_RE = re.compile( r""" (?P \s*\.\.\s+ (version(added|changed|removed)|deprecated(-removed)?) \s*::\s* ) next (?P .* ) """, re.VERBOSE | re.DOTALL, ) parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument( "version", help='String to replace "next" with. Usually `x.y`, but can be anything.', ) parser.add_argument( "directory", type=Path, help="Directory to process", ) parser.add_argument( "--verbose", "-v", action="count", default=0, help="Increase verbosity. Can be repeated (`-vv`).", ) def main(argv: list[str]) -> None: args = parser.parse_args(argv) version = args.version if args.verbose: print( f'Updating "next" versions in {args.directory} to {version!r}', file=sys.stderr, ) for path in Path(args.directory).glob("**/*.rst"): num_changed_lines = 0 lines = [] with open(path, encoding="utf-8") as file: for lineno, line in enumerate(file, start=1): try: if match := DIRECTIVE_RE.fullmatch(line): line = match["before"] + version + match["after"] num_changed_lines += 1 lines.append(line) except Exception as exc: exc.add_note(f"processing line {path}:{lineno}") raise if num_changed_lines: if args.verbose: s = "" if num_changed_lines == 1 else "s" print( f"Updating file {path} ({num_changed_lines} change{s})", file=sys.stderr, ) with open(path, "w", encoding="utf-8") as file: file.writelines(lines) else: if args.verbose > 1: print(f"Unchanged file {path}", file=sys.stderr) if __name__ == "__main__": main(sys.argv[1:]) ================================================ FILE: windows-release/README.md ================================================ # Windows Releases This build script is used for official releases of CPython on Windows. It is somewhat more complex than `Tools/msi/buildrelease.bat` because it uses additional parallelism and uses our official code signing certificate. This script is designed to be run on Azure Pipelines. Information about the syntax can be found at https://docs.microsoft.com/azure/devops/pipelines/ The current deployment is at https://dev.azure.com/Python/cpython/_build?definitionId=21 Chances are you don't have permission to do anything other than view builds. Access is controlled by the release team. If you do have permission, you can launch a release build by selecting **Run pipeline**, specify the desired **Git remote** and **Git tag**, enable **Publish release**, toggle any version specific options, and click **Run**. The version specific options are required due to changes in our build that require modifications to the publish pipeline. For example, whether to publish ARM64 binaries. When signing is enabled (any value besides "Unsigned"), authorised approvers will be notified and will need to approve each stage that requires the signing certificate (typically three). This helps prevent "surprise" builds from using the official certificate. Some additional points to be aware of: * packages are not automatically published to the Microsoft Store * successful builds should be retained by selecting "Retain" under the "..." menu in the top-right The `msixupload` artifacts should be uploaded to the Microsoft Store at https://partner.microsoft.com/en-us/dashboard/apps-and-games/overview. Access to this site is very limited. We also usually update the screenshots so that the version information they show matches the release. Azure DevOps no longer has a per-pipeline option for retention, and so the only way to permanently retain a build is to manually select the "Retain" option. Without this, the build records will be lost after 30 days. ## Finding/updating certificates For code signing, we use [Azure Trusted Signing](https://learn.microsoft.com/en-us/azure/trusted-signing/overview). This service belongs to the PSF's Azure subscription and is paid for on a monthly basis. When we send files for signing, it uploads a manifest (hash) of the file rather than the file itself, and then receives a signature that can be embedded into the target file. Authentication to Azure currently uses an [Entra app registration](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) rather than OIDC (which is available, and may be switched to in future). The authentication details are stored as private variables in a Variable group called CPythonSign. Referencing this variable group is what triggers approvals during the build. The group is at https://dev.azure.com/Python/cpython/_library?itemType=VariableGroups&view=VariableGroupView&variableGroupId=1&path=CPythonSign The five variables in the Variable Group identify the Entra ID with access, and the name of the certificate to use. * `TrustedSigningClientId` - the "Application (client) ID" of the App registration * `TrustedSigningTenantId` - the "Directory (tenant) ID" of the App registration * `TrustedSigningSecret` - the current "Client secret" of the App registration * `TrustedSigningUri` - the endpoint of the Trusted Signing service (provided by Azure) * `TrustedSigningAccount` - the name of our Trusted Signing account, "pythondev". This is not a secret * `TrustedSigningCertificateName` - the name of our certificate profile. This is not a secret Certificates are renewed daily, and as such it is no longer useful to reference the "thumbprint" (SHA1 hash) of the certificate. Instead, to trust all of our releases in restricted scenarios, you need to first trust one of the certificates in the certification path and then check for EKU `1.3.6.1.4.1.311.97.608394634.79987812.305991749.578777327`, which represents our signing account, or Subject `CN=Python Software Foundation,O=Python Software Foundation,L=Beaverton,S=Oregon,C=US`. TODO: Reference/link to documentation on verifying certificates with tools. Note that regular signing checks (such as `signtool.exe verify /pa python.exe`) and malware scans will treat the files as correctly signed. It's only more complicated to verify that it was signed _specifically_ with our cert. (Further documentation to be added as we find out what ought to be documented.) ================================================ FILE: windows-release/acquire-vcruntime.yml ================================================ parameters: Remote: https://github.com/python/cpython-bin-deps Ref: vcruntime steps: - powershell: | git clone --progress -v --depth 1 --branch ${{ parameters.Ref }} --single-branch ${{ parameters.Remote }} vcruntime $files = (dir "vcruntime\$(arch)\*.dll").FullName -join ";" "##vso[task.setvariable variable=VCRuntimeDLL]$files" displayName: 'Import VC redist' ================================================ FILE: windows-release/azure-pipelines.yml ================================================ name: Release_$(SourceTag)_$(Date:yyyyMMdd)$(Rev:.rr) parameters: - name: GitRemote displayName: "Git remote" type: string default: python values: - 'python' - 'savannahostrowski' - 'hugovk' - 'Yhg1s' - 'pablogsal' - 'ambv' - 'zooba' - '(Other)' - name: GitRemote_Other displayName: "If Other, specify Git remote" type: string default: 'python' - name: SourceTag displayName: "Git tag" type: string default: main - name: SourceCommit displayName: "Git commit ('empty' to disable commit SHA check)" type: string default: 'empty' - name: DoPublish displayName: "Publish release" type: boolean default: false - name: SigningCertificate displayName: "Code signing certificate" type: string default: 'PythonSoftwareFoundation' values: - 'PythonSoftwareFoundation' - 'TestSign' - 'Unsigned' - name: SigningDescription displayName: "Signature description" type: string default: '(default)' - name: Post315OutputDir displayName: "Separate free-threaded outputs (3.15 and later)" type: boolean default: false - name: DoTailCalling displayName: "Build with tail-calling support (3.15 and later)" type: boolean default: false - name: DoJIT displayName: "Build the JIT compiler (3.14 and later)" type: boolean default: true - name: DoGPG displayName: "Include GPG signatures (3.13 and earlier)" type: boolean default: false - name: DoFreethreaded displayName: "Include free-threaded builds" type: boolean default: true - name: DoARM64 displayName: "Publish ARM64 build" type: boolean default: true - name: DoPGO displayName: "Run PGO" type: boolean default: true - name: DoPGOARM64 displayName: "Run ARM64 PGO" type: boolean default: true - name: DoLayout displayName: "Produce full layout artifact" type: boolean default: true - name: DoMSIX displayName: "Produce Store packages (3.13 and earlier)" type: boolean default: false - name: DoNuget displayName: "Produce Nuget packages" type: boolean default: true - name: DoEmbed displayName: "Produce embeddable package (w/ PyManager or MSI options)" type: boolean default: true - name: DoMSI displayName: "Produce EXE/MSI installer" type: boolean default: true - name: TestMSI displayName: "Run EXE/MSI installer tests" type: boolean default: true - name: DoPyManager displayName: "Produce PyManager package" type: boolean default: true - name: BuildToPublish displayName: "Republish a build (select in Resources)" type: string default: current values: ['current', 'build_to_publish'] - name: BuildToPackage displayName: "Repackage and publish a build (select in Resources)" type: string default: current values: ['current', 'build_to_package'] - name: SignNuget displayName: "Enable Nuget signing (not recommended right now)" type: boolean default: false - name: DoJITEnabled displayName: "Enable the JIT compiler by default (not used yet)" type: boolean default: false - name: DoJITFreethreaded displayName: "Build the JIT compiler for free-threaded builds (not used yet)" type: boolean default: false - name: vmImage displayName: "VM Image" type: string default: windows-2025 resources: pipelines: - pipeline: build_to_publish source: 'Windows-Release' - pipeline: build_to_package source: 'Windows-Release' variables: ${{ if ne(parameters.GitRemote, '(Other)') }}: GitRemote: ${{ parameters.GitRemote }} ${{ else }}: GitRemote: ${{ parameters.GitRemote_Other }} SourceTag: ${{ parameters.SourceTag }} ${{ if ne(parameters.SourceCommit, 'empty') }}: SourceCommit: ${{ parameters.SourceCommit }} ${{ if eq(parameters.SigningCertificate, 'PythonSoftwareFoundation') }}: IsRealSigned: true ${{ else }}: IsRealSigned: false ${{ if ne(parameters.SigningDescription, '(default)') }}: SigningDescription: ${{ parameters.SigningDescription }} ${{ else }}: SigningDescription: '' PublishARM64: ${{ parameters.DoARM64 }} # QUEUE TIME VARIABLES # OverrideNugetVersion: '' # PyManagerIndexFilename: '' # SkipNugetPublish: '' # SkipPipTests: '' # SkipPythonOrgPublish: '' # SkipSBOM: '' # SkipTests: '' # SkipTkTests: '' trigger: none pr: none stages: - ${{ if and(eq(parameters.BuildToPublish, 'current'), eq(parameters.BuildToPackage, 'current')) }}: - stage: Build displayName: Build binaries pool: vmImage: ${{ iif(eq(parameters.DoTailCalling, 'true'), 'windows-2025-vs2026', parameters.vmImage) }} jobs: - template: start-arm64vm.yml parameters: DoARM64: ${{ parameters.DoARM64 }} DoPGOARM64: ${{ parameters.DoPGOARM64 }} - template: stage-build.yml parameters: DoFreethreaded: ${{ parameters.DoFreethreaded }} DoPGO: ${{ parameters.DoPGO }} DoPGOARM64: ${{ parameters.DoPGOARM64 }} ${{ if and(parameters.SigningCertificate, ne(parameters.SigningCertificate, 'Unsigned')) }}: ToBeSigned: true ${{ if ne(parameters.DoJIT, 'true') }}: ExtraOptions: '' ${{ elseif ne(parameters.DoJITEnabled, 'true') }}: ExtraOptions: '--experimental-jit-off' ${{ else }}: ExtraOptions: '--experimental-jit' ${{ if or(ne(parameters.DoJIT, 'true'), ne(parameters.DoJITFreethreaded, 'true')) }}: ExtraOptionsFreethreaded: '--disable-gil' ${{ elseif ne(parameters.DoJITEnabled, 'true') }}: ExtraOptionsFreethreaded: '--disable-gil --experimental-jit-off' ${{ else }}: ExtraOptionsFreethreaded: '--disable-gil --experimental-jit' ${{ if ne(parameters.DoTailCalling, 'true') }}: TailCallingOption: '' ${{ else }}: TailCallingOption: '--tail-call-interp' Post315OutputDir: ${{ parameters.Post315OutputDir }} - stage: Sign displayName: Sign binaries dependsOn: Build pool: vmImage: ${{ parameters.vmImage }} jobs: - template: stage-sign.yml parameters: SigningCertificate: ${{ parameters.SigningCertificate }} DoFreethreaded: ${{ parameters.DoFreethreaded }} - ${{ if eq(parameters.BuildToPublish, 'current') }}: - stage: Layout ${{ if eq(parameters.BuildToPackage, 'current') }}: displayName: Generate layouts dependsOn: Sign ${{ else }}: displayName: Generate layouts from prior build dependsOn: [] pool: vmImage: ${{ parameters.vmImage }} jobs: - template: stage-layout-full.yml parameters: BuildToPackage: ${{ parameters.BuildToPackage }} DoFreethreaded: ${{ parameters.DoFreethreaded }} SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }} - template: stage-layout-symbols.yml parameters: BuildToPackage: ${{ parameters.BuildToPackage }} DoFreethreaded: ${{ parameters.DoFreethreaded }} SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }} - ${{ if eq(parameters.DoEmbed, 'true') }}: - template: stage-layout-embed.yml parameters: BuildToPackage: ${{ parameters.BuildToPackage }} SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }} - ${{ if eq(parameters.DoNuget, 'true') }}: - template: stage-layout-nuget.yml parameters: BuildToPackage: ${{ parameters.BuildToPackage }} DoFreethreaded: ${{ parameters.DoFreethreaded }} SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }} - ${{ if eq(parameters.DoMSIX, 'true') }}: - template: stage-layout-msix.yml parameters: BuildToPackage: ${{ parameters.BuildToPackage }} SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }} - ${{ if eq(parameters.DoPyManager, 'true') }}: - template: stage-layout-pymanager.yml parameters: BuildToPackage: ${{ parameters.BuildToPackage }} DoFreethreaded: ${{ parameters.DoFreethreaded }} DoEmbed: ${{ parameters.DoEmbed }} SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }} - stage: Pack dependsOn: Layout displayName: Pack pool: vmImage: ${{ parameters.vmImage }} jobs: #- ${{ if eq(parameters.DoEmbed, 'true') }}: # - template: stage-pack-embed.yml # parameters: # SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }} - ${{ if eq(parameters.DoMSI, 'true') }}: - template: stage-msi.yml parameters: BuildToPackage: ${{ parameters.BuildToPackage }} DoARM64: ${{ parameters.DoARM64}} DoFreethreaded: ${{ parameters.DoFreethreaded }} SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }} Post315OutputDir: ${{ parameters.Post315OutputDir }} - ${{ if eq(parameters.DoMSIX, 'true') }}: - template: stage-pack-msix.yml parameters: SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }} - ${{ if eq(parameters.DoNuget, 'true') }}: - template: stage-pack-nuget.yml parameters: ${{ if eq(parameters.SignNuget, 'true') }}: SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }} DoFreethreaded: ${{ parameters.DoFreethreaded }} - ${{ if eq(parameters.DoPyManager, 'true') }}: - template: stage-pack-pymanager.yml parameters: DoFreethreaded: ${{ parameters.DoFreethreaded }} DoEmbed: ${{ parameters.DoEmbed }} SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }} - stage: Test dependsOn: Pack pool: vmImage: ${{ parameters.vmImage }} jobs: - ${{ if eq(parameters.DoEmbed, 'true') }}: - template: stage-test-embed.yml - ${{ if and(eq(parameters.DoMSI, 'true'), eq(parameters.TestMSI, 'true')) }}: - template: stage-test-msi.yml parameters: DoFreethreaded: ${{ parameters.DoFreethreaded }} - ${{ if eq(parameters.DoNuget, 'true') }}: - template: stage-test-nuget.yml parameters: DoFreethreaded: ${{ parameters.DoFreethreaded }} - ${{ if eq(parameters.DoPyManager, 'true') }}: - template: stage-test-pymanager.yml parameters: DoEmbed: ${{ parameters.DoEmbed }} DoFreethreaded: ${{ parameters.DoFreethreaded }} - ${{ if eq(parameters.DoPublish, 'true') }}: - stage: Publish displayName: Publish dependsOn: - ${{ if eq(parameters.BuildToPublish, 'current') }}: - Test pool: vmImage: ${{ parameters.vmImage }} jobs: - ${{ if eq(parameters.DoPyManager, 'true') }}: - template: stage-publish-pymanager.yml parameters: BuildToPublish: ${{ parameters.BuildToPublish }} DoEmbed: ${{ parameters.DoEmbed }} DoFreethreaded: ${{ parameters.DoFreethreaded }} SigningCertificate: ${{ iif(eq(parameters.SigningCertificate, 'Unsigned'), '', parameters.SigningCertificate) }} - ${{ if eq(parameters.DoMSI, 'true') }}: - template: stage-publish-pythonorg.yml parameters: BuildToPublish: ${{ parameters.BuildToPublish }} DoEmbed: ${{ parameters.DoEmbed }} IncludeGPG: ${{ parameters.DoGPG }} - ${{ if eq(parameters.DoNuget, 'true') }}: - template: stage-publish-nugetorg.yml parameters: BuildToPublish: ${{ parameters.BuildToPublish }} ================================================ FILE: windows-release/build-steps-pgo.yml ================================================ parameters: PGInstrument: false PGRun: false PGUpdate: false steps: - template: ./checkout.yml - ${{ if or(eq(parameters.PGInstrument, 'true'), eq(parameters.PGUpdate, 'true')) }}: - template: ./acquire-vcruntime.yml - powershell: | $d = (.\PCbuild\build.bat -V) | %{ if($_ -match '\s+(\w+):\s*(.+)\s*$') { @{$Matches[1] = $Matches[2];} }}; Write-Host "##vso[task.setvariable variable=VersionText]$($d.PythonVersion)" Write-Host "##vso[task.setvariable variable=VersionNumber]$($d.PythonVersionNumber)" Write-Host "##vso[task.setvariable variable=VersionHex]$($d.PythonVersionHex)" Write-Host "##vso[task.setvariable variable=VersionUnique]$($d.PythonVersionUnique)" Write-Host "##vso[build.addbuildtag]$($d.PythonVersion)" Write-Host "##vso[build.addbuildtag]$($d.PythonVersion)-$(Name)" displayName: 'Extract version numbers' - ${{ if eq(parameters.PGInstrument, 'true') }}: - powershell: | $env:SigningCertificate = $null .\PCbuild\build.bat -v -p $(Platform) -c PGInstrument $(ExtraOptions) displayName: 'Run build' env: IncludeUwp: true Py_OutDir: '$(Build.BinariesDirectory)\bin' - ${{ if ne(parameters.PGRun, 'true') }}: # Not running in this job, so we publish our entire build and object files - publish: '$(Build.BinariesDirectory)\bin\$(ArchDir)\instrumented' artifact: instrumented_bin_$(Name) displayName: 'Publish binaries for profiling' - powershell: | if ((Test-Path Python\frozen_modules) -and (Test-Path Python\deepfreeze)) { move Python\frozen_modules, Python\deepfreeze PCbuild\obj\ } displayName: 'Preserve frozen_modules' - publish: '$(Build.SourcesDirectory)\PCbuild\obj' artifact: instrumented_obj_$(Name) displayName: 'Download artifact: instrumented_obj_$(Name)' - ${{ if eq(parameters.PGRun, 'true') }}: - ${{ if ne(parameters.PGInstrument, 'true') }}: # Didn't build in this job, so download the required binaries - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: instrumented_bin_$(Name)' inputs: buildType: current artifact: instrumented_bin_$(Name) targetPath: '$(Build.BinariesDirectory)\bin\$(ArchDir)\instrumented' - powershell: | $exe = (gci "$(Build.BinariesDirectory)\bin\$(ArchDir)\instrumented\$(PythonExePattern)" | select -First 1) & $exe -m test --pgo ignoreLASTEXITCODE: true displayName: 'Collect profile' env: PYTHONHOME: '$(Build.SourcesDirectory)' - ${{ if ne(parameters.PGUpdate, 'true') }}: # Not finishing in this job, so publish the binaries - publish: '$(Build.BinariesDirectory)\bin\$(ArchDir)\instrumented' artifact: profile_bin_$(Name) displayName: 'Publish collected data and binaries' - ${{ if eq(parameters.PGUpdate, 'true') }}: - ${{ if ne(parameters.PGRun, 'true') }}: # Didn't run/build in this job, so download files - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: profile_bin_$(Name)' inputs: buildType: current artifact: profile_bin_$(Name) targetPath: '$(Build.BinariesDirectory)\bin\$(ArchDir)\instrumented' - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: instrumented_obj_$(Name)' inputs: buildType: current artifact: instrumented_obj_$(Name) targetPath: '$(Build.SourcesDirectory)\PCbuild\obj' - powershell: | if (Test-Path PCbuild\obj\frozen_modules) { copy -force -r PCbuild\obj\frozen_modules\* (mkdir -Force Python\frozen_modules) } if (Test-Path PCbuild\obj\deepfreeze) { copy -force -r PCbuild\obj\deepfreeze\* (mkdir -Force Python\deepfreeze) } displayName: 'Restore frozen_modules' - powershell: | $env:SigningCertificate = $null .\PCbuild\build.bat -v -p $(Platform) -c PGUpdate $(ExtraOptions) displayName: 'Run build with PGO' env: IncludeUwp: true Py_OutDir: '$(Build.BinariesDirectory)\bin' - powershell: | $kitroot = (gp 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots\').KitsRoot10 $tool = (gci -r "$kitroot\Bin\*\x64\signtool.exe" | sort FullName -Desc | select -First 1) if (-not $tool) { throw "SDK is not available" } Write-Host "##vso[task.prependpath]$($tool.Directory)" displayName: 'Add WinSDK tools to path' - powershell: | $env:SigningCertificate = $null $(_HostPython) PC\layout -vv -b "$(Build.BinariesDirectory)\bin" -t "$(Build.BinariesDirectory)\catalog" --catalog "${env:CAT}.cdf" --preset-default --arch $(Arch) makecat "${env:CAT}.cdf" del "${env:CAT}.cdf" if (-not (Test-Path "${env:CAT}.cat")) { throw "Failed to build catalog file" } displayName: 'Generate catalog' env: CAT: $(Build.BinariesDirectory)\bin\$(ArchDir)\python PYTHON_HEXVERSION: $(VersionHex) - powershell: | del instrumented -r -EA 0 del *.pgc, *.pgd, *.exp displayName: 'Cleanup build' workingDirectory: '$(Build.BinariesDirectory)\bin\$(ArchDir)' - powershell: | copy "$(Build.SourcesDirectory)\Lib\venv\scripts\common\Activate.ps1" . displayName: 'Copy Powershell scripts from source' workingDirectory: '$(Build.BinariesDirectory)\bin\$(ArchDir)' - publish: '$(Build.BinariesDirectory)\bin\$(ArchDir)' artifact: $(Artifact) displayName: 'Publish binaries' ================================================ FILE: windows-release/build-steps.yml ================================================ parameters: ShouldPGO: false steps: - template: ./checkout.yml - template: ./acquire-vcruntime.yml - powershell: | $d = (.\PCbuild\build.bat -V) | %{ if($_ -match '\s+(\w+):\s*(.+)\s*$') { @{$Matches[1] = $Matches[2];} }}; Write-Host "##vso[task.setvariable variable=VersionText]$($d.PythonVersion)" Write-Host "##vso[task.setvariable variable=VersionNumber]$($d.PythonVersionNumber)" Write-Host "##vso[task.setvariable variable=VersionHex]$($d.PythonVersionHex)" Write-Host "##vso[task.setvariable variable=VersionUnique]$($d.PythonVersionUnique)" Write-Host "##vso[build.addbuildtag]$($d.PythonVersion)" Write-Host "##vso[build.addbuildtag]$($d.PythonVersion)-$(Name)" displayName: 'Extract version numbers' - ${{ if eq(parameters.ShouldPGO, 'false') }}: - powershell: | $env:SigningCertificate = $null .\PCbuild\build.bat -v -p $(Platform) -c $(Configuration) $(ExtraOptions) displayName: 'Run build' env: IncludeUwp: true Py_OutDir: '$(Build.BinariesDirectory)\bin' - ${{ else }}: - powershell: | $env:SigningCertificate = $null .\PCbuild\build.bat -v -p $(Platform) --pgo $(ExtraOptions) displayName: 'Run build with PGO' env: IncludeUwp: true Py_OutDir: '$(Build.BinariesDirectory)\bin' - powershell: | $kitroot = (gp 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots\').KitsRoot10 $tool = (gci -r "$kitroot\Bin\*\x64\signtool.exe" | sort FullName -Desc | select -First 1) if (-not $tool) { throw "SDK is not available" } Write-Host "##vso[task.prependpath]$($tool.Directory)" displayName: 'Add WinSDK tools to path' - powershell: | $env:SigningCertificate = $null $(_HostPython) PC\layout -vv -b "$(Build.BinariesDirectory)\bin" -t "$(Build.BinariesDirectory)\catalog" --catalog "${env:CAT}.cdf" --preset-default --arch $(Arch) makecat "${env:CAT}.cdf" del "${env:CAT}.cdf" if (-not (Test-Path "${env:CAT}.cat")) { throw "Failed to build catalog file" } displayName: 'Generate catalog' env: CAT: $(Build.BinariesDirectory)\bin\$(ArchDir)\python PYTHON_HEXVERSION: $(VersionHex) - powershell: | del *.pgc, *.pgd, *.exp displayName: 'Cleanup binaries' workingDirectory: '$(Build.BinariesDirectory)\bin\$(ArchDir)' - powershell: | copy "$(Build.SourcesDirectory)\Lib\venv\scripts\common\Activate.ps1" . displayName: 'Copy Powershell scripts from source' workingDirectory: '$(Build.BinariesDirectory)\bin\$(ArchDir)' - publish: '$(Build.BinariesDirectory)\bin\$(ArchDir)' artifact: $(Artifact) displayName: 'Publish binaries' ================================================ FILE: windows-release/checkout.yml ================================================ parameters: depth: 3 IncludeSelf: false Path: . steps: - ${{ if eq(parameters.IncludeSelf, 'true') }}: - checkout: self path: release-tools - ${{ else }}: - checkout: none - script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(SourceTag) --single-branch https://github.com/$(GitRemote)/cpython.git ${{ parameters.Path }} displayName: 'git clone ($(GitRemote)/$(SourceTag))' condition: and(succeeded(), and(variables['GitRemote'], variables['SourceTag'])) - script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(SourceTag) --single-branch $(Build.Repository.Uri) ${{ parameters.Path }} displayName: 'git clone (/$(SourceTag))' condition: and(succeeded(), and(not(variables['GitRemote']), variables['SourceTag'])) - script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(Build.SourceBranchName) --single-branch https://github.com/$(GitRemote)/cpython.git ${{ parameters.Path }} displayName: 'git clone ($(GitRemote)/)' condition: and(succeeded(), and(variables['GitRemote'], not(variables['SourceTag']))) - script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(Build.SourceBranchName) --single-branch $(Build.Repository.Uri) ${{ parameters.Path }} displayName: 'git clone' condition: and(succeeded(), and(not(variables['GitRemote']), not(variables['SourceTag']))) - powershell: | $checkout_commit = (git rev-parse HEAD) if ($checkout_commit -ne '$(SourceCommit)') { throw "Expected git commit '$(SourceCommit)' didn't match tagged commit '$checkout_commit'" } displayName: "Verify CPython commit matches tag" ${{ if and(parameters.Path, ne(parameters.Path, '.')) }}: workingDirectory: ${{ parameters.Path }} condition: and(succeeded(), variables['SourceCommit']) - powershell: | if (-not (Test-Path "Misc\externals.spdx.json")) { "externals.spdx.json is missing - skipping SBOM" Write-Host "##vso[task.setvariable variable=SkipSBOM]1" } displayName: 'Checking for SBOM inputs' ${{ if and(parameters.Path, ne(parameters.Path, '.')) }}: workingDirectory: ${{ parameters.Path }} condition: and(succeeded(), not(variables['SkipSBOM'])) ================================================ FILE: windows-release/find-sdk.yml ================================================ # Locate the Windows SDK and add its binaries directory to PATH # # `toolname` can be overridden to use a different marker file. parameters: toolname: signtool.exe steps: - powershell: | $kitroot = (gp 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots\').KitsRoot10 $tool = (gci -r "$kitroot\Bin\*\${{ parameters.toolname }}" | sort FullName -Desc | select -First 1) if (-not $tool) { throw "SDK is not available" } Write-Host "##vso[task.prependpath]$($tool.Directory)" Write-Host "Adding $($tool.Directory) to PATH" displayName: 'Add WinSDK tools to path' ================================================ FILE: windows-release/find-tools.yml ================================================ # Locate a set of the tools used for builds steps: - template: ./find-sdk.yml parameters: toolname: 'signtool.exe' - powershell: | $vcvarsall = (& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" ` -prerelease ` -latest ` -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` -find VC\Auxiliary\Build\vcvarsall.bat) Write-Host "Found vcvarsall at $vcvarsall" Write-Host "##vso[task.setVariable variable=vcvarsall]$vcvarsall" displayName: 'Find vcvarsall.bat' - powershell: | $msbuild = (& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" ` -prerelease ` -latest ` -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` -find MSBuild\Current\Bin\msbuild.exe) Write-Host "Found MSBuild at $msbuild" Write-Host "##vso[task.setVariable variable=msbuild]$msbuild" displayName: 'Find MSBuild' ================================================ FILE: windows-release/layout-command.yml ================================================ parameters: Binaries: $(Pipeline.Workspace)\bin_$(Name) Sources: $(Build.SourcesDirectory) Temp: $(Build.BinariesDirectory)\layout-temp Docs: $(Build.BinariesDirectory)\doc LayoutSources: BuildToPackage: current steps: - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: bin_$(HostArch)' condition: and(succeeded(), variables['HostArch']) inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: bin_$(HostArch) targetPath: $(Pipeline.Workspace)\bin_$(HostArch) - powershell: | Write-Host "##vso[task.setvariable variable=Python]${{ parameters.Binaries }}\python.exe" condition: and(succeeded(), not(variables['HostArch'])) displayName: 'Set Python command' - powershell: | Write-Host "##vso[task.setvariable variable=Python]$(Pipeline.Workspace)\bin_$(HostArch)\python.exe" condition: and(succeeded(), variables['HostArch']) displayName: 'Set Python command' - powershell: > $layout_cmd = '& "$(Python)" "{4}\PC\layout" -vv --source "{1}" --build "{0}" --arch "$(Arch)" --temp "{2}" --include-cat "{0}\python.cat" --doc-build "{3}"' -f ( "${{ parameters.Binaries }}", "${{ parameters.Sources }}", "${{ parameters.Temp }}", "${{ parameters.Docs}}", "${{ coalesce(parameters.LayoutSources, parameters.Sources) }}"); Write-Host "##vso[task.setvariable variable=LayoutCmd]$layout_cmd"; Write-Host "Setting LayoutCmd=$layout_cmd" displayName: 'Set LayoutCmd' ================================================ FILE: windows-release/libffi-build.yml ================================================ parameters: - name: SourceTag displayName: 'LibFFI Source Tag' type: string - name: SigningCertificate displayName: "Code signing certificate" type: string default: 'PythonSoftwareFoundation' values: - 'PythonSoftwareFoundation' - 'TestSign' - 'Unsigned' - name: SourcesRepo displayName: 'Sources Repository' type: string default: 'https://github.com/python/cpython-source-deps' - name: LibFFIBuildScript displayName: 'Build script' type: string default: 'https://github.com/python/cpython/raw/main/PCbuild/prepare_libffi.bat' name: ${{ parameters.SourceTag }}_$(Date:yyyyMMdd)$(Rev:.rr) variables: - ${{ if eq(parameters.SigningCertificate, 'PythonSoftwareFoundation') }}: - group: CPythonSign - ${{ if eq(parameters.SigningCertificate, 'TestSign') }}: - group: CPythonTestSign - name: IntDir value: '$(Build.BinariesDirectory)' - name: OutDir value: '$(Build.ArtifactStagingDirectory)' - name: SigningDescription value: 'LibFFI for Python (${{ parameters.SourceTag }})' jobs: - job: Build_LibFFI displayName: LibFFI pool: vmImage: windows-latest workspace: clean: all steps: - checkout: none - template: ./find-tools.yml - powershell: | mkdir -Force "$(IntDir)\script" iwr "${{ parameters.LibFFIBuildScript }}" -outfile "$(IntDir)\script\prepare_libffi.bat" displayName: 'Download build script' - powershell: | git clone ${{ parameters.SourcesRepo }} -b ${{ parameters.SourceTag }} --depth 1 -c core.autocrlf=false -c core.eol=lf . displayName: 'Check out LibFFI sources' - script: 'prepare_libffi.bat --install-cygwin' workingDirectory: '$(IntDir)\script' displayName: 'Install Cygwin and build' env: VCVARSALL: '$(vcvarsall)' LIBFFI_SOURCE: '$(Build.SourcesDirectory)' LIBFFI_OUT: '$(OutDir)' - powershell: | if ((gci *\*.dll).Count -lt 3) { Write-Error "Did not generate enough DLL files" } if ((gci *\Include\ffi.h).Count -lt 3) { Write-Error "Did not generate enough include files" } failOnStderr: true workingDirectory: '$(OutDir)' displayName: 'Verify files were created' - publish: '$(OutDir)' artifact: 'unsigned' displayName: 'Publish unsigned build' - ${{ if ne(parameters.SigningCertificate, 'Unsigned') }}: - job: Sign_LibFFI displayName: Sign LibFFI dependsOn: Build_LibFFI pool: vmImage: windows-latest workspace: clean: all steps: - checkout: none - download: current artifact: unsigned - template: sign-files.yml parameters: Include: '-r *.dll' WorkingDir: '$(Pipeline.Workspace)\unsigned' SigningCertificate: ${{ parameters.SigningCertificate }} - publish: '$(Pipeline.Workspace)\unsigned' artifact: 'libffi' displayName: 'Publish libffi' ================================================ FILE: windows-release/merge-and-upload.py ================================================ import hashlib import json import os import re import subprocess import sys from pathlib import Path from urllib.parse import urlparse from urllib.request import Request, urlopen UPLOAD_URL_PREFIX = os.getenv("UPLOAD_URL_PREFIX", "https://www.python.org/ftp/") UPLOAD_PATH_PREFIX = os.getenv("UPLOAD_PATH_PREFIX", "/srv/www.python.org/ftp/") INDEX_URL = os.getenv("INDEX_URL", UPLOAD_URL_PREFIX + "python/index-windows.json") INDEX_FILE = os.getenv("INDEX_FILE") # A version will be inserted before the extension later on MANIFEST_FILE = os.getenv("MANIFEST_FILE") UPLOAD_HOST = os.getenv("UPLOAD_HOST", "") UPLOAD_HOST_KEY = os.getenv("UPLOAD_HOST_KEY", "") UPLOAD_KEYFILE = os.getenv("UPLOAD_KEYFILE", "") UPLOAD_USER = os.getenv("UPLOAD_USER", "") NO_UPLOAD = os.getenv("NO_UPLOAD", "no")[:1].lower() in "yt1" LOCAL_INDEX = os.getenv("LOCAL_INDEX", "no")[:1].lower() in "yt1" SIGN_COMMAND = os.getenv("SIGN_COMMAND", "") def find_cmd(env, exe): cmd = os.getenv(env) if cmd: cmd = Path(cmd) if not cmd.is_file(): raise RuntimeError( f"Could not find {cmd} to perform upload. Incorrect %{env}% setting." ) return cmd for p in os.getenv("PATH", "").split(";"): if p: cmd = Path(p) / exe if cmd.is_file(): return cmd if UPLOAD_HOST: raise RuntimeError( f"Could not find {exe} to perform upload. Try setting %{env}% or %PATH%" ) print(f"Did not find {exe}, but not uploading anyway.") PLINK = find_cmd("PLINK", "plink.exe") PSCP = find_cmd("PSCP", "pscp.exe") MAKECAT = find_cmd("MAKECAT", "makecat.exe") def _std_args(cmd): if not cmd: raise RuntimeError("Cannot upload because command is missing") all_args = [cmd, "-batch"] if UPLOAD_HOST_KEY: all_args.append("-hostkey") all_args.append(UPLOAD_HOST_KEY) if UPLOAD_KEYFILE: all_args.append("-noagent") all_args.append("-i") all_args.append(UPLOAD_KEYFILE) return all_args class RunError(Exception): pass def _run(*args, single_cmd=False): if single_cmd: args = args[0] with subprocess.Popen( args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="ascii", errors="replace", ) as p: out, _ = p.communicate(None) if out: print(out.encode("ascii", "replace").decode("ascii")) if p.returncode: raise RunError(p.returncode, out) return out def call_ssh(*args, allow_fail=True): if not UPLOAD_HOST or NO_UPLOAD or LOCAL_INDEX: print("Skipping", args, "because UPLOAD_HOST is missing") return "" try: return _run(*_std_args(PLINK), f"{UPLOAD_USER}@{UPLOAD_HOST}", *args) except RunError as ex: if not allow_fail: raise return ex.args[1] def upload_ssh(source, dest): if not UPLOAD_HOST or NO_UPLOAD or LOCAL_INDEX: print("Skipping upload of", source, "because UPLOAD_HOST is missing") return _run(*_std_args(PSCP), source, f"{UPLOAD_USER}@{UPLOAD_HOST}:{dest}") call_ssh(f"chgrp downloads {dest} && chmod g-x,o+r {dest}") def download_ssh(source, dest): if not UPLOAD_HOST: print("Skipping download of", source, "because UPLOAD_HOST is missing") return Path(dest).parent.mkdir(exist_ok=True, parents=True) _run(*_std_args(PSCP), f"{UPLOAD_USER}@{UPLOAD_HOST}:{source}", dest) def ls_ssh(dest): if not UPLOAD_HOST or LOCAL_INDEX: print("Skipping ls of", dest, "because UPLOAD_HOST is missing") return try: _run(*_std_args(PSCP), "-ls", f"{UPLOAD_USER}@{UPLOAD_HOST}:{dest}") except RunError as ex: if not ex.args[1].rstrip().endswith("No such file or directory"): raise print(dest, "was not found") def url2path(url): if not UPLOAD_URL_PREFIX: raise ValueError("%UPLOAD_URL_PREFIX% was not set") if not url: raise ValueError("Unexpected empty URL") if not url.startswith(UPLOAD_URL_PREFIX): if LOCAL_INDEX: return url raise ValueError(f"Unexpected URL: {url}") return UPLOAD_PATH_PREFIX + url[len(UPLOAD_URL_PREFIX) :] def get_hashes(src): h = hashlib.sha256() with open(src, "rb") as f: chunk = f.read(1024 * 1024) while chunk: h.update(chunk) chunk = f.read(1024 * 1024) return {"sha256": h.hexdigest()} def trim_install(install): return { k: v for k, v in install.items() if k not in ("aliases", "run-for", "shortcuts") } def validate_new_installs(installs): ids = [i["id"] for i in installs] id_set = set(ids) if len(id_set) < len(ids): for i in id_set: ids.remove(i) print("WARNING: Duplicate id fields:", *sorted(set(ids))) def purge(url): if not UPLOAD_HOST or NO_UPLOAD: print("Skipping purge of", url, "because UPLOAD_HOST is missing") return print("Purging", url) with urlopen(Request(url, method="PURGE", headers={"Fastly-Soft-Purge": 1})) as r: r.read() def calculate_uploads(): cwd = Path.cwd() for p in sorted( [ *cwd.glob("__install__.*.json"), *[p / "__install__.json" for p in cwd.iterdir()], ] ): if not p.is_file(): continue print("Processing", p) i = json.loads(p.read_bytes()) u = urlparse(i["url"]) src = p.parent / u.path.rpartition("/")[-1] dest = url2path(i["url"]) if LOCAL_INDEX: i["url"] = str(src.relative_to(Path.cwd())).replace("\\", "/") sbom = src.with_suffix(".spdx.json") sbom_dest = dest.rpartition("/")[0] + sbom.name if not sbom.is_file(): sbom = None sbom_dest = None yield ( i, src, url2path(i["url"]), sbom, sbom_dest, ) def sign_json(cat_file, *files): if not MAKECAT: if not UPLOAD_HOST or NO_UPLOAD: print("makecat.exe not found, but not uploading, so skip signing.") return raise RuntimeError("No makecat.exe found") if not SIGN_COMMAND: if not UPLOAD_HOST or NO_UPLOAD: print("No signing command set, but not uploading, so skip signing.") return raise RuntimeError("No SIGN_COMMAND set") cat = Path(cat_file).absolute() cdf = cat.with_suffix(".cdf") cdf.parent.mkdir(parents=True, exist_ok=True) with open(cdf, "w", encoding="ansi") as f: print("[CatalogHeader]", file=f) print("Name=", cat.name, sep="", file=f) print("ResultDir=", cat.parent, sep="", file=f) print("PublicVersion=0x00000001", file=f) print("CatalogVersion=2", file=f) print("HashAlgorithms=SHA256", file=f) print("EncodingType=", file=f) print(file=f) print("[CatalogFiles]", file=f) for a in map(Path, files): print("", a.name, "=", a.absolute(), sep="", file=f) _run(MAKECAT, "-v", cdf) if not cat.is_file(): raise FileNotFoundError(cat) # Pass as a single arg because the command variable has its own arguments _run(f'{SIGN_COMMAND} "{cat}"', single_cmd=True) cdf.unlink() def remove_and_insert(index, new_installs): new = {(i["id"].casefold(), i["sort-version"].casefold()) for i in new_installs} to_remove = [ x for x, i in enumerate(index) if (i["id"].casefold(), i["sort-version"].casefold()) in new ] for i in reversed(to_remove): del index[i] index[:0] = new_installs print("Added", len(new_installs), "entries:") for i in new_installs: print("-", i["id"], i["sort-version"]) print("Replaced", len(to_remove), "existing entries") print() def hash_packages(uploads): for i, src, *_ in uploads: i["hash"] = get_hashes(src) def number_sortkey(n): try: return f"{int(n):020}" except ValueError: return n def install_sortkey(install): key = re.split(r"(\d+)", install["id"]) ver = re.split(r"(\d+)", install["sort-version"]) return ( tuple(number_sortkey(k) for k in key), tuple(number_sortkey(k) for k in ver), ) def find_missing_from_index(url, installs): if not UPLOAD_HOST: print("Skipping check for upload race because UPLOAD_HOST is missing") return [] if NO_UPLOAD: print("Skipping check for upload race because NO_UPLOAD is set") return [] with urlopen(url) as r: x = {install_sortkey(i) for i in json.load(r)["versions"]} y = {install_sortkey(i) for i in installs} - x return [i for i in installs if install_sortkey(i) in y] UPLOADS = list(calculate_uploads()) if not UPLOADS: print("No files to upload!") sys.exit(1) hash_packages(UPLOADS) index = {"versions": []} INDEX_MTIME = 0 if INDEX_FILE: INDEX_PATH = url2path(INDEX_URL) try: INDEX_MTIME = int(call_ssh("stat", "-c", "%Y", INDEX_PATH) or "0") except ValueError: pass try: if not LOCAL_INDEX: download_ssh(INDEX_PATH, INDEX_FILE) except RunError as ex: err = ex.args[1] if not err.rstrip().endswith("no such file or directory"): raise else: try: with open(INDEX_FILE, encoding="utf-8") as f: index = json.load(f) except FileNotFoundError: pass print(INDEX_PATH, "mtime =", INDEX_MTIME) new_installs = [trim_install(i) for i, *_ in UPLOADS] validate_new_installs(new_installs) new_installs = sorted(new_installs, key=install_sortkey) remove_and_insert(index["versions"], new_installs) if INDEX_FILE: INDEX_FILE = Path(INDEX_FILE).absolute() INDEX_CAT_FILE = INDEX_FILE.with_name(f"{INDEX_FILE.name}.cat") INDEX_FILE.parent.mkdir(parents=True, exist_ok=True) with open(INDEX_FILE, "w", encoding="utf-8") as f: json.dump(index, f) sign_json(INDEX_CAT_FILE, INDEX_FILE) INDEX_CAT_URL = f"{INDEX_URL}.cat" INDEX_CAT_PATH = f"{INDEX_PATH}.cat" else: INDEX_CAT_FILE = None INDEX_CAT_URL = None INDEX_CAT_PATH = None if MANIFEST_FILE: # Use the sort-version so that the manifest name includes prerelease marks MANIFEST_FILE = Path(MANIFEST_FILE).absolute() name = f"{MANIFEST_FILE.stem}-{new_installs[0]['sort-version']}.json" MANIFEST_FILE = MANIFEST_FILE.with_name(name) MANIFEST_URL = new_installs[0]["url"].rpartition("/")[0] + "/" + name MANIFEST_PATH = url2path(MANIFEST_URL) with open(MANIFEST_FILE, "w", encoding="utf-8") as f: # Include an indent for readability. The release manifest is # far more likely to be read by humans than the index. json.dump({"versions": new_installs}, f, indent=2) # Upload last to ensure we've got a valid index first for i, src, dest, sbom, sbom_dest in UPLOADS: print("Uploading", src, "to", dest) destdir = dest.rpartition("/")[0] call_ssh(f"mkdir {destdir} && chgrp downloads {destdir} && chmod a+rx {destdir}") upload_ssh(src, dest) if sbom and sbom_dest: upload_ssh(sbom, sbom_dest) # Check that nobody else has published while we were uploading if INDEX_FILE and INDEX_MTIME: try: mtime = int(call_ssh("stat", "-c", "%Y", INDEX_PATH) or "0") except ValueError: mtime = 0 if mtime > INDEX_MTIME: print("##[error]Lost a race with another publish step!") print("Expecting mtime", INDEX_MTIME, "but saw", mtime) sys.exit(1) TO_PURGE = [i["url"] for i, *_ in UPLOADS] if MANIFEST_FILE: print("Uploading", MANIFEST_FILE, "to", MANIFEST_URL) upload_ssh(MANIFEST_FILE, MANIFEST_PATH) TO_PURGE.append(MANIFEST_URL) if INDEX_FILE: print("Uploading", INDEX_FILE, "to", INDEX_URL) upload_ssh(INDEX_FILE, INDEX_PATH) TO_PURGE.append(INDEX_URL) if INDEX_CAT_FILE: print("Uploading", INDEX_CAT_FILE, "to", INDEX_CAT_URL) upload_ssh(INDEX_CAT_FILE, INDEX_CAT_PATH) TO_PURGE.append(INDEX_CAT_URL) # Calculate directory parents for all files TO_PURGE.extend({i.rpartition("/")[0] + "/" for i in TO_PURGE}) print("Purging", len(TO_PURGE), "uploaded files, indexes and directories") for i in TO_PURGE: purge(i) if INDEX_URL: missing = find_missing_from_index(INDEX_URL, [i for i, *_ in UPLOADS]) if missing: print("##[error]Lost a race with another publish step!") print("Index at", INDEX_URL, "does not contain installs:") for m in missing: print(m["id"], m["sort-version"]) sys.exit(1) ================================================ FILE: windows-release/msi-steps.yml ================================================ parameters: BuildToPackage: current DoFreethreaded: false SigningCertificate: '' Artifacts: [] steps: - template: ./checkout.yml - powershell: | $d = (.\PCbuild\build.bat -V) | %{ if($_ -match '\s+(\w+):\s*(.+)\s*$') { @{$Matches[1] = $Matches[2];} }}; Write-Host "##vso[task.setvariable variable=SigningDescription]Python $($d.PythonVersion)" displayName: 'Update signing description' condition: and(succeeded(), not(variables['SigningDescription'])) - ${{ each a in parameters.Artifacts }}: - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: ${{ a.artifact }}' inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: ${{ a.artifact }} ${{ if a.target }}: targetPath: ${{ a.target }} ${{ else }}: targetPath: $(Pipeline.Workspace)\${{ a.artifact }} # Assuming we'll always run the amd64 build - ${{ if eq(a.artifact, 'bin_amd64') }}: - powershell: > copy "${env:TARGET_PATH}\Activate.ps1" "$(Build.SourcesDirectory)\Lib\venv\scripts\common\Activate.ps1" -Force -Verbose displayName: 'Copy signed files into sources' env: ${{ if a.target }}: TARGET_PATH: ${{ a.target }} ${{ else }}: TARGET_PATH: $(Pipeline.Workspace)\${{ a.artifact }} - script: | call PCbuild\find_python.bat "%PYTHON%" call Tools\msi\get_externals.bat call PCbuild\find_msbuild.bat echo ##vso[task.setvariable variable=PYTHON]%PYTHON% echo ##vso[task.setvariable variable=MSBUILD]%MSBUILD% %PYTHON% -m ensurepip displayName: 'Get external dependencies' env: PYTHON: $(Build.BinariesDirectory)\win32\python.exe PYTHONHOME: $(Build.SourcesDirectory) - powershell: | ("/p:Py_OutDir=" + $env:BUILD_BINARIESDIRECTORY) | Out-File msbuild.rsp -Encoding UTF8 "/p:BuildForRelease=true" | Out-File msbuild.rsp -Append -Encoding UTF8 Write-Host "##vso[task.setvariable variable=ResponseFile]$(gi msbuild.rsp)" gc msbuild.rsp displayName: 'Generate response file' - ${{ if eq(parameters.DoFreethreaded, 'true') }}: - powershell: | "/p:IncludeFreethreaded=true" | Out-File "$(ResponseFile)" -Append -Encoding UTF8 gc "$(ResponseFile)" displayName: 'Add Include_freethreaded to response file' - ${{ if parameters.SigningCertificate }}: - template: sign-files.yml parameters: Include: '' ExportCommand: SignCommand SigningCertificate: ${{ parameters.SigningCertificate }} # WiX never moved on from signtool.exe, so we'll use that here InstallLegacyTool: true - powershell: | $cmd = $env:SignCommand -replace '"', '\"' "/p:_SignCommand=""$cmd""" | Out-File $env:ResponseFile -Append -Encoding UTF8 displayName: 'Inject sign command into response file' env: SignCommand: $(SignCommand) ResponseFile: $(ResponseFile) - script: | %MSBUILD% Tools\msi\launcher\launcher.wixproj "@$(ResponseFile)" displayName: 'Build launcher installer' env: Platform: x86 # Only need the variable here for msi.props to detect SigningCertificate: ${{ parameters.SigningCertificate }} ${{ if parameters.SigningCertificate }}: AZURE_TENANT_ID: $(TrustedSigningTenantId) AZURE_CLIENT_ID: $(TrustedSigningClientId) AZURE_CLIENT_SECRET: $(TrustedSigningClientSecret) - ${{ each b in parameters.Bundles }}: - script: | %MSBUILD% Tools\msi\bundle\releaselocal.wixproj /t:Rebuild /p:RebuildAll=true "@$(ResponseFile)" displayName: 'Build ${{ b.bundle }} installer' env: Platform: ${{ b.Platform }} PYTHON: ${{ b.PythonForBuild }}\python.exe PythonForBuild: ${{ b.PythonForBuild }}\python.exe PYTHONHOME: $(Build.SourcesDirectory) ${{ if b.TclTkArtifact }}: TclTkLibraryDir: $(Pipeline.Workspace)\${{ b.TclTkArtifact }} # Only need the variable here for msi.props to detect SigningCertificate: ${{ parameters.SigningCertificate }} ${{ if parameters.SigningCertificate }}: AZURE_TENANT_ID: $(TrustedSigningTenantId) AZURE_CLIENT_ID: $(TrustedSigningClientId) AZURE_CLIENT_SECRET: $(TrustedSigningClientSecret) - powershell: | del $env:ResponseFile -ErrorAction Continue displayName: 'Remove response file (always runs)' condition: ne(variables['ResponseFile'], '') env: ResponseFile: $(ResponseFile) - ${{ each b in parameters.Bundles }}: - task: CopyFiles@2 displayName: 'Assemble artifact: msi (${{ b.bundle }})' inputs: sourceFolder: $(Build.BinariesDirectory)\${{ b.bundle }}\en-us targetFolder: $(Build.ArtifactStagingDirectory)\msi\${{ b.bundle }} contents: | *.msi *.cab *.exe - powershell: | git clone $(Build.Repository.Uri) -b $(Build.SourceBranchName) --single-branch --no-checkout "$(Pipeline.Workspace)\release-tools" git -C "$(Pipeline.Workspace)\release-tools" checkout $(Build.SourceVersion) displayName: 'Clone the python/release-tools repository' - powershell: > & $(${env:Python}.Trim('"')) "$(Pipeline.Workspace)\release-tools\sbom.py" "--cpython-source-dir=$(Build.SourcesDirectory)" $(gci -r "$(Build.ArtifactStagingDirectory)\msi\**\python-*.exe") workingDirectory: $(Build.BinariesDirectory) condition: and(succeeded(), not(variables['SkipSBOM'])) displayName: 'Create SBOMs for binaries' env: PYTHON: $(Build.BinariesDirectory)\win32\python.exe PYTHONHOME: $(Build.SourcesDirectory) - task: CopyFiles@2 displayName: 'Layout Artifact: sbom' condition: and(succeeded(), not(variables['SkipSBOM'])) inputs: sourceFolder: $(Build.ArtifactStagingDirectory)\msi targetFolder: $(Build.ArtifactStagingDirectory)\sbom flatten: true flattenFolders: true contents: | **\*.spdx.json - publish: '$(Build.ArtifactStagingDirectory)\msi' artifact: msi displayName: 'Publish MSI' - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: sbom' condition: and(succeeded(), not(variables['SkipSBOM'])) inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)\sbom' ArtifactName: sbom ================================================ FILE: windows-release/openssl-build.yml ================================================ parameters: - name: SourceTag displayName: 'OpenSSL Source Tag' type: string - name: SigningCertificate displayName: "Code signing certificate" type: string default: 'PythonSoftwareFoundation' values: - 'PythonSoftwareFoundation' - 'TestSign' - 'Unsigned' - name: SourcesRepo displayName: 'Sources Repository' type: string default: 'https://github.com/python/cpython-source-deps' - name: CustomBuildVM displayName: 'Custom build VM' type: boolean default: false - name: VCVarsOptions displayName: 'vcvarsall.bat options' type: string default: '(none)' - name: IncludeX86 displayName: 'Include x86' type: boolean default: true - name: IncludeX64 displayName: 'Include x64' type: boolean default: true - name: IncludeARM64 displayName: 'Include ARM64' type: boolean default: true name: ${{ parameters.SourceTag }}_$(Date:yyyyMMdd)$(Rev:.rr) variables: - ${{ if eq(parameters.SigningCertificate, 'PythonSoftwareFoundation') }}: - group: CPythonSign - ${{ if eq(parameters.SigningCertificate, 'TestSign') }}: - group: CPythonTestSign - name: IntDir value: '$(Build.BinariesDirectory)' - name: OutDir value: '$(Build.ArtifactStagingDirectory)' - name: SigningDescription value: 'OpenSSL for Python (${{ parameters.SourceTag }})' jobs: - job: Build_SSL displayName: OpenSSL pool: ${{ if eq(parameters.CustomBuildVM, 'true') }}: name: 'Windows Release' ${{ else }}: vmImage: windows-latest strategy: matrix: ${{ if eq(parameters.IncludeX86, 'true') }}: win32: Platform: 'win32' VCPlatform: 'amd64_x86' OpenSSLPlatform: 'VC-WIN32 no-asm' ${{ if eq(parameters.IncludeX64, 'true') }}: amd64: Platform: 'amd64' VCPlatform: 'amd64' OpenSSLPlatform: 'VC-WIN64A-masm' ${{ if eq(parameters.IncludeARM64, 'true') }}: arm64: Platform: 'arm64' VCPlatform: 'amd64_arm64' OpenSSLPlatform: 'VC-WIN64-ARM' workspace: clean: all steps: - checkout: none - template: ./find-tools.yml - powershell: | git clone ${{ parameters.SourcesRepo }} -b ${{ parameters.SourceTag }} --depth 1 . displayName: 'Check out OpenSSL sources' - script: | call "$(vcvarsall)" $(VCPlatform) %EXTRA_OPTS% perl "$(Build.SourcesDirectory)\Configure" $(OpenSSLPlatform) no-uplink nmake workingDirectory: '$(IntDir)' displayName: 'Build OpenSSL' env: ${{ if ne(parameters.VCVarsOptions, '(none)') }}: EXTRA_OPTS: ${{ parameters.VCVarsOptions }} - ${{ if ne(parameters.SigningCertificate, 'Unsigned') }}: - template: sign-files.yml parameters: Include: 'lib*.dll' WorkingDir: $(IntDir) SigningCertificate: ${{ parameters.SigningCertificate }} - task: CopyFiles@2 displayName: 'Copy built libraries for upload' inputs: SourceFolder: '$(IntDir)' Contents: | lib*.dll lib*.pdb lib*.lib include\openssl\*.h TargetFolder: '$(OutDir)' - task: CopyFiles@2 displayName: 'Copy header files for upload' inputs: SourceFolder: '$(Build.SourcesDirectory)' Contents: | include\openssl\* TargetFolder: '$(OutDir)' - task: CopyFiles@2 displayName: 'Copy applink files for upload' inputs: SourceFolder: '$(Build.SourcesDirectory)\ms' Contents: applink.c TargetFolder: '$(OutDir)\include' - task: CopyFiles@2 displayName: 'Copy LICENSE for upload' inputs: SourceFolder: '$(Build.SourcesDirectory)' Contents: | LICENSE LICENSE.txt TargetFolder: '$(OutDir)' - publish: '$(OutDir)' artifact: '$(Platform)' displayName: 'Publishing $(Platform)' ================================================ FILE: windows-release/purge.py ================================================ # Purges the Fastly cache for Windows download files # # Usage: # py -3 purge.py 3.5.1rc1 # __author__ = "Steve Dower " __version__ = "1.0.0" import re import sys from urllib.request import Request, urlopen VERSION_RE = re.compile(r"(\d+\.\d+\.\d+)([A-Za-z_]+\d+)?$") try: m = VERSION_RE.match(sys.argv[1]) if not m: print("Invalid version:", sys.argv[1]) print('Expected something like "3.5.1rc1"') sys.exit(1) except LookupError: print('Missing version argument. Expected something like "3.5.1rc1"') sys.exit(1) URL = f"https://www.python.org/ftp/python/{m.group(1)}/" REL = m.group(2) or "" FILES = [ "core.msi", "core_d.msi", "core_pdb.msi", "dev.msi", "dev_d.msi", "doc.msi", "exe.msi", "exe_d.msi", "exe_pdb.msi", "freethreaded.msi", "freethreaded_d.msi", "freethreaded_pdb.msi", "launcher.msi", "lib.msi", "lib_d.msi", "lib_pdb.msi", "path.msi", "pip.msi", "tcltk.msi", "tcltk_d.msi", "tcltk_pdb.msi", "test.msi", "test_d.msi", "test_pdb.msi", "tools.msi", "ucrt.msi", ] PATHS = [ f"python-{m.group(0)}.exe", f"python-{m.group(0)}-webinstall.exe", f"python-{m.group(0)}-amd64.exe", f"python-{m.group(0)}-amd64-webinstall.exe", f"python-{m.group(0)}-arm64.exe", f"python-{m.group(0)}-arm64-webinstall.exe", f"python-{m.group(0)}-embed-amd64.zip", f"python-{m.group(0)}-embed-win32.zip", f"python-{m.group(0)}-embed-arm64.zip", *[f"win32{REL}/{f}" for f in FILES], *[f"amd64{REL}/{f}" for f in FILES], *[f"arm64{REL}/{f}" for f in FILES], ] PATHS = PATHS + [p + ".asc" for p in PATHS] print("Purged:") for n in PATHS: u = URL + n with urlopen(Request(u, method="PURGE", headers={"Fastly-Soft-Purge": 1})) as r: r.read() print(" ", u) ================================================ FILE: windows-release/sign-files.yml ================================================ parameters: Include: '*' Exclude: '' Filter: '' WorkingDir: '$(Build.BinariesDirectory)' ExtractDir: '' SigningCertificate: '' ExportCommand: '' ExportLegacyCommand: '' ContinueOnError: false InstallTool: true InstallLegacyTool: false AzureServiceConnectionName: 'Python Signing' steps: - ${{ if and(parameters.SigningCertificate, ne(parameters.SigningCertificate, 'Unsigned')) }}: - ${{ if eq(parameters.InstallTool, 'true') }}: - powershell: | # Install sign tool dotnet tool install --global --prerelease sign $signtool = (gcm sign -EA SilentlyContinue).Source if (-not $signtool) { $signtool = (gi "${env:USERPROFILE}\.dotnet\tools\sign.exe").FullName } $signargs = 'code artifact-signing -v Information ' + ` '-fd sha256 -t http://timestamp.acs.microsoft.com -td sha256 ' + ` "-ase ""${env:ASE}"" -asa ""${env:ASA}"" -ascp ""${env:ASCP}"" " + ` "-act azure-cli -d ""${env:DESCRIPTION}""" Write-Host "##vso[task.setvariable variable=__TrustedSigningCmd]$signtool" Write-Host "##vso[task.setvariable variable=__TrustedSigningArgs]$signargs" if ($env:EXPORT_COMMAND) { $signcmd = """$signtool"" $signargs" Write-Host "##vso[task.setvariable variable=${env:EXPORT_COMMAND}]$signcmd" } workingDirectory: $(Build.BinariesDirectory) displayName: 'Install Azure Artifact Signing tools' env: ASE: $(TrustedSigningUri) ASA: $(TrustedSigningAccount) ASCP: $(TrustedSigningCertificateName) DESCRIPTION: $(SigningDescription) EXPORT_COMMAND: ${{ parameters.ExportCommand }} - ${{ if eq(parameters.InstallLegacyTool, 'true') }}: - powershell: | git clone https://github.com/python/cpython-bin-deps --revision fb06137dccc43ed5b030cdd9e3560990b37f39da --depth 1 --progress -v "signtool" $signtool = gi signtool\x64\signtool.exe $dlib = gi signtool\azure_trusted_signing\x64\Azure.CodeSigning.Dlib.dll Write-Host "##vso[task.setvariable variable=MAKECAT]$(gi signtool\x64\makecat.exe)" ConvertTo-Json @{ Endpoint=$env:ASE; CodeSigningAccountName=$env:ASA; CertificateProfileName=$env:ASCP; # Only allow Azure CLI credentials ExcludeCredentials=@( "ManagedIdentityCredential", "WorkloadIdentityCredential", "SharedTokenCacheCredential", "EnvironmentCredential", "VisualStudioCredential", "VisualStudioCodeCredential", "AzurePowerShellCredential", "AzureDeveloperCliCredential", "InteractiveBrowserCredential" ); } | Out-File signtool\metadata.json -Encoding ascii Write-Host "##vso[task.setvariable variable=SIGNTOOL_METADATA]$(gi signtool\metadata.json)" $signargs = 'sign /v /fd sha256 /tr http://timestamp.acs.microsoft.com /td sha256 ' + ` "/dlib ""$dlib"" /dmdf ""$(gi signtool\metadata.json)"" " + ` "/d ""${env:DESCRIPTION}""" Write-Host "##vso[task.setvariable variable=__TrustedSigningCmd]$signtool" Write-Host "##vso[task.setvariable variable=__TrustedSigningArgs]$signargs" if ($env:EXPORT_COMMAND) { $signcmd = """$signtool"" $signargs" Write-Host "##vso[task.setvariable variable=${env:EXPORT_COMMAND}]$signcmd" } workingDirectory: $(Pipeline.Workspace) displayName: 'Download signtool binaries' env: ASE: $(TrustedSigningUri) ASA: $(TrustedSigningAccount) ASCP: $(TrustedSigningCertificateName) DESCRIPTION: $(SigningDescription) EXPORT_COMMAND: ${{ parameters.ExportLegacyCommand }} - ${{ if parameters.AzureServiceConnectionName }}: # We sign in once with the AzureCLI task, as it uses OIDC to obtain a # temporary token. But the task also logs out, and so we save the token and # use it to log in persistently (for the rest of the build). - task: AzureCLI@2 displayName: 'Authenticate signing tools (1/2)' inputs: azureSubscription: ${{ parameters.AzureServiceConnectionName }} scriptType: 'ps' scriptLocation: 'inlineScript' inlineScript: | "##vso[task.setvariable variable=__AZURE_CLIENT_ID;issecret=true]${env:servicePrincipalId}" "##vso[task.setvariable variable=__AZURE_ID_TOKEN;issecret=true]${env:idToken}" "##vso[task.setvariable variable=__AZURE_TENANT_ID;issecret=true]${env:tenantId}" addSpnToEnvironment: true - powershell: > az login --service-principal -u $(__AZURE_CLIENT_ID) --tenant $(__AZURE_TENANT_ID) --allow-no-subscriptions --federated-token $(__AZURE_ID_TOKEN) displayName: 'Authenticate signing tools (2/2)' - ${{ if parameters.Include }}: - powershell: | if ($env:EXCLUDE) { $files = (dir ($env:INCLUDE -split ',').Trim() -Exclude ($env:EXCLUDE -split ',').Trim() -File) } else { $files = (dir ($env:INCLUDE -split ',').Trim() -File) } if ($env:FILTER) { ($env:FILTER -split ';').Trim() -join "`n" | Out-File __filelist.txt -Encoding utf8 } else { "*" | Out-File __filelist.txt -Encoding utf8 } foreach ($f in $files) { & $env:TRUSTED_SIGNING_CMD @(-split $env:TRUSTED_SIGNING_ARGS) -fl __filelist.txt $f if (-not $?) { exit $LASTEXITCODE } } del __filelist.txt displayName: 'Sign binaries' ${{ if eq(parameters.ContinueOnError, 'false') }}: retryCountOnTaskFailure: 3 ${{ else }}: continueOnError: true workingDirectory: ${{ parameters.WorkingDir }} env: INCLUDE: ${{ parameters.Include }} EXCLUDE: ${{ parameters.Exclude }} TRUSTED_SIGNING_CMD: $(__TrustedSigningCmd) TRUSTED_SIGNING_ARGS: $(__TrustedSigningArgs) ${{ if parameters.Filter }}: FILTER: ${{ parameters.Filter }} - ${{ if parameters.ExtractDir }}: - powershell: | if ($env:EXCLUDE) { $files = (dir ($env:INCLUDE -split ',').Trim() -Exclude ($env:EXCLUDE -split ',').Trim() -File) } else { $files = (dir ($env:INCLUDE -split ',').Trim() -File) } $c = $files | %{ (Get-AuthenticodeSignature $_).SignerCertificate } | ?{ $_ -ne $null } | select -First 1 if (-not $c) { Write-Host "Failed to find certificate for ${{ parameters.SigningCertificate }}" exit } $d = mkdir $env:EXTRACT_DIR -Force $cf = "$d\cert.cer" [IO.File]::WriteAllBytes($cf, $c.RawData) $csha = (Get-FileHash $cf -Algorithm SHA256).Hash.ToLower() $info = @{ Subject=$c.Subject; SHA256=$csha; } $info | ConvertTo-JSON -Compress | Out-File -Encoding utf8 "$d\certinfo.json" displayName: "Extract certificate info" workingDirectory: ${{ parameters.WorkingDir }} env: INCLUDE: ${{ parameters.Include }} EXCLUDE: ${{ parameters.Exclude }} EXTRACT_DIR: ${{ parameters.ExtractDir }} ================================================ FILE: windows-release/stage-build.yml ================================================ parameters: DoPGO: false # DoPGOARM64 only applies if DoPGO is also true DoPGOARM64: true DoFreethreaded: false ToBeSigned: false ExtraOptions: '' ExtraOptionsFreethreaded: '--disable-gil' TailCallingOption: '' Post315OutputDir: false jobs: - job: Build_Docs displayName: Docs build dependsOn: [] workspace: clean: all steps: - template: ./checkout.yml - script: Doc\make.bat html displayName: 'Build HTML docs' env: BUILDDIR: $(Build.BinariesDirectory)\Doc - task: CopyFiles@2 displayName: 'Assemble artifact: Doc' inputs: sourceFolder: $(Build.BinariesDirectory)\Doc targetFolder: $(Build.ArtifactStagingDirectory)\Doc contents: | html\**\* - publish: $(Build.ArtifactStagingDirectory)\Doc artifact: doc displayName: 'Publish artifact: doc' - job: Build_Python displayName: Python build dependsOn: [] workspace: clean: all strategy: matrix: win32: Name: win32 Arch: win32 ArchDir: win32 Platform: x86 Configuration: Release _HostPython: .\python ExtraOptions: ${{ parameters.ExtraOptions }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_win32 ${{ else }}: Artifact: bin_win32 win32_d: Name: win32_d Arch: win32 ArchDir: win32 Platform: x86 Configuration: Debug _HostPython: .\python ExtraOptions: ${{ parameters.ExtraOptions }} Artifact: bin_win32_d ${{ if ne(parameters.DoPGO, 'true') }}: amd64: Name: amd64 Arch: amd64 ArchDir: amd64 Platform: x64 Configuration: Release _HostPython: .\python ExtraOptions: ${{ parameters.ExtraOptions }} ${{ parameters.TailCallingOption }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_amd64 ${{ else }}: Artifact: bin_amd64 amd64_d: Name: amd64_d Arch: amd64 ArchDir: amd64 Platform: x64 Configuration: Debug _HostPython: .\python ExtraOptions: ${{ parameters.ExtraOptions }} Artifact: bin_amd64_d ${{ if or(ne(parameters.DoPGO, 'true'), ne(parameters.DoPGOARM64, 'true')) }}: arm64: Name: arm64 Arch: arm64 ArchDir: arm64 Platform: ARM64 Configuration: Release _HostPython: python ExtraOptions: ${{ parameters.ExtraOptions }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_arm64 ${{ else }}: Artifact: bin_arm64 arm64_d: Name: arm64_d Arch: arm64 ArchDir: arm64 Platform: ARM64 Configuration: Debug _HostPython: python ExtraOptions: ${{ parameters.ExtraOptions }} Artifact: bin_arm64_d ${{ if eq(parameters.DoFreethreaded, 'true') }}: win32_t: Name: win32_t Arch: win32 ArchDir: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'win32t', 'win32') }} Platform: x86 Configuration: Release _HostPython: .\python ExtraOptions: ${{ parameters.ExtraOptionsFreethreaded }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_win32_t ${{ else }}: Artifact: bin_win32_t win32_td: Name: win32_td Arch: win32 ArchDir: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'win32t', 'win32') }} Platform: x86 Configuration: Debug _HostPython: .\python ExtraOptions: ${{ parameters.ExtraOptionsFreethreaded }} Artifact: bin_win32_td ${{ if ne(parameters.DoPGO, 'true') }}: amd64_t: Name: amd64_t Arch: amd64 ArchDir: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'amd64t', 'amd64') }} Platform: x64 Configuration: Release _HostPython: .\python ExtraOptions: ${{ parameters.ExtraOptionsFreethreaded }} ${{ parameters.TailCallingOption }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_amd64_t ${{ else }}: Artifact: bin_amd64_t amd64_td: Name: amd64_td Arch: amd64 ArchDir: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'amd64t', 'amd64') }} Platform: x64 Configuration: Debug _HostPython: .\python ExtraOptions: ${{ parameters.ExtraOptionsFreethreaded }} Artifact: bin_amd64_td ${{ if or(ne(parameters.DoPGO, 'true'), ne(parameters.DoPGOARM64, 'true')) }}: arm64_t: Name: arm64_t Arch: arm64 ArchDir: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'arm64t', 'arm64') }} Platform: ARM64 Configuration: Release _HostPython: python ExtraOptions: ${{ parameters.ExtraOptionsFreethreaded }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_arm64_t ${{ else }}: Artifact: bin_arm64_t arm64_td: Name: arm64_td Arch: arm64 ArchDir: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'arm64t', 'arm64') }} Platform: ARM64 Configuration: Debug _HostPython: python ExtraOptions: ${{ parameters.ExtraOptionsFreethreaded }} Artifact: bin_arm64_td steps: - template: ./build-steps.yml - ${{ if eq(parameters.DoPGO, 'true') }}: - job: Build_Python_PGO_Native displayName: Python PGO build dependsOn: [] workspace: clean: all strategy: matrix: amd64: Name: amd64 Arch: amd64 ArchDir: amd64 Platform: x64 _HostPython: .\python PythonExePattern: python.exe ExtraOptions: ${{ parameters.ExtraOptions }} ${{ parameters.TailCallingOption }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_amd64 ${{ else }}: Artifact: bin_amd64 ${{ if eq(parameters.DoFreethreaded, 'true') }}: amd64_t: Name: amd64_t Arch: amd64 ArchDir: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'amd64t', 'amd64') }} Platform: x64 _HostPython: .\python PythonExePattern: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'python.exe', 'python3*t.exe') }} ExtraOptions: ${{ parameters.ExtraOptionsFreethreaded }} ${{ parameters.TailCallingOption }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_amd64_t ${{ else }}: Artifact: bin_amd64_t steps: - template: ./build-steps-pgo.yml parameters: PGInstrument: true PGRun: true PGUpdate: true - ${{ if eq(parameters.DoPGOARM64, 'true') }}: - job: Build_Python_PGO_1 displayName: Python PGO build dependsOn: [] workspace: clean: all variables: Platform: ARM64 _HostPython: python strategy: matrix: arm64: Name: arm64 Arch: arm64 ArchDir: arm64 PythonExePattern: python.exe ExtraOptions: ${{ parameters.ExtraOptions }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_arm64 ${{ else }}: Artifact: bin_arm64 ${{ if eq(parameters.DoFreethreaded, 'true') }}: arm64_t: Name: arm64_t Arch: arm64 ArchDir: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'arm64t', 'arm64') }} PythonExePattern: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'python.exe', 'python3*t.exe') }} ExtraOptions: ${{ parameters.ExtraOptionsFreethreaded }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_arm64_t ${{ else }}: Artifact: bin_arm64_t steps: - template: ./build-steps-pgo.yml parameters: PGInstrument: true - job: Build_Python_PGO_2 displayName: Collect PGO profile dependsOn: Build_Python_PGO_1 # Allow up to five hours for PGO run timeoutInMinutes: 300 pool: name: 'Windows ARM64' workspace: clean: all variables: Platform: ARM64 strategy: matrix: arm64: Name: arm64 Arch: arm64 ArchDir: arm64 PythonExePattern: python.exe ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_arm64 ${{ else }}: Artifact: bin_arm64 ${{ if eq(parameters.DoFreethreaded, 'true') }}: arm64_t: Name: arm64_t Arch: arm64 ArchDir: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'arm64t', 'arm64') }} PythonExePattern: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'python.exe', 'python3*t.exe') }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_arm64_t ${{ else }}: Artifact: bin_arm64_t steps: - template: ./build-steps-pgo.yml parameters: PGRun: true - job: Build_Python_PGO_3 displayName: Merge PGO profile dependsOn: Build_Python_PGO_2 workspace: clean: all variables: Platform: ARM64 _HostPython: python strategy: matrix: arm64: Name: arm64 Arch: arm64 ArchDir: arm64 PythonExePattern: python.exe ExtraOptions: ${{ parameters.ExtraOptions }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_arm64 ${{ else }}: Artifact: bin_arm64 ${{ if eq(parameters.DoFreethreaded, 'true') }}: arm64_t: Name: arm64_t Arch: arm64 ArchDir: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'arm64t', 'arm64') }} PythonExePattern: ${{ iif(eq(parameters.Post315OutputDir, 'true'), 'python.exe', 'python3*t.exe') }} ExtraOptions: ${{ parameters.ExtraOptionsFreethreaded }} ${{ if eq(parameters.ToBeSigned, 'true') }}: Artifact: unsigned_arm64_t ${{ else }}: Artifact: bin_arm64_t steps: - template: ./build-steps-pgo.yml parameters: PGUpdate: true - job: TclTk_Lib displayName: Publish Tcl/Tk Library dependsOn: [] variables: IncludeLLVM: false workspace: clean: all steps: - template: ./checkout.yml - script: PCbuild\get_externals.bat --no-openssl --no-libffi displayName: 'Get external dependencies' - task: MSBuild@1 displayName: 'Copy Tcl/Tk lib for publish' inputs: solution: PCbuild\tcltk.props platform: x86 msbuildArguments: /t:CopyTclTkLib /p:OutDir="$(Build.ArtifactStagingDirectory)\tcl_win32" - task: MSBuild@1 displayName: 'Copy Tcl/Tk lib for publish' inputs: solution: PCbuild\tcltk.props platform: x64 msbuildArguments: /t:CopyTclTkLib /p:OutDir="$(Build.ArtifactStagingDirectory)\tcl_amd64" - task: MSBuild@1 displayName: 'Copy Tcl/Tk lib for publish' inputs: solution: PCbuild\tcltk.props platform: ARM64 msbuildArguments: /t:CopyTclTkLib /p:OutDir="$(Build.ArtifactStagingDirectory)\tcl_arm64" - publish: '$(Build.ArtifactStagingDirectory)\tcl_win32' artifact: tcltk_lib_win32 displayName: 'Publish artifact: tcltk_lib_win32' - publish: '$(Build.ArtifactStagingDirectory)\tcl_amd64' artifact: tcltk_lib_amd64 displayName: 'Publish artifact: tcltk_lib_amd64' - publish: '$(Build.ArtifactStagingDirectory)\tcl_arm64' artifact: tcltk_lib_arm64 displayName: 'Publish artifact: tcltk_lib_arm64' ================================================ FILE: windows-release/stage-layout-embed.yml ================================================ parameters: BuildToPackage: current SigningCertificate: '' jobs: - job: Make_Embed_Layout displayName: Make embeddable layout workspace: clean: all variables: PYTHONHOME: $(Build.SourcesDirectory) strategy: matrix: win32: Name: win32 Arch: win32 amd64: Name: amd64 Arch: amd64 arm64: Name: arm64 Arch: arm64 HostArch: amd64 steps: - template: ./checkout.yml - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: bin_$(Name)' inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: bin_$(Name) targetPath: $(Pipeline.Workspace)\bin_$(Name) - template: ./layout-command.yml parameters: BuildToPackage: ${{ parameters.BuildToPackage }} - powershell: | $d = (.\PCbuild\build.bat -V) | %{ if($_ -match '\s+(\w+):\s*(.+)\s*$') { @{$Matches[1] = $Matches[2];} }}; Write-Host "##vso[task.setvariable variable=VersionText]$($d.PythonVersion)" displayName: 'Extract version numbers' - powershell: > $(LayoutCmd) --copy "$(Build.ArtifactStagingDirectory)\layout" --zip "$(Build.ArtifactStagingDirectory)\embed\python-$(VersionText)-embed-$(Name).zip" --preset-embed displayName: 'Generate embeddable layout' - powershell: | git clone $(Build.Repository.Uri) -b $(Build.SourceBranchName) --single-branch --no-checkout "$(Pipeline.Workspace)\release-tools" git -C "$(Pipeline.Workspace)\release-tools" checkout $(Build.SourceVersion) displayName: 'Clone the python/release-tools repository' - powershell: > & "$(Python)" "$(Pipeline.Workspace)\release-tools\sbom.py" "--cpython-source-dir=$(Build.SourcesDirectory)" "$(Build.ArtifactStagingDirectory)\embed\python-$(VersionText)-embed-$(Name).zip" workingDirectory: $(Build.BinariesDirectory) condition: and(succeeded(), not(variables['SkipSBOM'])) displayName: 'Create SBOMs for binaries' - task: CopyFiles@2 displayName: 'Layout Artifact: sbom' inputs: sourceFolder: $(Build.ArtifactStagingDirectory)\embed targetFolder: $(Build.ArtifactStagingDirectory)\sbom flatten: true contents: | **\*.spdx.json - publish: '$(Build.ArtifactStagingDirectory)\layout' artifact: layout_embed_$(Name) displayName: 'Publish Artifact: layout_embed_$(Name)' - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: embed' inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)\embed ArtifactName: embed - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: sbom' condition: and(succeeded(), not(variables['SkipSBOM'])) inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)\sbom ArtifactName: sbom ================================================ FILE: windows-release/stage-layout-full.yml ================================================ parameters: BuildToPackage: current DoFreethreaded: false SigningCertificate: '' jobs: - job: Make_Layouts displayName: Make layouts workspace: clean: all variables: PYTHONHOME: $(Build.SourcesDirectory) strategy: matrix: win32: Name: win32 DebugName: win32_d Arch: win32 TclLibrary: tcltk_lib_win32 ExtraOptions: '' amd64: Name: amd64 DebugName: amd64_d Arch: amd64 TclLibrary: tcltk_lib_amd64 ExtraOptions: '' arm64: Name: arm64 DebugName: arm64_d Arch: arm64 HostArch: amd64 TclLibrary: tcltk_lib_arm64 ExtraOptions: '' ${{ if eq(parameters.DoFreethreaded, 'true') }}: win32_t: Name: win32_t DebugName: win32_td Arch: win32 HostArch: win32 TclLibrary: tcltk_lib_win32 ExtraOptions: --include-freethreaded amd64_t: Name: amd64_t DebugName: amd64_td Arch: amd64 HostArch: amd64 TclLibrary: tcltk_lib_amd64 ExtraOptions: --include-freethreaded arm64_t: Name: arm64_t DebugName: arm64_td Arch: arm64 HostArch: amd64 TclLibrary: tcltk_lib_arm64 ExtraOptions: --include-freethreaded steps: - template: ./checkout.yml - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: bin_$(Name)' inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: bin_$(Name) targetPath: $(Pipeline.Workspace)\bin_$(Name) - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: bin_$(DebugName)' inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: bin_$(DebugName) targetPath: $(Pipeline.Workspace)\bin_$(DebugName) - powershell: | $dest = "$(Pipeline.Workspace)\bin_$(Name)" dir "$(Pipeline.Workspace)\bin_$(DebugName)" | ` ?{ -not (Test-Path "$dest\$($_.Name)") } | ` %{ copy $_.FullName $dest } displayName: 'Copy debug binaries' - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: doc' inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: doc targetPath: $(Pipeline.Workspace)\doc - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: $(TclLibrary)' condition: and(succeeded(), variables['TclLibrary']) inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: $(TclLibrary) targetPath: $(Pipeline.Workspace)\$(TclLibrary) - powershell: | Write-Host "##vso[task.setvariable variable=TCL_LIBRARY]$(Pipeline.Workspace)\$(TclLibrary)\tcl8" displayName: 'Update TCL_LIBRARY' condition: and(succeeded(), variables['TclLibrary']) - powershell: | copy "$(Pipeline.Workspace)\bin_$(Name)\Activate.ps1" Lib\venv\scripts\common\Activate.ps1 -Force -Verbose displayName: 'Copy signed files into sources' - template: ./layout-command.yml parameters: BuildToPackage: ${{ parameters.BuildToPackage }} - powershell: | $(LayoutCmd) --copy "$(Build.ArtifactStagingDirectory)\layout" --preset-default $(ExtraOptions) displayName: 'Generate full layout' - publish: '$(Build.ArtifactStagingDirectory)\layout' artifact: layout_full_$(Name) displayName: 'Publish Artifact: layout_full_$(Name)' ================================================ FILE: windows-release/stage-layout-msix.yml ================================================ parameters: BuildToPackage: current SigningCertificate: '' jobs: - job: Make_MSIX_Layout displayName: Make MSIX layout workspace: clean: all variables: PYTHONHOME: $(Build.SourcesDirectory) strategy: matrix: #win32: # Name: win32 # TclLibrary: tcltk_lib_win32 # ExtraOptions: --precompile amd64: Name: amd64 Arch: amd64 TclLibrary: tcltk_lib_amd64 ExtraOptions: --precompile arm64: Name: arm64 Arch: arm64 HostArch: amd64 TclLibrary: tcltk_lib_arm64 ExtraOptions: --precompile steps: - template: ./checkout.yml - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: bin_$(Name)' inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: bin_$(Name) targetPath: $(Pipeline.Workspace)\bin_$(Name) - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: $(TclLibrary)' condition: and(succeeded(), variables['TclLibrary']) inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: $(TclLibrary) targetPath: $(Pipeline.Workspace)\$(TclLibrary) - powershell: | Write-Host "##vso[task.setvariable variable=TCL_LIBRARY]$(Pipeline.Workspace)\$(TclLibrary)\tcl8" displayName: 'Update TCL_LIBRARY' condition: and(succeeded(), variables['TclLibrary']) - ${{ if parameters.SigningCertificate }}: - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: cert' inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: cert targetPath: $(Pipeline.Workspace)\cert - powershell: | copy "$(Pipeline.Workspace)\bin_$(Name)\Activate.ps1" Lib\venv\scripts\common\Activate.ps1 -Force -Verbose displayName: 'Copy signed files into sources' - template: ./layout-command.yml parameters: BuildToPackage: ${{ parameters.BuildToPackage }} - powershell: | Remove-Item "$(Build.ArtifactStagingDirectory)\appx-store" -Recurse -Force -EA 0 $(LayoutCmd) --copy "$(Build.ArtifactStagingDirectory)\appx-store" --preset-appx $(ExtraOptions) displayName: 'Generate store APPX layout' env: TCL_LIBRARY: $(TclLibrary) - ${{ if parameters.SigningCertificate }}: # The dotnet sign tool shouldn't need this, but we do because of the sccd file - powershell: | $info = (gc "$(Pipeline.Workspace)\cert\certinfo.json" | ConvertFrom-JSON) Write-Host "Side-loadable APPX must be signed with '$($info.Subject)'" Write-Host "##vso[task.setvariable variable=APPX_DATA_PUBLISHER]$($info.Subject)" Write-Host "##vso[task.setvariable variable=APPX_DATA_SHA256]$($info.SHA256)" displayName: 'Override signing parameters' - powershell: | Remove-Item "$(Build.ArtifactStagingDirectory)\appx" -Recurse -Force -EA 0 $(LayoutCmd) --copy "$(Build.ArtifactStagingDirectory)\appx" --preset-appx --include-symbols --include-tests $(ExtraOptions) displayName: 'Generate sideloading APPX layout' env: TCL_LIBRARY: $(TclLibrary) - publish: '$(Build.ArtifactStagingDirectory)\appx-store' artifact: layout_appxstore_$(Name) displayName: 'Publish Artifact: layout_appxstore_$(Name)' - publish: '$(Build.ArtifactStagingDirectory)\appx' artifact: layout_appx_$(Name) displayName: 'Publish Artifact: layout_appx_$(Name)' ================================================ FILE: windows-release/stage-layout-nuget.yml ================================================ parameters: BuildToPackage: current DoFreethreaded: false SigningCertificate: '' jobs: - job: Make_Nuget_Layout displayName: Make Nuget layout workspace: clean: all variables: PYTHONHOME: $(Build.SourcesDirectory) strategy: matrix: win32: Name: win32 Arch: win32 ExtraOptions: '' amd64: Name: amd64 Arch: amd64 ExtraOptions: '' arm64: Name: arm64 Arch: arm64 HostArch: amd64 ExtraOptions: '' ${{ if eq(parameters.DoFreethreaded, 'true') }}: win32_t: Name: win32_t Arch: win32 HostArch: win32 ExtraOptions: --include-freethreaded amd64_t: Name: amd64_t Arch: amd64 HostArch: amd64 ExtraOptions: --include-freethreaded arm64_t: Name: arm64_t Arch: arm64 HostArch: amd64 ExtraOptions: --include-freethreaded steps: - template: ./checkout.yml - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: bin_$(Name)' inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: bin_$(Name) targetPath: $(Pipeline.Workspace)\bin_$(Name) - powershell: | copy $(Pipeline.Workspace)\bin_$(Name)\Activate.ps1 Lib\venv\scripts\common\Activate.ps1 -Force -Verbose displayName: 'Copy signed files into sources' - template: ./layout-command.yml parameters: BuildToPackage: ${{ parameters.BuildToPackage }} - powershell: | $(LayoutCmd) --copy "$(Build.ArtifactStagingDirectory)\nuget" --preset-nuget $(ExtraOptions) displayName: 'Generate nuget layout' - publish: '$(Build.ArtifactStagingDirectory)\nuget' artifact: layout_nuget_$(Name) displayName: 'Publish Artifact: layout_nuget_$(Name)' ================================================ FILE: windows-release/stage-layout-pymanager.yml ================================================ parameters: BuildToPackage: current DoFreethreaded: false DoEmbed: false LayoutScriptBranch: main SigningCertificate: '' jobs: - job: Make_PyManager_Layouts displayName: Make PyManager layouts workspace: clean: all variables: PYTHONHOME: $(Build.SourcesDirectory)\cpython strategy: matrix: win32: Name: win32 BinArtifact: bin_win32 Arch: win32 TclLibrary: tcltk_lib_win32 LayoutOptions: '--preset-pymanager' IncludeDoc: true amd64: Name: amd64 BinArtifact: bin_amd64 Arch: amd64 TclLibrary: tcltk_lib_amd64 LayoutOptions: '--preset-pymanager' IncludeDoc: true arm64: Name: arm64 BinArtifact: bin_arm64 Arch: arm64 HostArch: amd64 TclLibrary: tcltk_lib_arm64 LayoutOptions: '--preset-pymanager' IncludeDoc: true win32_test: Name: win32_test BinArtifact: bin_win32 Arch: win32 TclLibrary: tcltk_lib_win32 LayoutOptions: '--preset-pymanager-test' IncludeDoc: true amd64_test: Name: amd64_test BinArtifact: bin_amd64 Arch: amd64 TclLibrary: tcltk_lib_amd64 LayoutOptions: '--preset-pymanager-test' IncludeDoc: true arm64_test: Name: arm64_test BinArtifact: bin_arm64 Arch: arm64 HostArch: amd64 TclLibrary: tcltk_lib_arm64 LayoutOptions: '--preset-pymanager-test' IncludeDoc: true ${{ if eq(parameters.DoFreethreaded, 'true') }}: win32_t: Name: win32_t BinArtifact: bin_win32_t Arch: win32 HostArch: win32 TclLibrary: tcltk_lib_win32 LayoutOptions: '--preset-pymanager --include-freethreaded' IncludeDoc: true amd64_t: Name: amd64_t BinArtifact: bin_amd64_t Arch: amd64 HostArch: amd64 TclLibrary: tcltk_lib_amd64 LayoutOptions: '--preset-pymanager --include-freethreaded' IncludeDoc: true arm64_t: Name: arm64_t BinArtifact: bin_arm64_t Arch: arm64 HostArch: amd64 TclLibrary: tcltk_lib_arm64 LayoutOptions: '--preset-pymanager --include-freethreaded' IncludeDoc: true ${{ if eq(parameters.DoEmbed, 'true') }}: win32_embed: Name: win32_embed BinArtifact: bin_win32 Arch: win32 HostArch: win32 LayoutOptions: '--preset-embed --include-install-embed-json' amd64_embed: Name: amd64_embed BinArtifact: bin_amd64 Arch: amd64 HostArch: amd64 LayoutOptions: '--preset-embed --include-install-embed-json' arm64_embed: Name: arm64_embed BinArtifact: bin_arm64 Arch: arm64 HostArch: amd64 LayoutOptions: '--preset-embed --include-install-embed-json' steps: - template: ./checkout.yml parameters: IncludeSelf: true Path: $(Build.SourcesDirectory)\cpython - ${{ if ne(parameters.BuildToPackage, 'current') }}: - powershell: > git clone --progress -v --depth 1 --branch ${{ parameters.LayoutScriptBranch }} --single-branch https://github.com/$(GitRemote)/cpython.git "$(Build.SourcesDirectory)\layout-script" displayName: 'Clone PC/layout script from main' - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: $(BinArtifact)' inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: $(BinArtifact) targetPath: $(Pipeline.Workspace)\$(BinArtifact) - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: doc' condition: and(succeeded(), variables['IncludeDoc']) inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: doc targetPath: $(Pipeline.Workspace)\doc - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: $(TclLibrary)' condition: and(succeeded(), variables['TclLibrary']) inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: $(TclLibrary) targetPath: $(Pipeline.Workspace)\$(TclLibrary) - powershell: | Write-Host "##vso[task.setvariable variable=TCL_LIBRARY]$(Pipeline.Workspace)\$(TclLibrary)\tcl8" displayName: 'Update TCL_LIBRARY' condition: and(succeeded(), variables['TclLibrary']) - powershell: | copy "$(Pipeline.Workspace)\$(BinArtifact)\Activate.ps1" Lib\venv\scripts\common\Activate.ps1 -Force -Verbose displayName: 'Copy signed files into sources' workingDirectory: $(Build.SourcesDirectory)\cpython - template: ./layout-command.yml parameters: Binaries: $(Pipeline.Workspace)\$(BinArtifact) Sources: $(Build.SourcesDirectory)\cpython Docs: $(Pipeline.Workspace)\doc BuildToPackage: ${{ parameters.BuildToPackage }} ${{ if ne(parameters.BuildToPackage, 'current') }}: LayoutSources: $(Build.SourcesDirectory)\layout-script - powershell: > $(LayoutCmd) --copy "$(Build.ArtifactStagingDirectory)\layout" --zip "$(Build.ArtifactStagingDirectory)\zip\package.zip" $(LayoutOptions) displayName: 'Generate PyManager layout' - powershell: | # ConvertFrom-Json can't handle empty keys, but we don't need them anyway. # Replace with an underscore so it can load. $install = (gc -raw "layout\__install__.json") -replace '"":', '"_":' | ConvertFrom-Json # Bring a copy of the install data separate from the ZIP copy "layout\__install__.json" "zip\__install__.json" # Rename the ZIP to match the target filename # (which we didn't know when we named it package.zip) $filename = Split-Path -Leaf $install.url move "zip\package.zip" "zip\$filename" displayName: 'Prepare PyManager distribution files' workingDirectory: $(Build.ArtifactStagingDirectory) - powershell: > & $(Python) "$(Pipeline.Workspace)\release-tools\sbom.py" "--cpython-source-dir=$(Build.SourcesDirectory)\cpython" $(gci "zip\*.zip") workingDirectory: $(Build.ArtifactStagingDirectory) condition: and(succeeded(), not(variables['SkipSBOM'])) displayName: 'Create SBOMs for package' - publish: '$(Build.ArtifactStagingDirectory)\layout' artifact: layout_pymanager_$(Name) displayName: 'Publish Artifact: layout_pymanager_$(Name)' - publish: '$(Build.ArtifactStagingDirectory)\zip' artifact: pymanager_$(Name) displayName: 'Publish Artifact: pymanager_$(Name)' ================================================ FILE: windows-release/stage-layout-symbols.yml ================================================ parameters: BuildToPackage: current DoFreethreaded: false Packages: - win32 - amd64 - arm64 - win32_d - amd64_d - arm64_d PackagesFreethreaded: - win32_t - amd64_t - arm64_t - win32_td - amd64_td - arm64_td SigningCertificate: '' jobs: - job: Layout_Symbols displayName: Make symbols layout workspace: clean: all steps: - checkout: none - ${{ each p in parameters.Packages }}: - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: bin_${{ p }}' inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: bin_${{ p }} itemPattern: '**\*.pdb' targetPath: $(Build.ArtifactStagingDirectory)\${{ p }} - ${{ if eq(parameters.DoFreethreaded, 'true') }}: - ${{ each p in parameters.PackagesFreethreaded }}: - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: bin_${{ p }}' inputs: ${{ if eq(parameters.BuildToPackage, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_package.projectId) pipeline: $(resources.pipeline.build_to_package.pipelineId) runId: $(resources.pipeline.build_to_package.runID) artifact: bin_${{ p }} itemPattern: '**\*.pdb' targetPath: $(Build.ArtifactStagingDirectory)\${{ p }} - publish: $(Build.ArtifactStagingDirectory) artifact: symbols displayName: 'Publish Artifact: symbols' ================================================ FILE: windows-release/stage-msi.yml ================================================ parameters: BuildToPackage: current DoARM64: true DoFreethreaded: false SigningCertificate: '' Post315OutputDir: false jobs: - job: Make_MSI displayName: Make MSI variables: - ${{ if eq(parameters.SigningCertificate, 'PythonSoftwareFoundation') }}: - group: CPythonSign - ${{ if eq(parameters.SigningCertificate, 'TestSign') }}: - group: CPythonTestSign - name: ReleaseUri value: http://www.python.org/{arch} - name: DownloadUrl value: https://www.python.org/ftp/python/{version}/{arch}{releasename}/{msi} - name: Py_OutDir value: $(Build.BinariesDirectory) workspace: clean: all steps: - template: msi-steps.yml parameters: BuildToPackage: ${{ parameters.BuildToPackage }} DoFreethreaded: ${{ parameters.DoFreethreaded }} SigningCertificate: ${{ parameters.SigningCertificate }} Artifacts: - artifact: doc target: $(Build.SourcesDirectory)\Doc\build - artifact: bin_win32 target: $(Build.BinariesDirectory)\win32 - artifact: bin_win32_d target: $(Build.BinariesDirectory)\win32 - artifact: tcltk_lib_win32 - artifact: bin_amd64 target: $(Build.BinariesDirectory)\amd64 - artifact: bin_amd64_d target: $(Build.BinariesDirectory)\amd64 - artifact: tcltk_lib_amd64 - ${{ if eq(parameters.DoARM64, 'true') }}: - artifact: bin_arm64 target: $(Build.BinariesDirectory)\arm64 - artifact: bin_arm64_d target: $(Build.BinariesDirectory)\arm64 - artifact: tcltk_lib_arm64 # Freethreaded binaries copy into the same target directory, but files # are not overwritten. - ${{ if eq(parameters.DoFreethreaded, 'true') }}: - ${{ if eq(parameters.Post315OutputDir, 'true') }}: - artifact: bin_win32_t target: $(Build.BinariesDirectory)\win32t - artifact: bin_win32_td target: $(Build.BinariesDirectory)\win32t - artifact: bin_amd64_t target: $(Build.BinariesDirectory)\amd64t - artifact: bin_amd64_td target: $(Build.BinariesDirectory)\amd64t - ${{ if eq(parameters.DoARM64, 'true') }}: - artifact: bin_arm64_t target: $(Build.BinariesDirectory)\arm64t - artifact: bin_arm64_td target: $(Build.BinariesDirectory)\arm64t - ${{ else }}: - artifact: bin_win32_t target: $(Build.BinariesDirectory)\win32 - artifact: bin_win32_td target: $(Build.BinariesDirectory)\win32 - artifact: bin_amd64_t target: $(Build.BinariesDirectory)\amd64 - artifact: bin_amd64_td target: $(Build.BinariesDirectory)\amd64 - ${{ if eq(parameters.DoARM64, 'true') }}: - artifact: bin_arm64_t target: $(Build.BinariesDirectory)\arm64 - artifact: bin_arm64_td target: $(Build.BinariesDirectory)\arm64 Bundles: - bundle: win32 Platform: x86 PythonForBuild: $(Build.BinariesDirectory)\win32 TclTkArtifact: tcltk_lib_win32 - bundle: amd64 Platform: x64 PythonForBuild: $(Build.BinariesDirectory)\amd64 TclTkArtifact: tcltk_lib_amd64 - ${{ if eq(parameters.DoARM64, 'true') }}: - bundle: arm64 Platform: ARM64 PythonForBuild: $(Build.BinariesDirectory)\win32 TclTkArtifact: tcltk_lib_arm64 ================================================ FILE: windows-release/stage-pack-msix.yml ================================================ parameters: SigningCertificate: '' jobs: - job: Pack_MSIX displayName: Pack MSIX bundles workspace: clean: all variables: SigningCertificate: ${{ parameters.SigningCertificate }} strategy: matrix: amd64: Name: amd64 Artifact: appx Suffix: ShouldSign: true amd64_store: Name: amd64 Artifact: appxstore Suffix: -store CreateMsixUpload: true arm64: Name: arm64 Artifact: appx Suffix: ShouldSign: true arm64_store: Name: arm64 Artifact: appxstore Suffix: -store CreateMsixUpload: true steps: - template: ./checkout.yml - download: current artifact: layout_$(Artifact)_$(Name) displayName: 'Download artifact: layout_$(Artifact)_$(Name)' - download: current artifact: symbols patterns: $(Name)\* displayName: 'Download artifact: symbols' - powershell: | $d = (.\PCbuild\build.bat -V) | %{ if($_ -match '\s+(\w+):\s*(.+)\s*$') { @{$Matches[1] = $Matches[2];} }}; Write-Host "##vso[task.setvariable variable=VersionText]$($d.PythonVersion)" Write-Host "##vso[task.setvariable variable=VersionNumber]$($d.PythonVersionNumber)" Write-Host "##vso[task.setvariable variable=VersionHex]$($d.PythonVersionHex)" Write-Host "##vso[task.setvariable variable=VersionUnique]$($d.PythonVersionUnique)" Write-Host "##vso[task.setvariable variable=Filename]python-$($d.PythonVersion)-$(Name)$(Suffix)" displayName: 'Extract version numbers' - powershell: | ./Tools/msi/make_appx.ps1 -layout "$(Pipeline.Workspace)\layout_$(Artifact)_$(Name)" -msix "$(Build.ArtifactStagingDirectory)\msix\$(Filename).msix" displayName: 'Build msix' - powershell: | 7z a -tzip "$(Build.ArtifactStagingDirectory)\msix\$(Filename).appxsym" *.pdb displayName: 'Build appxsym' workingDirectory: $(Pipeline.Workspace)\symbols\$(Name) - powershell: | 7z a -tzip "$(Build.ArtifactStagingDirectory)\msixupload\$(Filename).msixupload" * displayName: 'Build msixupload' condition: and(succeeded(), eq(variables['CreateMsixUpload'], 'true')) workingDirectory: $(Build.ArtifactStagingDirectory)\msix - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: MSIX' inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)\msix' ${{ if parameters.SigningCertificate }}: ArtifactName: unsigned_msix ${{ else }}: ArtifactName: msix - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: MSIXUpload' condition: and(succeeded(), eq(variables['CreateMsixUpload'], 'true')) inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)\msixupload' ArtifactName: msixupload - ${{ if parameters.SigningCertificate }}: - job: Sign_MSIX displayName: Sign side-loadable MSIX bundles dependsOn: - Pack_MSIX workspace: clean: all variables: - ${{ if eq(parameters.SigningCertificate, 'PythonSoftwareFoundation') }}: - group: CPythonSign - ${{ if eq(parameters.SigningCertificate, 'TestSign') }}: - group: CPythonTestSign steps: - template: ./checkout.yml - powershell: | $d = (.\PCbuild\build.bat -V) | %{ if($_ -match '\s+(\w+):\s*(.+)\s*$') { @{$Matches[1] = $Matches[2];} }}; Write-Host "##vso[task.setvariable variable=SigningDescription]Python $($d.PythonVersion)" displayName: 'Update signing description' condition: and(succeeded(), not(variables['SigningDescription'])) - task: DownloadBuildArtifacts@0 displayName: 'Download Artifact: unsigned_msix' inputs: artifactName: unsigned_msix downloadPath: $(Build.BinariesDirectory) # Getting "Error: SignerSign() failed." (-2147024885/0x8007000b)"? # It may be that the certificate info collected in stage-sign.yml is wrong. Check that # you do not have multiple matches for the certificate name you have specified. - template: sign-files.yml parameters: Include: '*.msix' # Additional filter to avoid recursively signing package contents Filter: '*.msix' WorkingDir: $(Build.BinariesDirectory)\unsigned_msix SigningCertificate: ${{ parameters.SigningCertificate }} - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: MSIX' inputs: PathtoPublish: '$(Build.BinariesDirectory)\unsigned_msix' ArtifactName: msix ================================================ FILE: windows-release/stage-pack-nuget.yml ================================================ parameters: DoFreethreaded: false SigningCertificate: '' jobs: - job: Pack_Nuget displayName: Pack Nuget bundles workspace: clean: all strategy: matrix: amd64: Name: amd64 win32: Name: win32 arm64: Name: arm64 ${{ if eq(parameters.DoFreethreaded, 'true') }}: amd64_t: Name: amd64_t win32_t: Name: win32_t arm64_t: Name: arm64_t variables: - ${{ if eq(parameters.SigningCertificate, 'PythonSoftwareFoundation') }}: - group: CPythonSign - ${{ if eq(parameters.SigningCertificate, 'TestSign') }}: - group: CPythonTestSign steps: - checkout: none - download: current artifact: layout_nuget_$(Name) displayName: 'Download artifact: layout_nuget_$(Name)' - task: NugetToolInstaller@0 displayName: 'Install Nuget' inputs: versionSpec: '>=5.0' - powershell: > nuget pack "$(Pipeline.Workspace)\layout_nuget_$(Name)\python.nuspec" -OutputDirectory $(Build.ArtifactStagingDirectory) -NoPackageAnalysis -NonInteractive condition: and(succeeded(), not(variables['OverrideNugetVersion'])) displayName: 'Create nuget package' - powershell: > nuget pack "$(Pipeline.Workspace)\layout_nuget_$(Name)\python.nuspec" -OutputDirectory $(Build.ArtifactStagingDirectory) -NoPackageAnalysis -NonInteractive -Version "$(OverrideNugetVersion)" condition: and(succeeded(), variables['OverrideNugetVersion']) displayName: 'Create nuget package' - template: sign-files.yml parameters: Include: '*.nupkg' # Additional filter to avoid recursively signing package contents Filter: '*.nupkg' WorkingDir: $(Build.ArtifactStagingDirectory) SigningCertificate: ${{ parameters.SigningCertificate }} # Nuget signing is not supported by our test certificate, so ignore errors ${{ if eq(parameters.SigningCertificate, 'TestSign') }}: ContinueOnError: true - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: nuget' inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)' ArtifactName: nuget ================================================ FILE: windows-release/stage-pack-pymanager.yml ================================================ parameters: DoFreethreaded: false DoEmbed: false SigningCertificate: '' Artifacts: - name: win32 - name: amd64 - name: arm64 - name: win32_test - name: amd64_test - name: arm64_test - name: win32_t freethreaded: true - name: amd64_t freethreaded: true - name: arm64_t freethreaded: true - name: win32_embed embed: true - name: amd64_embed embed: true - name: arm64_embed embed: true jobs: - job: Pack_PyManager displayName: Pack PyManager bundle workspace: clean: all steps: - task: UsePythonVersion@0 displayName: 'Use Python 3.10 or later' inputs: versionSpec: '>=3.10' - ${{ each a in parameters.artifacts }}: - ${{ if and(or(not(a.freethreaded), eq(parameters.DoFreethreaded, 'true')), or(not(a.embed), eq(parameters.DoEmbed, 'true'))) }}: - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: pymanager_${{ a.name }}' inputs: buildType: current artifact: pymanager_${{ a.name }} targetPath: $(Build.BinariesDirectory)\${{ a.name }} - powershell: | cp *.zip (mkdir $env:TARGET -Force) cp __install__.json "${env:TARGET}\__install__.${{ a.name }}.json" displayName: 'Relocate ${{ a.name }}' workingDirectory: $(Build.BinariesDirectory)\${{ a.name }} env: TARGET: $(Build.ArtifactStagingDirectory) - powershell: | "Bundling the following packages:" (dir __install__.*.json).FullName python "$(Build.SourcesDirectory)\windows-release\merge-and-upload.py" del __install__.*.json workingDirectory: $(Build.ArtifactStagingDirectory) displayName: 'Generate local index' env: LOCAL_INDEX: 1 NO_UPLOAD: 1 INDEX_FILE: 'index.json' - publish: '$(Build.ArtifactStagingDirectory)' artifact: pymanager_bundle displayName: 'Publish Artifact: pymanager_bundle' ================================================ FILE: windows-release/stage-publish-nugetorg.yml ================================================ parameters: BuildToPublish: current jobs: - job: Publish_Nuget displayName: Publish Nuget packages condition: and(succeeded(), ne(variables['SkipNugetPublish'], 'true')) workspace: clean: all steps: - checkout: none - task: DownloadBuildArtifacts@1 displayName: 'Download artifact: nuget' inputs: ${{ if eq(parameters.BuildToPublish, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_publish.projectId) pipeline: $(resources.pipeline.build_to_publish.pipelineId) runId: $(resources.pipeline.build_to_publish.runID) artifactName: nuget downloadPath: $(Build.BinariesDirectory) - powershell: 'gci pythonarm*.nupkg | %{ Write-Host "Not publishing: $($_.Name)"; gi $_ } | del' displayName: 'Prevent publishing ARM64 packages' workingDirectory: '$(Build.BinariesDirectory)\nuget' condition: and(succeeded(), ne(variables['PublishARM64'], 'true')) - task: NuGetCommand@2 displayName: Push packages condition: and(succeeded(), eq(variables['IsRealSigned'], 'true')) inputs: command: push packagesToPush: '$(Build.BinariesDirectory)\nuget\*.nupkg' nuGetFeedType: external publishFeedCredentials: 'Python on Nuget' ================================================ FILE: windows-release/stage-publish-pymanager.yml ================================================ parameters: BuildToPublish: current DoFreethreaded: false DoEmbed: false HashAlgorithms: ['SHA256'] SigningCertificate: '' Artifacts: - name: win32 - name: amd64 - name: arm64 - name: win32_test - name: amd64_test - name: arm64_test - name: win32_t freethreaded: true - name: amd64_t freethreaded: true - name: arm64_t freethreaded: true - name: win32_embed embed: true - name: amd64_embed embed: true - name: arm64_embed embed: true jobs: - job: Publish_PyManager displayName: Publish PyManager packages to python.org condition: and(succeeded(), ne(variables['SkipPythonOrgPublish'], 'true')) variables: - group: PythonOrgPublish - ${{ if eq(parameters.SigningCertificate, 'PythonSoftwareFoundation') }}: - group: CPythonSign - ${{ if eq(parameters.SigningCertificate, 'TestSign') }}: - group: CPythonTestSign # Override the SigningDescription here, since we're only signing the feed # and not the actual binaries. - name: SigningDescription value: "Python $(Build.BuildNumber)" workspace: clean: all steps: - task: UsePythonVersion@0 displayName: 'Use Python 3.10 or later' inputs: versionSpec: '>=3.10' - ${{ each a in parameters.artifacts }}: - ${{ if and(or(not(a.freethreaded), eq(parameters.DoFreethreaded, 'true')), or(not(a.embed), eq(parameters.DoEmbed, 'true'))) }}: - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: pymanager_${{ a.name }}' inputs: ${{ if eq(parameters.BuildToPublish, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_publish.projectId) pipeline: $(resources.pipeline.build_to_publish.pipelineId) runId: $(resources.pipeline.build_to_publish.runID) artifact: pymanager_${{ a.name }} targetPath: $(Build.BinariesDirectory)\${{ a.name }} - ${{ if eq(parameters.SigningCertificate, 'PythonSoftwareFoundation') }}: - task: DownloadSecureFile@1 name: sshkey inputs: secureFile: pydotorg-ssh.ppk displayName: 'Download PuTTY key' - powershell: | git clone https://github.com/python/cpython-bin-deps --revision 9f9e6fc31a55406ee5ff0198ea47bbb445eeb942 --depth 1 --progress -v "putty" "##vso[task.prependpath]$(gi putty)" workingDirectory: $(Pipeline.Workspace) displayName: 'Download PuTTY binaries' # Use the template just to configure the signing tool. # This will set MAKECAT and SIGN_COMMAND to be injected into later build steps - template: sign-files.yml parameters: Include: "" InstallTool: false InstallLegacyTool: true ExportLegacyCommand: SIGN_COMMAND SigningCertificate: ${{ parameters.SigningCertificate }} - powershell: | if ($env:FILENAME) { "##vso[task.setvariable variable=_PyManagerIndexFilename]${env:FILENAME}" "Updating index named '${env:FILENAME}'" } else { "##vso[task.setvariable variable=_PyManagerIndexFilename]index-windows.json" "Updating index named 'index-windows.json'" } env: FILENAME: $(PyManagerIndexFilename) displayName: 'Infer index filename' - ${{ if ne(parameters.SigningCertificate, 'PythonSoftwareFoundation') }}: - powershell: | "Preparing following packages:" (dir "__install__.*.json").FullName (dir "*\__install__.json").FullName python "$(Build.SourcesDirectory)\windows-release\merge-and-upload.py" workingDirectory: $(Build.BinariesDirectory) displayName: 'Produce uploadable ZIPs (no upload)' env: NO_UPLOAD: 1 INDEX_URL: '$(PyDotOrgUrlPrefix)python/$(_PyManagerIndexFilename)' INDEX_FILE: '$(Build.ArtifactStagingDirectory)\index\$(_PyManagerIndexFilename)' MANIFEST_FILE: '$(Build.ArtifactStagingDirectory)\index\windows.json' - ${{ else }}: - powershell: | "Uploading following packages:" (dir "__install__.*.json").FullName (dir "*\__install__.json").FullName python "$(Build.SourcesDirectory)\windows-release\merge-and-upload.py" workingDirectory: $(Build.BinariesDirectory) displayName: 'Upload ZIPs' env: INDEX_URL: '$(PyDotOrgUrlPrefix)python/$(_PyManagerIndexFilename)' INDEX_FILE: '$(Build.ArtifactStagingDirectory)\index\$(_PyManagerIndexFilename)' MANIFEST_FILE: '$(Build.ArtifactStagingDirectory)\index\windows.json' UPLOAD_URL_PREFIX: $(PyDotOrgUrlPrefix) UPLOAD_PATH_PREFIX: $(PyDotOrgUploadPathPrefix) UPLOAD_HOST: $(PyDotOrgServer) UPLOAD_HOST_KEY: $(PyDotOrgHostKey) UPLOAD_USER: $(PyDotOrgUsername) UPLOAD_KEYFILE: $(sshkey.secureFilePath) - ${{ each alg in parameters.HashAlgorithms }}: - powershell: | $files = (dir "*\__install__.json").Directory | %{ dir -File "$_\*.zip" } $files = $files, (dir -File "${env:INDEX_DIR}\*.json") $hashes = $files | ` Sort-Object Name | ` Format-Table Name, @{ Label="${{ alg }}"; Expression={(Get-FileHash $_ -Algorithm ${{ alg }}).Hash} }, Length -AutoSize | ` Out-String -Width 4096 $d = mkdir "$(Build.ArtifactStagingDirectory)\hashes" -Force $hashes | Out-File "$d\hashes.txt" -Encoding ascii -Append $hashes workingDirectory: $(Build.BinariesDirectory) displayName: 'Generate hashes (${{ alg }})' env: INDEX_DIR: '$(Build.ArtifactStagingDirectory)\index' - publish: '$(Build.ArtifactStagingDirectory)\index' artifact: pymanager_index - publish: '$(Build.ArtifactStagingDirectory)\hashes' artifact: pymanager_hashes displayName: 'Publish Artifact: hashes' ================================================ FILE: windows-release/stage-publish-pythonorg.yml ================================================ parameters: BuildToPublish: current DoEmbed: true IncludeGPG: false HashAlgorithms: ['SHA256', 'MD5'] jobs: - job: Publish_Python displayName: Publish python.org packages condition: and(succeeded(), ne(variables['SkipPythonOrgPublish'], 'true')) variables: - group: PythonOrgPublish workspace: clean: all steps: - task: UsePythonVersion@0 displayName: 'Use Python 3.10 or later' inputs: versionSpec: '>=3.10' - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: msi' inputs: ${{ if eq(parameters.BuildToPublish, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_publish.projectId) pipeline: $(resources.pipeline.build_to_publish.pipelineId) runId: $(resources.pipeline.build_to_publish.runID) artifact: msi targetPath: $(Pipeline.Workspace)\msi - ${{ if eq(parameters.DoEmbed, 'true') }}: - task: DownloadBuildArtifacts@1 displayName: 'Download artifact: embed' inputs: ${{ if eq(parameters.BuildToPublish, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_publish.projectId) pipeline: $(resources.pipeline.build_to_publish.pipelineId) buildId: $(resources.pipeline.build_to_publish.runID) artifactName: embed # Artifact name is added to path for DownloadBuildArtifacts downloadPath: $(Pipeline.Workspace) - task: DownloadBuildArtifacts@1 displayName: 'Download artifact: sbom' inputs: ${{ if eq(parameters.BuildToPublish, 'current') }}: buildType: current ${{ else }}: buildType: specific buildVersionToDownload: specific project: $(resources.pipeline.build_to_publish.projectId) pipeline: $(resources.pipeline.build_to_publish.pipelineId) buildId: $(resources.pipeline.build_to_publish.runID) artifactName: sbom # Artifact name is added to path for DownloadBuildArtifacts downloadPath: $(Pipeline.Workspace) - ${{ if eq(parameters.DoEmbed, 'true') }}: # Note that ARM64 MSIs are skipped at build when this option is specified - powershell: 'gci *embed-arm*.zip | %{ Write-Host "Not publishing: $($_.Name)"; gi $_ } | del' displayName: 'Prevent publishing ARM64 packages' workingDirectory: '$(Pipeline.Workspace)\embed' condition: and(succeeded(), ne(variables['PublishARM64'], 'true')) - ${{ if eq(parameters.IncludeGPG, 'true') }}: - task: DownloadSecureFile@1 name: gpgkey inputs: secureFile: 'python-signing.key' displayName: 'Download GPG key' - powershell: | git clone https://github.com/python/cpython-bin-deps --branch gpg --single-branch --depth 1 --progress -v "gpg" gpg/gpg2.exe --import "$(gpgkey.secureFilePath)" $files = gci -File "msi\*\*", "embed\*.zip" -EA SilentlyContinue $files.FullName | %{ gpg/gpg2.exe -ba --batch --passphrase $(GPGPassphrase) $_ "Made signature for $_" } displayName: 'Generate GPG signatures' workingDirectory: $(Pipeline.Workspace) - powershell: | $p = gps "gpg-agent" -EA 0 if ($p) { $p.Kill() } displayName: 'Kill GPG agent' condition: true - task: DownloadSecureFile@1 name: sshkey inputs: secureFile: pydotorg-ssh.ppk displayName: 'Download PuTTY key' - powershell: | git clone https://github.com/python/cpython-bin-deps --branch putty --single-branch --depth 1 --progress -v "putty" "##vso[task.prependpath]$(gi putty)" workingDirectory: $(Pipeline.Workspace) displayName: 'Download PuTTY binaries' - powershell: > $(Build.SourcesDirectory)\windows-release\uploadrelease.ps1 -build msi -user $(PyDotOrgUsername) -server $(PyDotOrgServer) -hostkey $(PyDotOrgHostKey) -keyfile "$(sshkey.secureFilePath)" -embed embed -sbom sbom workingDirectory: $(Pipeline.Workspace) condition: and(succeeded(), eq(variables['IsRealSigned'], 'true')) displayName: 'Upload files to python.org' - powershell: > python "$(Build.SourcesDirectory)\windows-release\purge.py" (gci msi\*\python-*.exe | %{ $_.Name -replace 'python-(.+?)(-|\.exe).+', '$1' } | select -First 1) workingDirectory: $(Pipeline.Workspace) condition: and(succeeded(), eq(variables['IsRealSigned'], 'true')) displayName: 'Purge CDN' - powershell: | $failures = 0 gci "msi\*\*.exe" -File | %{ $d = mkdir "tests\$($_.BaseName)" -Force gci $d -r -File | del $ic = copy $_ $d -PassThru "Checking layout for $($ic.Name)" Start-Process -wait $ic "/passive", "/layout", "$d\layout", "/log", "$d\log\install.log" if (-not $?) { Write-Error "Failed to validate layout of $($inst.Name)" $failures += 1 } } if ($failures) { Write-Error "Failed to validate $failures installers" exit 1 } workingDirectory: $(Pipeline.Workspace) condition: and(succeeded(), eq(variables['IsRealSigned'], 'true')) displayName: 'Test layouts' - ${{ each alg in parameters.HashAlgorithms }}: - powershell: | $files = gci -File "msi\*\*.exe", "embed\*.zip" -EA SilentlyContinue $hashes = $files | ` Sort-Object Name | ` Format-Table Name, @{ Label="${{ alg }}"; Expression={(Get-FileHash $_ -Algorithm ${{ alg }}).Hash} }, Length -AutoSize | ` Out-String -Width 4096 $d = mkdir "$(Build.ArtifactStagingDirectory)\hashes" -Force $hashes | Out-File "$d\hashes.txt" -Encoding ascii -Append $hashes workingDirectory: $(Pipeline.Workspace) displayName: 'Generate hashes (${{ alg }})' - ${{ if eq(parameters.IncludeGPG, 'true') }}: - powershell: | "Copying:" $files = gci -File "msi\*\python*.asc", "embed\*.asc" -EA SilentlyContinue $files.FullName $d = mkdir "$(Build.ArtifactStagingDirectory)\hashes" -Force move $files $d -Force gci msi -Directory | %{ move "msi\$_\*.asc" (mkdir "$d\$_" -Force) } workingDirectory: $(Pipeline.Workspace) displayName: 'Copy GPG signatures for build' - publish: '$(Build.ArtifactStagingDirectory)\hashes' artifact: hashes displayName: 'Publish Artifact: hashes' ================================================ FILE: windows-release/stage-sign.yml ================================================ parameters: Include: '*.exe, *.dll, *.pyd, *.cat, *.ps1' Exclude: 'vcruntime*, libffi*, libcrypto*, libssl*' SigningCertificate: '' DoFreethreaded: 'false' jobs: - ${{ if and(parameters.SigningCertificate, ne(parameters.SigningCertificate, 'Unsigned')) }}: - job: Sign_Files displayName: Sign Python binaries workspace: clean: all strategy: matrix: win32: Name: win32 amd64: Name: amd64 arm64: Name: arm64 ${{ if eq(parameters.DoFreethreaded, 'true') }}: win32_t: Name: win32_t amd64_t: Name: amd64_t arm64_t: Name: arm64_t variables: - ${{ if eq(parameters.SigningCertificate, 'PythonSoftwareFoundation') }}: - group: CPythonSign - ${{ if eq(parameters.SigningCertificate, 'TestSign') }}: - group: CPythonTestSign steps: - template: ./checkout.yml - powershell: | $d = (.\PCbuild\build.bat -V) | %{ if($_ -match '\s+(\w+):\s*(.+)\s*$') { @{$Matches[1] = $Matches[2];} }}; $tag = git rev-parse --short HEAD $desc = "Python $($d.PythonVersion) ($tag)" Write-Host "##vso[task.setvariable variable=SigningDescription]$desc" Write-Host "Updated signing description to: $desc" displayName: 'Update signing description' condition: and(succeeded(), not(variables['SigningDescription'])) - powershell: | Write-Host "##vso[build.addbuildtag]signed" displayName: 'Add build tags' - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: unsigned_$(Name)' inputs: artifactName: unsigned_$(Name) targetPath: $(Build.BinariesDirectory)\bin - template: sign-files.yml parameters: Include: ${{ parameters.Include }} Exclude: ${{ parameters.Exclude }} WorkingDir: $(Build.BinariesDirectory)\bin ExtractDir: $(Build.BinariesDirectory)\cert SigningCertificate: ${{ parameters.SigningCertificate }} - publish: '$(Build.BinariesDirectory)\bin' artifact: bin_$(Name) displayName: 'Publish artifact: bin_$(Name)' - publish: '$(Build.BinariesDirectory)\cert' artifact: cert displayName: 'Publish artifact: cert' - ${{ else }}: - job: Mark_Unsigned displayName: Tag unsigned build steps: - checkout: none - powershell: | Write-Host "##vso[build.addbuildtag]unsigned" displayName: 'Add build tag' ================================================ FILE: windows-release/stage-test-embed.yml ================================================ jobs: - job: Test_Embed displayName: Test Embed workspace: clean: all strategy: matrix: win32: Name: win32 amd64: Name: amd64 steps: - checkout: none - task: DownloadBuildArtifacts@0 displayName: 'Download artifact: embed' inputs: artifactName: embed downloadPath: $(Build.BinariesDirectory) - powershell: | $p = gi "$(Build.BinariesDirectory)\embed\python*embed-$(Name).zip" Expand-Archive -Path $p -DestinationPath "$(Build.BinariesDirectory)\Python" $p = gi "$(Build.BinariesDirectory)\Python\python.exe" Write-Host "##vso[task.prependpath]$(Split-Path -Parent $p)" displayName: 'Install Python and add to PATH' - script: | python -c "import sys; print(sys.version)" displayName: 'Collect version number' condition: and(succeeded(), not(variables['SkipTests'])) - script: | python -m site displayName: 'Collect site' condition: and(succeeded(), not(variables['SkipTests'])) ================================================ FILE: windows-release/stage-test-msi.yml ================================================ parameters: DoFreethreaded: false jobs: - job: Test_MSI displayName: Test MSI workspace: clean: all variables: ${{ if eq(parameters.DoFreethreaded, 'true') }}: IncludeFreethreadedOpt: Include_freethreaded=1 ${{ else }}: IncludeFreethreadedOpt: '' strategy: matrix: win32_User: ExeMatch: 'python-[\dabrc.]+\.exe' Logs: $(Build.ArtifactStagingDirectory)\logs\win32_User InstallAllUsers: 0 win32_Machine: ExeMatch: 'python-[\dabrc.]+\.exe' Logs: $(Build.ArtifactStagingDirectory)\logs\win32_Machine InstallAllUsers: 1 amd64_User: ExeMatch: 'python-[\dabrc.]+-amd64\.exe' Logs: $(Build.ArtifactStagingDirectory)\logs\amd64_User InstallAllUsers: 0 amd64_Machine: ExeMatch: 'python-[\dabrc.]+-amd64\.exe' Logs: $(Build.ArtifactStagingDirectory)\logs\amd64_Machine InstallAllUsers: 1 steps: - checkout: none - task: DownloadPipelineArtifact@2 displayName: 'Download artifact: msi' inputs: artifactName: msi targetPath: $(Build.BinariesDirectory)\msi - powershell: | $p = (gci -r *.exe | ?{ $_.Name -match '$(ExeMatch)' } | select -First 1) Write-Host "##vso[task.setvariable variable=SetupExe]$($p.FullName)" Write-Host "##vso[task.setvariable variable=SetupExeName]$($p.Name)" displayName: 'Find installer executable' workingDirectory: $(Build.BinariesDirectory)\msi - script: > "$(SetupExe)" /passive /log "$(Logs)\install\log.txt" TargetDir="$(Build.BinariesDirectory)\Python" Include_debug=1 Include_symbols=1 InstallAllUsers=$(InstallAllUsers) $(IncludeFreethreadedOpt) displayName: 'Install Python' - powershell: | gci "$(Build.BinariesDirectory)\python" displayName: 'List installed files' - powershell: | $p = gi "$(Build.BinariesDirectory)\Python\python.exe" Write-Host "##vso[task.prependpath]$(Split-Path -Parent $p)" displayName: 'Add test Python to PATH' - script: | python -c "import sys; print(sys.version)" displayName: 'Collect version number' condition: and(succeeded(), not(variables['SkipTests'])) - script: | python -m site displayName: 'Collect site' condition: and(succeeded(), not(variables['SkipTests'])) - ${{ if eq(parameters.DoFreethreaded, 'true') }}: - powershell: | $p = (gci "$(Build.BinariesDirectory)\Python\python3*t.exe" | select -First 1) Write-Host "Found $p" if (-not $p) { Write-Host "Did not find python3*t.exe in:" dir "$(Build.BinariesDirectory)\Python" throw "Free-threaded binaries were not installed" } else { & $p -c "import sys; print(sys.version)" } displayName: 'Collect free-threaded version number' condition: and(succeeded(), not(variables['SkipTests'])) - powershell: | gci -r "${env:PROGRAMDATA}\Microsoft\Windows\Start Menu\Programs\Python*" displayName: 'Capture per-machine Start Menu items' - powershell: | gci -r "${env:APPDATA}\Microsoft\Windows\Start Menu\Programs\Python*" displayName: 'Capture per-user Start Menu items' - powershell: | gci -r "HKLM:\Software\WOW6432Node\Python" displayName: 'Capture per-machine 32-bit registry' - powershell: | gci -r "HKLM:\Software\Python" displayName: 'Capture per-machine native registry' - powershell: | gci -r "HKCU:\Software\Python" displayName: 'Capture current-user registry' - script: | python -m pip install "azure<0.10" python -m pip uninstall -y azure python-dateutil six displayName: 'Test (un)install package' condition: and(succeeded(), not(variables['SkipTests'])) - powershell: | if (Test-Path -Type Container "$(Build.BinariesDirectory)\Python\Lib\test\test_ttk") { # New set of tests (3.12 and later) python -m test -uall -v test_ttk test_tkinter test_idle } else { # Old set of tests python -m test -uall -v test_ttk_guionly test_tk test_idle } displayName: 'Test Tkinter and Idle' condition: and(succeeded(), not(variables['SkipTests']), not(variables['SkipTkTests'])) - script: > "$(SetupExe)" /passive /uninstall /log "$(Logs)\uninstall\log.txt" displayName: 'Uninstall Python' - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: logs' condition: true continueOnError: true inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)\logs' ArtifactName: msi_testlogs ================================================ FILE: windows-release/stage-test-nuget.yml ================================================ jobs: - job: Test_Nuget displayName: Test Nuget workspace: clean: all strategy: matrix: win32: Package: pythonx86 amd64: Package: python steps: - checkout: none - task: DownloadBuildArtifacts@0 displayName: 'Download artifact: nuget' inputs: artifactName: nuget downloadPath: $(Build.BinariesDirectory) - task: NugetToolInstaller@0 inputs: versionSpec: '>= 5' - powershell: > nuget install $(Package) -Source "$(Build.BinariesDirectory)\nuget" -OutputDirectory "$(Build.BinariesDirectory)\install" -Prerelease -ExcludeVersion -NonInteractive displayName: 'Install Python' - powershell: | $p = gi "$(Build.BinariesDirectory)\install\$(Package)\tools\python.exe" Write-Host "##vso[task.prependpath]$(Split-Path -Parent $p)" displayName: 'Add test Python to PATH' - script: | python -c "import sys; print(sys.version)" displayName: 'Collect version number' condition: and(succeeded(), not(variables['SkipTests'])) - script: | python -m site displayName: 'Collect site' condition: and(succeeded(), not(variables['SkipTests'])) - script: | python -m pip install "azure<0.10" python -m pip uninstall -y azure python-dateutil six displayName: 'Test (un)install package' condition: and(succeeded(), not(variables['SkipTests'])) ================================================ FILE: windows-release/stage-test-pymanager.yml ================================================ parameters: DoEmbed: false DoFreethreaded: false jobs: - job: Test_PyManager displayName: Test PyManager workspace: clean: all strategy: matrix: win32: Name: win32 amd64: Name: amd64 win32_test: Name: win32_test amd64_test: Name: amd64_test ${{ if eq(parameters.DoEmbed, 'true') }}: win32_embed: Name: win32_embed SkipPipTest: 1 amd64_embed: Name: amd64_embed SkipPipTest: 1 ${{ if eq(parameters.DoFreethreaded, 'true') }}: win32_t: Name: win32_t amd64_t: Name: amd64_t steps: - checkout: none # TODO: Install PyManager and use that to install the package - download: current artifact: layout_pymanager_$(Name) displayName: 'Download artifact: layout_pymanager_$(Name)' - powershell: | $p = gi "$(Pipeline.Workspace)\layout_pymanager_$(Name)\python*.exe" | select -First 1 Write-Host "##vso[task.setvariable variable=PYTHON]$p" displayName: 'Add test Python to PATH' - powershell: | & $env:PYTHON -c "import sys; print(sys.version)" displayName: 'Collect version number' condition: and(succeeded(), not(variables['SkipTests'])) - powershell: | & $env:PYTHON -m site displayName: 'Collect site' condition: and(succeeded(), not(variables['SkipTests'])) - powershell: | & $env:PYTHON -m pip install "azure<0.10" & $env:PYTHON -m pip uninstall -y azure python-dateutil six displayName: 'Test (un)install package' condition: and(succeeded(), not(variables['SkipTests']), not(variables['SkipPipTest'])) ================================================ FILE: windows-release/start-arm64vm.yml ================================================ parameters: DoARM64: false DoPGOARM64: false jobs: # Only include the job if we need the VM, which means ARM64 PGO. - ${{ if eq(parameters.DoPGOARM64, 'true') }}: - job: Start_ARM64VM displayName: 'Ensure ARM64 VM is running' dependsOn: [] steps: - checkout: none - task: AzureCLI@2 displayName: 'Start pythonarm64 and set auto-shutdown to (UTC now - 1h)' inputs: azureSubscription: "Steve's VM" # WIF service connection name scriptType: pscore scriptLocation: inlineScript inlineScript: | $ErrorActionPreference = 'Stop' $rg = 'cpythonbuild' $vm = 'pythonarm64' # Compute UTC time minus 12 hours, format HHmm (e.g. 1830) $shutdownTime = (Get-Date).ToUniversalTime().AddHours(-12).ToString('HHmm') Write-Host "Setting auto-shutdown time to: $shutdownTime UTC" # Configure daily auto-shutdown in 12 hours az vm auto-shutdown -g $rg -n $vm --time $shutdownTime | Out-Null if ($?) { Write-Host "Successfully configured auto-shutdown for ARM64 VM in 12 hours." } else { Write-Host "##[warning]Failed to configure ARM64 VM auto-shutdown." } # Start VM, but don't fail if it's already running az vm start -g $rg -n $vm | Out-Null $u = "https://dev.azure.com/Python/cpython/_settings/agentqueues?queueId=24&view=agents" if ($?) { Write-Host "Successfully started ARM64 VM. Check $u for running status." } else { Write-Host "##[warning]Failed to start ARM64 VM. Check $u in case it is already active, or ping Steve." } ================================================ FILE: windows-release/tcltk-build.yml ================================================ parameters: - name: TclSourceTag displayName: 'Tcl Source Tag' type: string - name: TkSourceTag displayName: 'Tk Source Tag' type: string - name: IncludeTix displayName: 'Include Tix (pre-3.13)' type: boolean default: false - name: TixSourceTag displayName: 'Tix Source Tag' type: string default: tix-8.4.3.6 - name: SigningCertificate displayName: "Code signing certificate" type: string default: 'PythonSoftwareFoundation' values: - 'PythonSoftwareFoundation' - 'TestSign' - 'Unsigned' - name: SourcesRepo displayName: 'Sources Repository' type: string default: 'https://github.com/python/cpython-source-deps' name: tcltk$(TkSourceTag)_$(Date:yyyyMMdd)$(Rev:.rr) resources: repositories: - repository: cpython type: github name: Python/cpython endpoint: "Steve's github repos" variables: - ${{ if eq(parameters.SigningCertificate, 'PythonSoftwareFoundation') }}: - group: CPythonSign - ${{ if eq(parameters.SigningCertificate, 'TestSign') }}: - group: CPythonTestSign - name: IntDir value: '$(Build.BinariesDirectory)\obj' - name: ExternalsDir value: '$(Build.BinariesDirectory)\externals' - name: OutDir value: '$(Build.ArtifactStagingDirectory)' - name: Configuration value: 'Release' - name: SigningDescription value: 'Tcl/Tk for Python (${{ parameters.TclSourceTag }})' - name: SourcesRepo value: ${{ parameters.SourcesRepo }} - name: TclSourceTag value: ${{ parameters.TclSourceTag }} - name: TkSourceTag value: ${{ parameters.TkSourceTag }} - name: TixSourceTag value: ${{ parameters.TixSourceTag }} jobs: - job: Build_TclTk displayName: 'Tcl/Tk' pool: vmImage: windows-latest workspace: clean: all steps: - checkout: self - checkout: cpython - template: ./find-tools.yml - powershell: | git clone $(SourcesRepo) -b $(TclSourceTag) --depth 1 "$(ExternalsDir)\$(TclSourceTag)" displayName: 'Check out Tcl sources' - powershell: | git clone $(SourcesRepo) -b $(TkSourceTag) --depth 1 "$(ExternalsDir)\$(TkSourceTag)" displayName: 'Check out Tk sources' - ${{ if eq(parameters.IncludeTix, 'true') }}: - powershell: | git clone $(SourcesRepo) -b $(TixSourceTag) --depth 1 "$(ExternalsDir)\$(TixSourceTag)" displayName: 'Check out Tix sources' # This msbuild.rsp file will be used by the build to forcibly override these variables - powershell: | del -Force -EA 0 msbuild.rsp "/p:IntDir=$(IntDir)\" >> msbuild.rsp "/p:ExternalsDir=$(ExternalsDir)\" >> msbuild.rsp "/p:tclDir=$(ExternalsDir)\$(TclSourceTag)\" >> msbuild.rsp "/p:tkDir=$(ExternalsDir)\$(TkSourceTag)\" >> msbuild.rsp "/p:tixDir=$(ExternalsDir)\$(TixSourceTag)\" >> msbuild.rsp displayName: 'Generate msbuild.rsp' - powershell: | & "$(msbuild)" cpython\PCbuild\tcl.vcxproj "@msbuild.rsp" /p:Platform=Win32 /p:tcltkDir="$(OutDir)\win32" & "$(msbuild)" cpython\PCbuild\tk.vcxproj "@msbuild.rsp" /p:Platform=Win32 /p:tcltkDir="$(OutDir)\win32" displayName: 'Build for win32' - powershell: | & "$(msbuild)" cpython\PCbuild\tcl.vcxproj "@msbuild.rsp" /p:Platform=x64 /p:tcltkDir="$(OutDir)\amd64" & "$(msbuild)" cpython\PCbuild\tk.vcxproj "@msbuild.rsp" /p:Platform=x64 /p:tcltkDir="$(OutDir)\amd64" displayName: 'Build for amd64' - powershell: | & "$(msbuild)" cpython\PCbuild\tcl.vcxproj "@msbuild.rsp" /p:Platform=ARM64 /p:tcltkDir="$(OutDir)\arm64" & "$(msbuild)" cpython\PCbuild\tk.vcxproj "@msbuild.rsp" /p:Platform=ARM64 /p:tcltkDir="$(OutDir)\arm64" displayName: 'Build for arm64' - ${{ if eq(parameters.IncludeTix, 'true') }}: - powershell: | & "$(msbuild)" cpython\PCbuild\tix.vcxproj "@msbuild.rsp" /p:Platform=Win32 /p:tcltkDir="$(OutDir)\win32" & "$(msbuild)" cpython\PCbuild\tix.vcxproj "@msbuild.rsp" /p:Platform=x64 /p:tcltkDir="$(OutDir)\amd64" & "$(msbuild)" cpython\PCbuild\tix.vcxproj "@msbuild.rsp" /p:Platform=ARM64 /p:tcltkDir="$(OutDir)\arm64" displayName: 'Build Tix' - ${{ if ne(parameters.SigningCertificate, 'Unsigned') }}: - template: sign-files.yml parameters: Include: '-r *.dll' WorkingDir: '$(OutDir)' SigningCertificate: ${{ parameters.SigningCertificate }} - publish: '$(OutDir)' artifact: 'tcltk' displayName: 'Publishing tcltk' ================================================ FILE: windows-release/uploadrelease.ps1 ================================================ <# .Synopsis Uploads from a VSTS release build layout to python.org .Description Given the downloaded/extracted build artifact from a release build run on python.visualstudio.com, this script uploads the files to the correct locations. .Parameter build The location on disk of the extracted build artifact. .Parameter user The username to use when logging into the host. .Parameter server The host or PuTTY session name. .Parameter target The subdirectory on the host to copy files to. .Parameter tests The path to run download tests in. .Parameter embed Optional path besides -build to locate ZIP files. #> param( [Parameter(Mandatory=$true)][string]$build, [Parameter(Mandatory=$true)][string]$user, [Parameter(Mandatory=$true)][string]$server, [Parameter(Mandatory=$true)][string]$hostkey, [Parameter(Mandatory=$true)][string]$keyfile, [string]$target="/srv/www.python.org/ftp/python", [string]$tests=${env:TEMP}, [string]$embed=$null, [string]$sbom=$null ) if (-not $build) { throw "-build option is required" } if (-not $user) { throw "-user option is required" } $tools = $script:MyInvocation.MyCommand.Path | Split-Path -parent; if (-not ((Test-Path "$build\win32\python-*.exe") -or (Test-Path "$build\amd64\python-*.exe"))) { throw "-build argument does not look like a 'build' directory" } function find-putty-tool { param ([string]$n) $t = gcm $n -EA 0 if (-not $t) { $t = gcm ".\$n" -EA 0 } if (-not $t) { $t = gcm "${env:ProgramFiles}\PuTTY\$n" -EA 0 } if (-not $t) { $t = gcm "${env:ProgramFiles(x86)}\PuTTY\$n" -EA 0 } if (-not $t) { throw "Unable to locate $n.exe. Please put it on $PATH" } return gi $t.Path } $p = gci -r "$build\python-*.exe" | ` ?{ $_.Name -match '^python-(\d+\.\d+\.\d+)((a|b|rc)\d+)?-.+' } | ` select -first 1 | ` %{ $Matches[1], $Matches[2] } "Uploading version $($p[0]) $($p[1])" " from: $build" " to: $($server):$target/$($p[0])" "" # Upload files to the server $pscp = find-putty-tool "pscp" $plink = find-putty-tool "plink" "Upload using $pscp and $plink" "" $d = "$target/$($p[0])/" & $plink -batch -hostkey $hostkey -noagent -i $keyfile "$user@$server" mkdir $d & $plink -batch -hostkey $hostkey -noagent -i $keyfile "$user@$server" chgrp downloads $d & $plink -batch -hostkey $hostkey -noagent -i $keyfile "$user@$server" chmod "a+rx" $d $dirs = gci "$build" -Directory if ($embed -and (Test-Path $embed)) { $dirs = ($dirs, (gi $embed)) | %{ $_ } } if ($sbom -and (Test-Path $sbom)) { $dirs = ($dirs, $sbom) | %{ $_ } } foreach ($a in $dirs) { "Uploading files from $($a.FullName)" pushd "$($a.FullName)" $exe = gci *.exe, *.exe.asc, *.zip, *.zip.asc $msi = gci *.msi, *.msi.asc, *.msu, *.msu.asc $spdx_json = gci *.spdx.json popd if ($exe) { & $pscp -batch -hostkey $hostkey -noagent -i $keyfile $exe.FullName "$user@${server}:$d" if (-not $?) { throw "Failed to upload $exe" } } if ($spdx_json) { & $pscp -batch -hostkey $hostkey -noagent -i $keyfile $spdx_json.FullName "$user@${server}:$d" if (-not $?) { Write-Host "##[warning]Failed to upload $spdx_json" } } if ($msi) { $sd = "$d$($a.Name)$($p[1])/" & $plink -batch -hostkey $hostkey -noagent -i $keyfile "$user@$server" mkdir $sd & $plink -batch -hostkey $hostkey -noagent -i $keyfile "$user@$server" chgrp downloads $sd & $plink -batch -hostkey $hostkey -noagent -i $keyfile "$user@$server" chmod "a+rx" $sd & $pscp -batch -hostkey $hostkey -noagent -i $keyfile $msi.FullName "$user@${server}:$sd" if (-not $?) { throw "Failed to upload $msi" } & $plink -batch -hostkey $hostkey -noagent -i $keyfile "$user@$server" chgrp downloads $sd* & $plink -batch -hostkey $hostkey -noagent -i $keyfile "$user@$server" chmod "g-x,o+r" $sd* } } & $plink -batch -hostkey $hostkey -noagent -i $keyfile "$user@$server" chgrp downloads $d* & $plink -batch -hostkey $hostkey -noagent -i $keyfile "$user@$server" chmod "g-x,o+r" $d* & $pscp -batch -hostkey $hostkey -noagent -i $keyfile -ls "$user@${server}:$d"