| #!/usr/bin/env python3 | |
| # Licensed to the Apache Software Foundation (ASF) under one or more | |
| # contributor license agreements. See the NOTICE file distributed with | |
| # this work for additional information regarding copyright ownership. | |
| # The ASF licenses this file to You under the Apache License, Version 2.0 | |
| # (the "License"); you may not use this file except in compliance with | |
| # the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """Verify Python release archives and their legal metadata.""" | |
| from __future__ import annotations | |
| import argparse | |
| import base64 | |
| import csv | |
| import hashlib | |
| import io | |
| import re | |
| import stat | |
| import sys | |
| import tarfile | |
| import tomllib | |
| import unicodedata | |
| import zipfile | |
| from email import policy | |
| from email.message import Message | |
| from email.parser import BytesParser | |
| from pathlib import Path, PurePosixPath | |
| ROOT = Path(__file__).resolve().parents[1] | |
| PYTHON_DIR = ROOT / "bindings/python" | |
| LEGAL_FILES = ("LICENSE", "NOTICE", "THIRD-PARTY-LICENSES.html") | |
| TARGETS = { | |
| "x86_64-unknown-linux-gnu", | |
| "aarch64-unknown-linux-gnu", | |
| "x86_64-apple-darwin", | |
| "aarch64-apple-darwin", | |
| "x86_64-pc-windows-msvc", | |
| } | |
| PLACEHOLDERS = ( | |
| b"<year>", | |
| b"<owner>", | |
| b"<copyright holders>", | |
| b"<year>", | |
| b"<owner>", | |
| b"<copyright holders>", | |
| ) | |
| AVRO_NOTICE_MARKERS = ( | |
| b"Apache Avro", | |
| b"Flavien Raynaud", | |
| b"donated to the Apache Avro project in 2020.", | |
| ) | |
| XZ_LICENSE_ANCHOR = b'id="bundled-xz-utils-0bsd"' | |
| class VerificationError(RuntimeError): | |
| """A release archive failed verification.""" | |
| def require(condition: bool, message: str) -> None: | |
| if not condition: | |
| raise VerificationError(message) | |
| def normalized_relative_path( | |
| value: str, description: str, allow_trailing_slash: bool = False | |
| ) -> PurePosixPath: | |
| require(bool(value), f"empty {description}") | |
| require("\\" not in value, f"invalid {description}: {value!r}") | |
| require( | |
| re.match(r"^[A-Za-z]:", value) is None, | |
| f"Windows drive {description}: {value!r}", | |
| ) | |
| candidate = value[:-1] if allow_trailing_slash and value.endswith("/") else value | |
| path = PurePosixPath(candidate) | |
| require(not path.is_absolute(), f"absolute {description}: {value!r}") | |
| require(".." not in path.parts, f"unsafe {description}: {value!r}") | |
| canonical = path.as_posix() | |
| require( | |
| value == canonical | |
| or (allow_trailing_slash and value == f"{canonical}/"), | |
| f"non-canonical {description}: {value!r}", | |
| ) | |
| return path | |
| def portable_path_key(path: PurePosixPath) -> str: | |
| return unicodedata.normalize("NFC", path.as_posix()).casefold() | |
| def index_tar_members( | |
| members: list[tarfile.TarInfo], description: str | |
| ) -> dict[str, tarfile.TarInfo]: | |
| result = {} | |
| portable_names = set() | |
| for member in members: | |
| require( | |
| member.isfile() or member.isdir(), | |
| f"{description} has a link or special member: {member.name}", | |
| ) | |
| normalized = normalized_relative_path( | |
| member.name, | |
| f"{description} member", | |
| allow_trailing_slash=member.isdir(), | |
| ) | |
| name = normalized.as_posix() | |
| require( | |
| name not in result, | |
| f"{description} has duplicate normalized member: {name}", | |
| ) | |
| portable_name = portable_path_key(normalized) | |
| require( | |
| portable_name not in portable_names, | |
| f"{description} has a portable path collision: {name}", | |
| ) | |
| result[name] = member | |
| portable_names.add(portable_name) | |
| return result | |
| def verify_record( | |
| archive: zipfile.ZipFile, | |
| names: list[str], | |
| record_member: str, | |
| description: str, | |
| ) -> None: | |
| try: | |
| content = archive.read(record_member).decode("utf-8") | |
| rows = list(csv.reader(io.StringIO(content, newline=""), strict=True)) | |
| except (UnicodeDecodeError, csv.Error) as error: | |
| raise VerificationError(f"invalid RECORD in {description}: {error}") from error | |
| entries = {} | |
| for row in rows: | |
| require(len(row) == 3, f"invalid RECORD row in {description}: {row}") | |
| name, digest, size = row | |
| normalized_relative_path(name, "RECORD path") | |
| require(name not in entries, f"duplicate RECORD path in {description}: {name}") | |
| entries[name] = (digest, size) | |
| require( | |
| set(entries) == set(names), | |
| f"RECORD members differ in {description}", | |
| ) | |
| record_dir = PurePosixPath(record_member).parent | |
| unsigned = { | |
| record_member, | |
| str(record_dir / "RECORD.jws"), | |
| str(record_dir / "RECORD.p7s"), | |
| } | |
| for name in names: | |
| digest, size = entries[name] | |
| if name in unsigned: | |
| require(not digest and not size, f"signed RECORD row in {description}: {name}") | |
| continue | |
| data = archive.read(name) | |
| encoded = base64.urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b"=") | |
| require( | |
| digest == f"sha256={encoded.decode()}", | |
| f"RECORD hash differs in {description}: {name}", | |
| ) | |
| require( | |
| size.isdecimal() and int(size) == len(data), | |
| f"RECORD size differs in {description}: {name}", | |
| ) | |
| def verify_legal_bytes(content: bytes, description: str) -> None: | |
| require(b"\r" not in content, f"{description} contains CRLF line endings") | |
| lowered = content.lower() | |
| for placeholder in PLACEHOLDERS: | |
| require( | |
| placeholder not in lowered, | |
| f"{description} contains license placeholder {placeholder!r}", | |
| ) | |
| def verify_avro_notice(content: bytes, description: str) -> None: | |
| for marker in AVRO_NOTICE_MARKERS: | |
| require(marker in content, f"{description} is missing {marker!r}") | |
| def verify_license_report(content: bytes, target: str, description: str) -> None: | |
| markers = ( | |
| b"<strong>Artifact:</strong> <code>python</code>", | |
| f"<strong>Rust target:</strong> <code>{target}</code>".encode(), | |
| b"<strong>Root manifest:</strong> <code>bindings/python/Cargo.toml</code>", | |
| XZ_LICENSE_ANCHOR, | |
| ) | |
| for marker in markers: | |
| require(marker in content, f"{description} is missing {marker!r}") | |
| def parse_metadata(content: bytes, description: str) -> Message: | |
| try: | |
| metadata = BytesParser(policy=policy.default).parsebytes(content) | |
| except Exception as error: | |
| raise VerificationError(f"invalid {description}: {error}") from error | |
| require( | |
| metadata.get("Metadata-Version") == "2.4", | |
| f"{description} must use Metadata-Version 2.4", | |
| ) | |
| require( | |
| metadata.get("Name") == "pypaimon-rust", | |
| f"{description} has an invalid package name", | |
| ) | |
| require(bool(metadata.get("Version")), f"{description} has no version") | |
| require( | |
| metadata.get("License-Expression") == "Apache-2.0", | |
| f"{description} has an invalid License-Expression", | |
| ) | |
| license_files = metadata.get_all("License-File", []) | |
| require( | |
| sorted(license_files) == sorted(LEGAL_FILES), | |
| f"{description} has invalid License-File entries: {license_files}", | |
| ) | |
| return metadata | |
| def workspace_version() -> str: | |
| with (ROOT / "Cargo.toml").open("rb") as source: | |
| return str(tomllib.load(source)["workspace"]["package"]["version"]) | |
| def release_pyproject(source: str, version: str, base_version: str) -> str: | |
| if version == base_version: | |
| return source | |
| dynamic_version = 'dynamic = ["version"]\n' | |
| fixed_version = f'version = "{version}"\n' | |
| dynamic_count = source.count(dynamic_version) | |
| fixed_count = source.count(fixed_version) | |
| if dynamic_count == 1 and fixed_count == 0: | |
| return source.replace(dynamic_version, fixed_version, 1) | |
| require( | |
| dynamic_count == 0 and fixed_count == 1, | |
| "unexpected Python release version setting", | |
| ) | |
| return source | |
| def expected_sdist_pyproject(version: str) -> bytes: | |
| source = (PYTHON_DIR / "pyproject.toml").read_text(encoding="utf-8") | |
| source = release_pyproject(source, version, workspace_version()) | |
| python_source = 'python-source = "python"\n' | |
| dependency_groups = "\n[dependency-groups]\n" | |
| require(source.count(python_source) == 1, "unexpected python-source setting") | |
| require( | |
| source.count(dependency_groups) == 1, | |
| "unexpected dependency-groups section", | |
| ) | |
| source = source.replace(python_source, "", 1) | |
| rewrite = ( | |
| 'manifest-path = "bindings/python/Cargo.toml"\n' | |
| + python_source | |
| + dependency_groups | |
| ) | |
| return source.replace(dependency_groups, rewrite, 1).encode() | |
| def target_from_wheel_name(path: Path) -> str: | |
| parts = path.name.removesuffix(".whl").rsplit("-", 3) | |
| require(len(parts) == 4, f"invalid wheel filename: {path.name}") | |
| platform_tags = parts[-1].split(".") | |
| targets = set() | |
| for platform_tag in platform_tags: | |
| if platform_tag == "win_amd64": | |
| targets.add("x86_64-pc-windows-msvc") | |
| elif platform_tag.startswith("macosx_") and platform_tag.endswith("_x86_64"): | |
| targets.add("x86_64-apple-darwin") | |
| elif platform_tag.startswith("macosx_") and platform_tag.endswith("_arm64"): | |
| targets.add("aarch64-apple-darwin") | |
| elif re.fullmatch( | |
| r"(?:manylinux(?:\d{4}|_\d+_\d+)|linux)_x86_64", platform_tag | |
| ): | |
| targets.add("x86_64-unknown-linux-gnu") | |
| elif re.fullmatch( | |
| r"(?:manylinux(?:\d{4}|_\d+_\d+)|linux)_aarch64", platform_tag | |
| ): | |
| targets.add("aarch64-unknown-linux-gnu") | |
| else: | |
| raise VerificationError( | |
| f"unsupported wheel platform tag {platform_tag!r}: {path.name}" | |
| ) | |
| require(len(targets) == 1, f"wheel has mixed platform targets: {path.name}") | |
| return targets.pop() | |
| def verify_native_header(content: bytes, target: str, description: str) -> None: | |
| if target.endswith("linux-gnu"): | |
| require(content[:4] == b"\x7fELF", f"{description} is not ELF") | |
| require(len(content) >= 20 and content[4] == 2, f"{description} is not ELF64") | |
| byte_order = "little" if content[5] == 1 else "big" | |
| machine = int.from_bytes(content[18:20], byte_order) | |
| expected = 62 if target.startswith("x86_64") else 183 | |
| require(machine == expected, f"{description} has ELF machine {machine}") | |
| return | |
| if target.endswith("windows-msvc"): | |
| require(content[:2] == b"MZ" and len(content) >= 64, f"{description} is not PE") | |
| pe_offset = int.from_bytes(content[60:64], "little") | |
| require( | |
| len(content) >= pe_offset + 6, f"{description} has a truncated PE header" | |
| ) | |
| require( | |
| content[pe_offset : pe_offset + 4] == b"PE\0\0", f"{description} is not PE" | |
| ) | |
| machine = int.from_bytes(content[pe_offset + 4 : pe_offset + 6], "little") | |
| require(machine == 0x8664, f"{description} has PE machine {machine:#x}") | |
| return | |
| magic = content[:4] | |
| if magic == b"\xcf\xfa\xed\xfe": | |
| byte_order = "little" | |
| elif magic == b"\xfe\xed\xfa\xcf": | |
| byte_order = "big" | |
| else: | |
| raise VerificationError(f"{description} is not 64-bit Mach-O") | |
| require(len(content) >= 8, f"{description} has a truncated Mach-O header") | |
| cpu_type = int.from_bytes(content[4:8], byte_order) | |
| expected = 0x01000007 if target.startswith("x86_64") else 0x0100000C | |
| require(cpu_type == expected, f"{description} has Mach-O CPU type {cpu_type:#x}") | |
| def is_native_library(name: str) -> bool: | |
| filename = PurePosixPath(name).name.lower() | |
| return filename.endswith((".so", ".pyd", ".dylib", ".dll")) or ".so." in filename | |
| def verify_wheel(path: Path) -> tuple[str, str]: | |
| target = target_from_wheel_name(path) | |
| expected_dir = PYTHON_DIR / "licenses" / target | |
| if not expected_dir.is_dir(): | |
| expected_dir = PYTHON_DIR | |
| filename_prefix = "pypaimon_rust-" | |
| filename_head = path.name.removesuffix(".whl").rsplit("-", 3)[0] | |
| require( | |
| filename_head.startswith(filename_prefix), | |
| f"invalid wheel distribution name: {path.name}", | |
| ) | |
| filename_version = filename_head.removeprefix(filename_prefix) | |
| try: | |
| archive = zipfile.ZipFile(path) | |
| except (OSError, zipfile.BadZipFile) as error: | |
| raise VerificationError(f"cannot open wheel {path}: {error}") from error | |
| with archive: | |
| infos = [info for info in archive.infolist() if not info.is_dir()] | |
| names = [info.filename for info in infos] | |
| portable_names = set() | |
| for info in infos: | |
| mode = info.external_attr >> 16 | |
| file_type = stat.S_IFMT(mode) | |
| require( | |
| file_type in {0, stat.S_IFREG}, | |
| f"wheel has a link or special member: {info.filename}", | |
| ) | |
| normalized = normalized_relative_path(info.filename, "wheel member") | |
| key = portable_path_key(normalized) | |
| require( | |
| key not in portable_names, | |
| f"wheel has a portable path collision: {path.name}:{info.filename}", | |
| ) | |
| portable_names.add(key) | |
| bad_member = archive.testzip() | |
| require(bad_member is None, f"wheel has a corrupt member: {bad_member}") | |
| metadata_members = [ | |
| name for name in names if name.endswith(".dist-info/METADATA") | |
| ] | |
| require( | |
| len(metadata_members) == 1, f"wheel must contain one METADATA: {path.name}" | |
| ) | |
| metadata_member = metadata_members[0] | |
| metadata = parse_metadata(archive.read(metadata_member), metadata_member) | |
| version = str(metadata["Version"]) | |
| require( | |
| version == filename_version, | |
| f"wheel filename and metadata versions differ: {path.name}", | |
| ) | |
| dist_info = PurePosixPath(metadata_member).parent | |
| wheel_members = [name for name in names if name == f"{dist_info}/WHEEL"] | |
| require(len(wheel_members) == 1, f"wheel metadata is incomplete: {path.name}") | |
| wheel_metadata = BytesParser(policy=policy.default).parsebytes( | |
| archive.read(wheel_members[0]) | |
| ) | |
| require( | |
| wheel_metadata.get("Root-Is-Purelib", "").lower() == "false", | |
| f"wheel is incorrectly marked pure: {path.name}", | |
| ) | |
| record_member = f"{dist_info}/RECORD" | |
| require(record_member in names, f"wheel has no RECORD: {path.name}") | |
| verify_record(archive, names, record_member, path.name) | |
| license_report = None | |
| for license_file in metadata.get_all("License-File", []): | |
| relative = normalized_relative_path(license_file, "License-File entry") | |
| members = ( | |
| str(relative), | |
| str(dist_info / "licenses" / relative), | |
| ) | |
| matches = [ | |
| name | |
| for name in names | |
| if PurePosixPath(name).name == relative.name | |
| and relative.name in LEGAL_FILES | |
| ] | |
| require( | |
| sorted(matches) == sorted(members), | |
| f"wheel has invalid {relative.name} members: {matches}", | |
| ) | |
| expected_path = expected_dir / relative | |
| require( | |
| expected_path.is_file(), | |
| f"missing committed legal file: {expected_path}", | |
| ) | |
| expected = expected_path.read_bytes() | |
| for member in members: | |
| actual = archive.read(member) | |
| require( | |
| actual == expected, | |
| f"wheel {member} differs for {target}", | |
| ) | |
| verify_legal_bytes(actual, f"{path.name}:{member}") | |
| if relative.name == "NOTICE": | |
| verify_avro_notice(actual, f"{path.name}:{member}") | |
| if relative.name == "THIRD-PARTY-LICENSES.html": | |
| license_report = actual | |
| verify_license_report(actual, target, f"{path.name}:{member}") | |
| native_members = [ | |
| name | |
| for name in names | |
| if len(PurePosixPath(name).parts) == 2 | |
| and PurePosixPath(name).parts[0] == "pypaimon_rust" | |
| and PurePosixPath(name).name.startswith("pypaimon_rust.") | |
| and PurePosixPath(name).suffix in {".so", ".pyd"} | |
| ] | |
| require( | |
| len(native_members) == 1, | |
| f"wheel must contain one native module: {path.name}", | |
| ) | |
| native_member = native_members[0] | |
| expected_suffix = ".pyd" if target.endswith("windows-msvc") else ".so" | |
| require( | |
| PurePosixPath(native_member).suffix == expected_suffix, | |
| f"wheel has an invalid native module suffix: {native_member}", | |
| ) | |
| with archive.open(native_member) as native_file: | |
| verify_native_header( | |
| native_file.read(65536), target, f"{path.name}:{native_member}" | |
| ) | |
| extra_native_members = [ | |
| name for name in names if name != native_member and is_native_library(name) | |
| ] | |
| if target.endswith("linux-gnu"): | |
| openssl_version = ( | |
| b"OpenSSL 1.1.1k FIPS" | |
| if target.startswith("x86_64") | |
| else b"OpenSSL 1.1.1w" | |
| ) | |
| openssl_libraries = ( | |
| (r"pypaimon_rust\.libs/libcrypto-[^/]+\.so\.1\.1", openssl_version), | |
| (r"pypaimon_rust\.libs/libssl-[^/]+\.so\.1\.1", None), | |
| ) | |
| for pattern, version_marker in openssl_libraries: | |
| matches = [ | |
| name for name in extra_native_members if re.fullmatch(pattern, name) | |
| ] | |
| require( | |
| len(matches) == 1, | |
| f"wheel must contain one {pattern}: {extra_native_members}", | |
| ) | |
| with archive.open(matches[0]) as native_file: | |
| content = native_file.read() | |
| verify_native_header( | |
| content, | |
| target, | |
| f"{path.name}:{matches[0]}", | |
| ) | |
| if version_marker is not None: | |
| require( | |
| version_marker in content, | |
| f"wheel libcrypto is missing {version_marker!r}", | |
| ) | |
| require( | |
| len(extra_native_members) == len(openssl_libraries), | |
| f"wheel has unexpected native libraries: {extra_native_members}", | |
| ) | |
| require( | |
| license_report is not None | |
| and b'id="bundled-openssl-1.1.1"' in license_report, | |
| "Linux wheel license report is missing the OpenSSL anchor", | |
| ) | |
| else: | |
| require( | |
| not extra_native_members, | |
| f"wheel has unexpected native libraries: {extra_native_members}", | |
| ) | |
| return target, version | |
| def verify_sdist(path: Path) -> str: | |
| try: | |
| archive = tarfile.open(path, mode="r:gz") | |
| except (OSError, tarfile.TarError) as error: | |
| raise VerificationError(f"cannot open sdist {path}: {error}") from error | |
| with archive: | |
| members_by_name = index_tar_members( | |
| archive.getmembers(), f"sdist {path.name}" | |
| ) | |
| names = list(members_by_name) | |
| roots = {PurePosixPath(name).parts[0] for name in names} | |
| require(len(roots) == 1, f"sdist must have one root directory: {path.name}") | |
| root = roots.pop() | |
| def read_member(name: str) -> bytes: | |
| try: | |
| member = members_by_name[name] | |
| except KeyError as error: | |
| raise VerificationError( | |
| f"sdist is missing {name}: {path.name}" | |
| ) from error | |
| require(member.isfile(), f"sdist member is not a file: {name}") | |
| extracted = archive.extractfile(member) | |
| require(extracted is not None, f"cannot read sdist member: {name}") | |
| return extracted.read() | |
| metadata = parse_metadata(read_member(f"{root}/PKG-INFO"), f"{root}/PKG-INFO") | |
| version = str(metadata["Version"]) | |
| require(root == f"pypaimon_rust-{version}", f"invalid sdist root: {root}") | |
| require( | |
| path.name == f"pypaimon_rust-{version}.tar.gz", | |
| f"invalid sdist filename: {path.name}", | |
| ) | |
| for license_file in metadata.get_all("License-File", []): | |
| relative = normalized_relative_path(license_file, "License-File entry") | |
| member = f"{root}/{relative}" | |
| actual = read_member(member) | |
| expected_path = PYTHON_DIR / relative | |
| require( | |
| expected_path.is_file(), f"missing source legal file: {expected_path}" | |
| ) | |
| require( | |
| actual == expected_path.read_bytes(), | |
| f"sdist {relative} differs from source", | |
| ) | |
| verify_legal_bytes(actual, f"{path.name}:{member}") | |
| require( | |
| read_member(f"{root}/Cargo.lock") == (ROOT / "Cargo.lock").read_bytes(), | |
| "sdist Cargo.lock differs from source", | |
| ) | |
| require( | |
| read_member(f"{root}/pyproject.toml") == expected_sdist_pyproject(version), | |
| "sdist pyproject.toml has an unexpected rewrite", | |
| ) | |
| target_reports = [ | |
| name for name in names if "/bindings/python/licenses/" in name | |
| ] | |
| require( | |
| not target_reports, | |
| f"sdist contains target-specific license reports: {target_reports}", | |
| ) | |
| native_members = [name for name in names if is_native_library(name)] | |
| require( | |
| not native_members, f"sdist contains native libraries: {native_members}" | |
| ) | |
| return version | |
| def collect_artifacts(inputs: list[str]) -> list[Path]: | |
| candidates = [Path(value) for value in inputs] or [PYTHON_DIR / "dist"] | |
| artifacts = [] | |
| for candidate in candidates: | |
| if candidate.is_dir(): | |
| artifacts.extend(sorted(candidate.iterdir())) | |
| else: | |
| artifacts.append(candidate) | |
| artifacts = [ | |
| path | |
| for path in artifacts | |
| if path.name.endswith(".whl") or path.name.endswith(".tar.gz") | |
| ] | |
| unique = list(dict.fromkeys(path.resolve() for path in artifacts)) | |
| require(bool(unique), "no wheel or sdist artifacts found") | |
| for path in unique: | |
| require(path.is_file(), f"artifact is not a file: {path}") | |
| return unique | |
| def main() -> int: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("artifacts", nargs="*", help="artifact files or directories") | |
| parser.add_argument( | |
| "--allow-partial", | |
| action="store_true", | |
| help="verify supplied artifacts without requiring the full release set", | |
| ) | |
| args = parser.parse_args() | |
| try: | |
| artifacts = collect_artifacts(args.artifacts) | |
| targets = {} | |
| sdist_count = 0 | |
| versions = set() | |
| for artifact in artifacts: | |
| if artifact.name.endswith(".whl"): | |
| target, version = verify_wheel(artifact) | |
| require(target not in targets, f"duplicate wheel target: {target}") | |
| targets[target] = artifact | |
| versions.add(version) | |
| print(f"verified {artifact.name} ({target})") | |
| else: | |
| versions.add(verify_sdist(artifact)) | |
| sdist_count += 1 | |
| print(f"verified {artifact.name} (sdist)") | |
| require(len(versions) == 1, f"artifact versions differ: {sorted(versions)}") | |
| require(sdist_count <= 1, f"expected at most one sdist, found {sdist_count}") | |
| if not args.allow_partial: | |
| missing = sorted(TARGETS - targets.keys()) | |
| require(not missing, f"missing wheel targets: {missing}") | |
| require(sdist_count == 1, f"expected one sdist, found {sdist_count}") | |
| print(f"verified {len(artifacts)} Python release artifacts") | |
| return 0 | |
| except ( | |
| KeyError, | |
| OSError, | |
| VerificationError, | |
| tarfile.TarError, | |
| tomllib.TOMLDecodeError, | |
| zipfile.BadZipFile, | |
| ) as error: | |
| print(f"Python release verification failed: {error}", file=sys.stderr) | |
| return 1 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |