From d36954b7ead06daead3dcf9b0dd9f8002eab508f Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Wed, 22 Jun 2022 16:32:22 +0300 Subject: [PATCH] gh-91172: Create a workflow for verifying bundled pip and setuptools (GH-31885) Co-authored-by: Hugo van Kemenade Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- .github/workflows/verify-ensurepip-wheels.yml | 28 ++++++ .../2022-03-14-23-28-17.bpo-47016.K-t2QX.rst | 2 + Tools/scripts/verify_ensurepip_wheels.py | 98 +++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 .github/workflows/verify-ensurepip-wheels.yml create mode 100644 Misc/NEWS.d/next/Tests/2022-03-14-23-28-17.bpo-47016.K-t2QX.rst create mode 100755 Tools/scripts/verify_ensurepip_wheels.py diff --git a/.github/workflows/verify-ensurepip-wheels.yml b/.github/workflows/verify-ensurepip-wheels.yml new file mode 100644 index 00000000000..61e3d1adf53 --- /dev/null +++ b/.github/workflows/verify-ensurepip-wheels.yml @@ -0,0 +1,28 @@ +name: Verify bundled pip and setuptools + +on: + workflow_dispatch: + push: + paths: + - 'Lib/ensurepip/_bundled/**' + - '.github/workflows/verify-ensurepip-wheels.yml' + - 'Tools/scripts/verify_ensurepip_wheels.py' + pull_request: + paths: + - 'Lib/ensurepip/_bundled/**' + - '.github/workflows/verify-ensurepip-wheels.yml' + - 'Tools/scripts/verify_ensurepip_wheels.py' + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3' + - name: Compare checksums of bundled pip and setuptools to ones published on PyPI + run: ./Tools/scripts/verify_ensurepip_wheels.py diff --git a/Misc/NEWS.d/next/Tests/2022-03-14-23-28-17.bpo-47016.K-t2QX.rst b/Misc/NEWS.d/next/Tests/2022-03-14-23-28-17.bpo-47016.K-t2QX.rst new file mode 100644 index 00000000000..774bfafc021 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2022-03-14-23-28-17.bpo-47016.K-t2QX.rst @@ -0,0 +1,2 @@ +Create a GitHub Actions workflow for verifying bundled pip and setuptools. +Patch by Illia Volochii and Adam Turner. diff --git a/Tools/scripts/verify_ensurepip_wheels.py b/Tools/scripts/verify_ensurepip_wheels.py new file mode 100755 index 00000000000..044d1fd6b3c --- /dev/null +++ b/Tools/scripts/verify_ensurepip_wheels.py @@ -0,0 +1,98 @@ +#! /usr/bin/env python3 + +""" +Compare checksums for wheels in :mod:`ensurepip` against the Cheeseshop. + +When GitHub Actions executes the script, output is formatted accordingly. +https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message +""" + +import hashlib +import json +import os +import re +from pathlib import Path +from urllib.request import urlopen + +PACKAGE_NAMES = ("pip", "setuptools") +ENSURE_PIP_ROOT = Path(__file__).parent.parent.parent / "Lib/ensurepip" +WHEEL_DIR = ENSURE_PIP_ROOT / "_bundled" +ENSURE_PIP_INIT_PY_TEXT = (ENSURE_PIP_ROOT / "__init__.py").read_text(encoding="utf-8") +GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" + + +def print_notice(file_path: str, message: str) -> None: + if GITHUB_ACTIONS: + message = f"::notice file={file_path}::{message}" + print(message, end="\n\n") + + +def print_error(file_path: str, message: str) -> None: + if GITHUB_ACTIONS: + message = f"::error file={file_path}::{message}" + print(message, end="\n\n") + + +def verify_wheel(package_name: str) -> bool: + # Find the package on disk + package_path = next(WHEEL_DIR.glob(f"{package_name}*.whl"), None) + if not package_path: + print_error("", f"Could not find a {package_name} wheel on disk.") + return False + + print(f"Verifying checksum for {package_path}.") + + # Find the version of the package used by ensurepip + package_version_match = re.search( + f'_{package_name.upper()}_VERSION = "([^"]+)', ENSURE_PIP_INIT_PY_TEXT + ) + if not package_version_match: + print_error( + package_path, + f"No {package_name} version found in Lib/ensurepip/__init__.py.", + ) + return False + package_version = package_version_match[1] + + # Get the SHA 256 digest from the Cheeseshop + try: + raw_text = urlopen(f"https://pypi.org/pypi/{package_name}/json").read() + except (OSError, ValueError): + print_error(package_path, f"Could not fetch JSON metadata for {package_name}.") + return False + + release_files = json.loads(raw_text)["releases"][package_version] + for release_info in release_files: + if package_path.name != release_info["filename"]: + continue + expected_digest = release_info["digests"].get("sha256", "") + break + else: + print_error(package_path, f"No digest for {package_name} found from PyPI.") + return False + + # Compute the SHA 256 digest of the wheel on disk + actual_digest = hashlib.sha256(package_path.read_bytes()).hexdigest() + + print(f"Expected digest: {expected_digest}") + print(f"Actual digest: {actual_digest}") + + if actual_digest != expected_digest: + print_error( + package_path, f"Failed to verify the checksum of the {package_name} wheel." + ) + return False + + print_notice( + package_path, + f"Successfully verified the checksum of the {package_name} wheel.", + ) + return True + + +if __name__ == "__main__": + exit_status = 0 + for package_name in PACKAGE_NAMES: + if not verify_wheel(package_name): + exit_status = 1 + raise SystemExit(exit_status)