diff --git a/src/auditwheel/main_repair.py b/src/auditwheel/main_repair.py index ccaeba77..c1a1fcc0 100644 --- a/src/auditwheel/main_repair.py +++ b/src/auditwheel/main_repair.py @@ -2,6 +2,7 @@ import argparse import logging +import shutil import zlib from pathlib import Path from typing import Any @@ -124,6 +125,13 @@ def configure_parser(sub_parsers: Any) -> None: # noqa: ANN401 help="Do not check for extended ISA compatibility (e.g. x86_64_v2)", default=False, ) + parser.add_argument( + "--allow-pure-python-wheel", + dest="ALLOW_PURE_PY_WHEEL", + action="store_true", + help="Allow processing of pure Python wheels (no platform-specific binaries) without error", + default=False, + ) parser.set_defaults(func=execute) @@ -151,6 +159,7 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int: wheel_filename = wheel_file.name arch = requested_architecture + is_pure_python = False try: arch = get_wheel_architecture(wheel_filename) if requested_architecture is not None and requested_architecture != arch: @@ -159,10 +168,11 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int: f"wheel targeting {requested_architecture.value}" ) parser.error(msg) - except (WheelToolsError, NonPlatformWheelError): + except (WheelToolsError, NonPlatformWheelError) as e: logger.warning( "The architecture could not be deduced from the wheel filename", ) + is_pure_python = isinstance(e, NonPlatformWheelError) try: libc = get_wheel_libc(wheel_filename) @@ -206,6 +216,12 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int: ) except NonPlatformWheelError as e: logger.info(e.message) + if is_pure_python and args.ALLOW_PURE_PY_WHEEL: + dest_fname = wheel_dir / wheel_file.name + if not dest_fname.is_file() or not dest_fname.samefile(wheel_file): + shutil.copy2(wheel_file, dest_fname) + # process next wheel + continue return 1 policies = wheel_abi.policies diff --git a/src/auditwheel/main_show.py b/src/auditwheel/main_show.py index a3a55e47..a184f044 100644 --- a/src/auditwheel/main_show.py +++ b/src/auditwheel/main_show.py @@ -21,6 +21,13 @@ def configure_parser(sub_parsers: Any) -> None: # noqa: ANN401 help="Do not check for extended ISA compatibility (e.g. x86_64_v2)", default=False, ) + p.add_argument( + "--allow-pure-python-wheel", + dest="ALLOW_PURE_PY_WHEEL", + action="store_true", + help="Allow processing of pure Python wheels (no platform-specific binaries) without error", + default=False, + ) p.set_defaults(func=execute) @@ -44,10 +51,12 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int: parser.error(f"cannot access {wheel_file}. No such file") fn = wheel_file.name + is_pure_python = False try: arch = get_wheel_architecture(fn) - except (WheelToolsError, NonPlatformWheelError): + except (WheelToolsError, NonPlatformWheelError) as e: logger.warning("The architecture could not be deduced from the wheel filename") + is_pure_python = isinstance(e, NonPlatformWheelError) arch = None try: @@ -67,6 +76,8 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int: ) except NonPlatformWheelError as e: logger.info("%s", e.message) + if is_pure_python and args.ALLOW_PURE_PY_WHEEL: + return 0 return 1 policies = winfo.policies diff --git a/tests/integration/test_nonplatform_wheel.py b/tests/integration/test_nonplatform_wheel.py index b68ed422..4d1303cc 100644 --- a/tests/integration/test_nonplatform_wheel.py +++ b/tests/integration/test_nonplatform_wheel.py @@ -1,13 +1,14 @@ from __future__ import annotations -import pathlib +import shutil import subprocess +from pathlib import Path import pytest from auditwheel.architecture import Architecture -HERE = pathlib.Path(__file__).parent.resolve() +HERE = Path(__file__).parent.resolve() @pytest.mark.parametrize("mode", ["repair", "show"]) @@ -24,15 +25,79 @@ def test_non_platform_wheel_pure(mode): assert "AttributeError" not in proc.stderr +@pytest.mark.parametrize( + ("mode", "samefile"), + [("repair", False), ("repair", True), ("show", False)], +) +def test_non_platform_wheel_pure_allow(mode: str, samefile: bool, tmp_path: Path) -> None: + wheel = HERE / "plumbum-1.6.8-py2.py3-none-any.whl" + dest_wheel = tmp_path / wheel.name + if samefile: + shutil.copy2(wheel, dest_wheel) + wheel = dest_wheel + args = ["auditwheel", mode, "--allow-pure-python-wheel", str(wheel)] + if mode == "repair": + args.extend(["-w", str(tmp_path)]) + proc = subprocess.run( + args, + stderr=subprocess.PIPE, + text=True, + check=True, + ) + assert "This does not look like a platform wheel" in proc.stderr + assert "AttributeError" not in proc.stderr + if mode == "repair": + assert dest_wheel.is_file() + assert dest_wheel.read_bytes() == wheel.read_bytes() + + +def test_non_platform_wheel_pure_allow_multiple(tmp_path: Path) -> None: + wheel = HERE / "plumbum-1.6.8-py2.py3-none-any.whl" + src_wheels = tmp_path / "src" + src_wheels.mkdir() + src_wheel1 = src_wheels / "plumbum-1.6.8-py2-none-any.whl" + src_wheel2 = src_wheels / "plumbum-1.6.8-py3-none-any.whl" + shutil.copy2(wheel, src_wheel1) + shutil.copy2(wheel, src_wheel2) + dst_wheels = tmp_path / "dst" + dst_wheel1 = dst_wheels / src_wheel1.name + dst_wheel2 = dst_wheels / src_wheel2.name + + args = [ + "auditwheel", + "repair", + "-w", + str(dst_wheels), + "--allow-pure-python-wheel", + str(src_wheel1), + str(src_wheel2), + ] + proc = subprocess.run( + args, + stderr=subprocess.PIPE, + text=True, + check=True, + ) + assert "This does not look like a platform wheel" in proc.stderr + assert "AttributeError" not in proc.stderr + for dst_wheel in [dst_wheel1, dst_wheel2]: + assert dst_wheel.is_file() + assert dst_wheel.read_bytes() == wheel.read_bytes() + + @pytest.mark.parametrize("mode", ["repair", "show"]) @pytest.mark.parametrize("arch", ["armv5l", "mips64"]) -def test_non_platform_wheel_unknown_arch(mode, arch, tmp_path): +@pytest.mark.parametrize("allow_pure_python", [True, False]) +def test_non_platform_wheel_unknown_arch(mode, arch, allow_pure_python, tmp_path): wheel_name = f"testsimple-0.0.1-cp313-cp313-linux_{arch}.whl" wheel_path = HERE / "arch-wheels" / "glibc" / wheel_name wheel_x86_64 = tmp_path / f"{wheel_path.stem}_x86_64.whl" wheel_x86_64.symlink_to(wheel_path) + args = ["auditwheel", mode, str(wheel_x86_64)] + if allow_pure_python: + args.append("--allow-pure-python-wheel") proc = subprocess.run( - ["auditwheel", mode, str(wheel_x86_64)], + args, stderr=subprocess.PIPE, text=True, check=False, @@ -48,7 +113,8 @@ def test_non_platform_wheel_unknown_arch(mode, arch, tmp_path): "arch", ["aarch64", "armv7l", "i686", "x86_64", "ppc64le", "s390x"], ) -def test_non_platform_wheel_bad_arch(mode, arch, tmp_path): +@pytest.mark.parametrize("allow_pure_python", [True, False]) +def test_non_platform_wheel_bad_arch(mode, arch, allow_pure_python, tmp_path): host_arch = Architecture.detect().value if host_arch == arch: pytest.skip("host architecture") @@ -56,8 +122,11 @@ def test_non_platform_wheel_bad_arch(mode, arch, tmp_path): wheel_path = HERE / "arch-wheels" / "glibc" / wheel_name wheel_host = tmp_path / f"{wheel_path.stem}_{host_arch}.whl" wheel_host.symlink_to(wheel_path) + args = ["auditwheel", mode, str(wheel_host)] + if allow_pure_python: + args.append("--allow-pure-python-wheel") proc = subprocess.run( - ["auditwheel", mode, str(wheel_host)], + args, stderr=subprocess.PIPE, text=True, check=False,