From 318d9628c244d5a5252904ae3e1da5b98e5cc4e3 Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Fri, 15 Aug 2025 00:31:26 -0400 Subject: [PATCH 1/5] feat(repair): options to set strip level and collect debug symbols --- src/auditwheel/main_repair.py | 46 +++++++++++- src/auditwheel/repair.py | 138 +++++++++++++++++++++++++++++++--- 2 files changed, 173 insertions(+), 11 deletions(-) diff --git a/src/auditwheel/main_repair.py b/src/auditwheel/main_repair.py index 8068e437..a2750203 100644 --- a/src/auditwheel/main_repair.py +++ b/src/auditwheel/main_repair.py @@ -2,6 +2,7 @@ import argparse import logging +import warnings import zlib from pathlib import Path @@ -12,6 +13,7 @@ from auditwheel.wheeltools import get_wheel_architecture, get_wheel_libc from .policy import WheelPolicies +from .repair import StripLevel from .tools import EnvironmentDefault logger = logging.getLogger(__name__) @@ -96,9 +98,30 @@ def configure_parser(sub_parsers) -> None: # type: ignore[no-untyped-def] "--strip", dest="STRIP", action="store_true", - help="Strip symbols in the resulting wheel", + help="(DEPRECATED) Strip all symbols in the resulting wheel. Use --strip-level=all instead.", default=False, ) + parser.add_argument( + "--strip-level", + dest="STRIP_LEVEL", + choices=[level.value for level in StripLevel], + help="Strip level for symbol processing. Options: none (default), debug (remove debug symbols), unneeded (remove unneeded symbols), all (remove all symbols).", + default="none", + ) + parser.add_argument( + "--collect-debug-symbols", + dest="COLLECT_DEBUG_SYMBOLS", + action="store_true", + help="Extract debug symbols before stripping and create a zip archive.", + default=False, + ) + parser.add_argument( + "--debug-symbols-output", + dest="DEBUG_SYMBOLS_OUTPUT", + type=Path, + help="Output path for debug symbols zip file. Defaults to {wheel_name}_debug_symbols.zip", + default=None, + ) parser.add_argument( "--exclude", dest="EXCLUDE", @@ -251,6 +274,22 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int: *abis, ] + # Handle argument validation and backward compatibility + if args.STRIP and args.STRIP_LEVEL != "none": + parser.error("Cannot specify both --strip and --strip-level") + + if args.STRIP: + warnings.warn( + "The --strip option is deprecated. Use --strip-level=all instead.", + DeprecationWarning, + stacklevel=2 + ) + + if args.COLLECT_DEBUG_SYMBOLS and args.STRIP_LEVEL == "none" and not args.STRIP: + parser.error("--collect-debug-symbols requires stripping to be enabled. Use --strip-level or --strip.") + + strip_level = StripLevel(args.STRIP_LEVEL) + patcher = Patchelf() out_wheel = repair_wheel( wheel_abi, @@ -260,7 +299,10 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int: out_dir=wheel_dir, update_tags=args.UPDATE_TAGS, patcher=patcher, - strip=args.STRIP, + strip=args.STRIP if args.STRIP else None, + strip_level=strip_level, + collect_debug_symbols=args.COLLECT_DEBUG_SYMBOLS, + debug_symbols_output=args.DEBUG_SYMBOLS_OUTPUT, zip_compression_level=args.ZIP_COMPRESSION_LEVEL, ) diff --git a/src/auditwheel/repair.py b/src/auditwheel/repair.py index 6a9088c8..b6fbf923 100644 --- a/src/auditwheel/repair.py +++ b/src/auditwheel/repair.py @@ -7,7 +7,10 @@ import re import shutil import stat +import tempfile +import zipfile from collections.abc import Iterable +from enum import Enum from os.path import isabs from pathlib import Path from subprocess import check_call @@ -24,6 +27,14 @@ logger = logging.getLogger(__name__) + +class StripLevel(Enum): + """Strip levels for symbol processing.""" + NONE = "none" + DEBUG = "debug" + UNNEEDED = "unneeded" + ALL = "all" + # Copied from wheel 0.31.1 WHEEL_INFO_RE = re.compile( r"""^(?P(?P.+?)-(?P\d.*?))(-(?P\d.*?))? @@ -40,8 +51,11 @@ def repair_wheel( out_dir: Path, update_tags: bool, patcher: ElfPatcher, - strip: bool, - zip_compression_level: int, + strip: bool | None = None, + strip_level: StripLevel | None = None, + collect_debug_symbols: bool = False, + debug_symbols_output: Path | None = None, + zip_compression_level: int = 0, ) -> Path | None: external_refs_by_fn = wheel_abi.full_external_refs # Do not repair a pure wheel, i.e. has no external refs @@ -121,18 +135,124 @@ def repair_wheel( if update_tags: output_wheel = add_platforms(ctx, abis, get_replace_platforms(abis[0])) - if strip: - libs_to_strip = [path for (_, path) in soname_map.values()] - extensions = external_refs_by_fn.keys() - strip_symbols(itertools.chain(libs_to_strip, extensions)) + # Handle backward compatibility for strip parameter + if strip is not None and strip_level is not None: + raise ValueError("Cannot specify both 'strip' and 'strip_level' parameters") + + if strip is True: + strip_level = StripLevel.ALL + elif strip_level is None: + strip_level = StripLevel.NONE + + if strip_level != StripLevel.NONE: + libs_to_process = [path for (_, path) in soname_map.values()] + extensions = list(external_refs_by_fn.keys()) + all_libraries = list(itertools.chain(libs_to_process, extensions)) + + debug_symbols_zip = None + if collect_debug_symbols: + debug_symbols_zip = debug_symbols_output or ( + out_dir / f"{match.group('namever')}_debug_symbols.zip" + ) + + process_symbols( + all_libraries, + strip_level, + collect_debug_symbols, + debug_symbols_zip + ) return output_wheel +def process_symbols( + libraries: Iterable[Path], + strip_level: StripLevel, + collect_debug_symbols: bool = False, + debug_symbols_zip: Path | None = None, +) -> None: + """Process symbols in libraries according to strip level and optionally collect debug symbols.""" + libraries_list = list(libraries) + + if not libraries_list: + return + + if collect_debug_symbols and debug_symbols_zip: + _collect_debug_symbols(libraries_list, debug_symbols_zip) + + if strip_level == StripLevel.NONE: + return + + strip_args = _get_strip_args(strip_level) + for lib in libraries_list: + logger.info("Processing symbols in %s (level: %s)", lib, strip_level.value) + check_call(["strip"] + strip_args + [str(lib)]) + + +def _get_strip_args(strip_level: StripLevel) -> list[str]: + """Get strip command arguments for the given strip level.""" + if strip_level == StripLevel.DEBUG: + return ["-g"] # Remove debug symbols only + elif strip_level == StripLevel.UNNEEDED: + return ["--strip-unneeded"] # Remove unneeded symbols + elif strip_level == StripLevel.ALL: + return ["-s"] # Remove all symbols + else: + return [] + + +def _collect_debug_symbols(libraries: list[Path], debug_symbols_zip: Path) -> None: + """Extract debug symbols from libraries and create a zip archive.""" + logger.info("Collecting debug symbols to %s", debug_symbols_zip) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + debug_files = [] + + for lib in libraries: + debug_file = temp_path / f"{lib.name}.debug" + logger.debug("Extracting debug symbols from %s to %s", lib, debug_file) + + try: + # Extract debug symbols + check_call([ + "objcopy", + "--only-keep-debug", + str(lib), + str(debug_file) + ]) + + # Add gnu debuglink + check_call([ + "objcopy", + f"--add-gnu-debuglink={debug_file}", + str(lib) + ]) + + debug_files.append((lib, debug_file)) + + except Exception as e: + logger.warning("Failed to extract debug symbols from %s: %s", lib, e) + + if debug_files: + # Create zip archive with preserved directory structure + with zipfile.ZipFile(debug_symbols_zip, 'w', zipfile.ZIP_DEFLATED) as zf: + for original_lib, debug_file in debug_files: + # Preserve relative path structure in the zip + if str(original_lib).startswith("./"): + arcname = str(original_lib)[2:] + ".debug" + else: + arcname = original_lib.name + ".debug" + zf.write(debug_file, arcname) + logger.debug("Added %s to debug symbols archive", arcname) + else: + logger.warning("No debug symbols were successfully extracted") + + +# Backward compatibility function def strip_symbols(libraries: Iterable[Path]) -> None: - for lib in libraries: - logger.info("Stripping symbols from %s", lib) - check_call(["strip", "-s", lib]) + """Legacy function for backward compatibility. Use process_symbols instead.""" + process_symbols(libraries, StripLevel.ALL, False, None) def copylib(src_path: Path, dest_dir: Path, patcher: ElfPatcher) -> tuple[str, Path]: From 3a00123e809322113dc8471bd79750c1b3c19d63 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 15 Aug 2025 04:32:32 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/auditwheel/main_repair.py | 14 +++++---- src/auditwheel/repair.py | 57 ++++++++++++++--------------------- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/src/auditwheel/main_repair.py b/src/auditwheel/main_repair.py index a2750203..896fb5da 100644 --- a/src/auditwheel/main_repair.py +++ b/src/auditwheel/main_repair.py @@ -277,19 +277,21 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int: # Handle argument validation and backward compatibility if args.STRIP and args.STRIP_LEVEL != "none": parser.error("Cannot specify both --strip and --strip-level") - + if args.STRIP: warnings.warn( "The --strip option is deprecated. Use --strip-level=all instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) - + if args.COLLECT_DEBUG_SYMBOLS and args.STRIP_LEVEL == "none" and not args.STRIP: - parser.error("--collect-debug-symbols requires stripping to be enabled. Use --strip-level or --strip.") - + parser.error( + "--collect-debug-symbols requires stripping to be enabled. Use --strip-level or --strip." + ) + strip_level = StripLevel(args.STRIP_LEVEL) - + patcher = Patchelf() out_wheel = repair_wheel( wheel_abi, diff --git a/src/auditwheel/repair.py b/src/auditwheel/repair.py index b6fbf923..1e176217 100644 --- a/src/auditwheel/repair.py +++ b/src/auditwheel/repair.py @@ -30,11 +30,13 @@ class StripLevel(Enum): """Strip levels for symbol processing.""" + NONE = "none" DEBUG = "debug" UNNEEDED = "unneeded" ALL = "all" + # Copied from wheel 0.31.1 WHEEL_INFO_RE = re.compile( r"""^(?P(?P.+?)-(?P\d.*?))(-(?P\d.*?))? @@ -138,7 +140,7 @@ def repair_wheel( # Handle backward compatibility for strip parameter if strip is not None and strip_level is not None: raise ValueError("Cannot specify both 'strip' and 'strip_level' parameters") - + if strip is True: strip_level = StripLevel.ALL elif strip_level is None: @@ -148,18 +150,15 @@ def repair_wheel( libs_to_process = [path for (_, path) in soname_map.values()] extensions = list(external_refs_by_fn.keys()) all_libraries = list(itertools.chain(libs_to_process, extensions)) - + debug_symbols_zip = None if collect_debug_symbols: debug_symbols_zip = debug_symbols_output or ( out_dir / f"{match.group('namever')}_debug_symbols.zip" ) - + process_symbols( - all_libraries, - strip_level, - collect_debug_symbols, - debug_symbols_zip + all_libraries, strip_level, collect_debug_symbols, debug_symbols_zip ) return output_wheel @@ -173,16 +172,16 @@ def process_symbols( ) -> None: """Process symbols in libraries according to strip level and optionally collect debug symbols.""" libraries_list = list(libraries) - + if not libraries_list: return - + if collect_debug_symbols and debug_symbols_zip: _collect_debug_symbols(libraries_list, debug_symbols_zip) - + if strip_level == StripLevel.NONE: return - + strip_args = _get_strip_args(strip_level) for lib in libraries_list: logger.info("Processing symbols in %s (level: %s)", lib, strip_level.value) @@ -193,50 +192,40 @@ def _get_strip_args(strip_level: StripLevel) -> list[str]: """Get strip command arguments for the given strip level.""" if strip_level == StripLevel.DEBUG: return ["-g"] # Remove debug symbols only - elif strip_level == StripLevel.UNNEEDED: + if strip_level == StripLevel.UNNEEDED: return ["--strip-unneeded"] # Remove unneeded symbols - elif strip_level == StripLevel.ALL: + if strip_level == StripLevel.ALL: return ["-s"] # Remove all symbols - else: - return [] + return [] def _collect_debug_symbols(libraries: list[Path], debug_symbols_zip: Path) -> None: """Extract debug symbols from libraries and create a zip archive.""" logger.info("Collecting debug symbols to %s", debug_symbols_zip) - + with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) debug_files = [] - + for lib in libraries: debug_file = temp_path / f"{lib.name}.debug" logger.debug("Extracting debug symbols from %s to %s", lib, debug_file) - + try: # Extract debug symbols - check_call([ - "objcopy", - "--only-keep-debug", - str(lib), - str(debug_file) - ]) - + check_call(["objcopy", "--only-keep-debug", str(lib), str(debug_file)]) + # Add gnu debuglink - check_call([ - "objcopy", - f"--add-gnu-debuglink={debug_file}", - str(lib) - ]) - + check_call(["objcopy", f"--add-gnu-debuglink={debug_file}", str(lib)]) + debug_files.append((lib, debug_file)) - + except Exception as e: logger.warning("Failed to extract debug symbols from %s: %s", lib, e) - + if debug_files: # Create zip archive with preserved directory structure - with zipfile.ZipFile(debug_symbols_zip, 'w', zipfile.ZIP_DEFLATED) as zf: + with zipfile.ZipFile(debug_symbols_zip, "w", zipfile.ZIP_DEFLATED) as zf: for original_lib, debug_file in debug_files: # Preserve relative path structure in the zip if str(original_lib).startswith("./"): From 4dd3514e47a53a201a09f5edfb01e3ad0e011c00 Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Fri, 15 Aug 2025 00:41:16 -0400 Subject: [PATCH 3/5] address ruff complaints --- src/auditwheel/repair.py | 62 +++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/src/auditwheel/repair.py b/src/auditwheel/repair.py index b6fbf923..395b5a32 100644 --- a/src/auditwheel/repair.py +++ b/src/auditwheel/repair.py @@ -30,11 +30,13 @@ class StripLevel(Enum): """Strip levels for symbol processing.""" + NONE = "none" DEBUG = "debug" UNNEEDED = "unneeded" ALL = "all" + # Copied from wheel 0.31.1 WHEEL_INFO_RE = re.compile( r"""^(?P(?P.+?)-(?P\d.*?))(-(?P\d.*?))? @@ -137,8 +139,9 @@ def repair_wheel( # Handle backward compatibility for strip parameter if strip is not None and strip_level is not None: - raise ValueError("Cannot specify both 'strip' and 'strip_level' parameters") - + msg = "Cannot specify both 'strip and 'strip_level' parameters" + raise ValueError(msg) + if strip is True: strip_level = StripLevel.ALL elif strip_level is None: @@ -148,18 +151,15 @@ def repair_wheel( libs_to_process = [path for (_, path) in soname_map.values()] extensions = list(external_refs_by_fn.keys()) all_libraries = list(itertools.chain(libs_to_process, extensions)) - + debug_symbols_zip = None if collect_debug_symbols: debug_symbols_zip = debug_symbols_output or ( out_dir / f"{match.group('namever')}_debug_symbols.zip" ) - + process_symbols( - all_libraries, - strip_level, - collect_debug_symbols, - debug_symbols_zip + all_libraries, strip_level, collect_debug_symbols, debug_symbols_zip ) return output_wheel @@ -173,70 +173,60 @@ def process_symbols( ) -> None: """Process symbols in libraries according to strip level and optionally collect debug symbols.""" libraries_list = list(libraries) - + if not libraries_list: return - + if collect_debug_symbols and debug_symbols_zip: _collect_debug_symbols(libraries_list, debug_symbols_zip) - + if strip_level == StripLevel.NONE: return - + strip_args = _get_strip_args(strip_level) for lib in libraries_list: logger.info("Processing symbols in %s (level: %s)", lib, strip_level.value) - check_call(["strip"] + strip_args + [str(lib)]) + check_call(["strip", *strip_args, str(lib)]) def _get_strip_args(strip_level: StripLevel) -> list[str]: """Get strip command arguments for the given strip level.""" if strip_level == StripLevel.DEBUG: return ["-g"] # Remove debug symbols only - elif strip_level == StripLevel.UNNEEDED: + if strip_level == StripLevel.UNNEEDED: return ["--strip-unneeded"] # Remove unneeded symbols - elif strip_level == StripLevel.ALL: + if strip_level == StripLevel.ALL: return ["-s"] # Remove all symbols - else: - return [] + return [] def _collect_debug_symbols(libraries: list[Path], debug_symbols_zip: Path) -> None: """Extract debug symbols from libraries and create a zip archive.""" logger.info("Collecting debug symbols to %s", debug_symbols_zip) - + with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) debug_files = [] - + for lib in libraries: debug_file = temp_path / f"{lib.name}.debug" logger.debug("Extracting debug symbols from %s to %s", lib, debug_file) - + try: # Extract debug symbols - check_call([ - "objcopy", - "--only-keep-debug", - str(lib), - str(debug_file) - ]) - + check_call(["objcopy", "--only-keep-debug", str(lib), str(debug_file)]) + # Add gnu debuglink - check_call([ - "objcopy", - f"--add-gnu-debuglink={debug_file}", - str(lib) - ]) - + check_call(["objcopy", f"--add-gnu-debuglink={debug_file}", str(lib)]) + debug_files.append((lib, debug_file)) - + except Exception as e: logger.warning("Failed to extract debug symbols from %s: %s", lib, e) - + if debug_files: # Create zip archive with preserved directory structure - with zipfile.ZipFile(debug_symbols_zip, 'w', zipfile.ZIP_DEFLATED) as zf: + with zipfile.ZipFile(debug_symbols_zip, "w", zipfile.ZIP_DEFLATED) as zf: for original_lib, debug_file in debug_files: # Preserve relative path structure in the zip if str(original_lib).startswith("./"): From 7ad6f76550b85ee9877b6a8e847c3e1b81001ba1 Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Mon, 18 Aug 2025 09:14:42 -0400 Subject: [PATCH 4/5] add tests --- tests/unit/test_main_repair_debug_symbols.py | 252 ++++++++++++++ tests/unit/test_repair.py | 6 +- tests/unit/test_repair_debug_symbols.py | 258 +++++++++++++++ tests/unit/test_repair_wheel_integration.py | 330 +++++++++++++++++++ 4 files changed, 844 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_main_repair_debug_symbols.py create mode 100644 tests/unit/test_repair_debug_symbols.py create mode 100644 tests/unit/test_repair_wheel_integration.py diff --git a/tests/unit/test_main_repair_debug_symbols.py b/tests/unit/test_main_repair_debug_symbols.py new file mode 100644 index 00000000..2124ebdb --- /dev/null +++ b/tests/unit/test_main_repair_debug_symbols.py @@ -0,0 +1,252 @@ +from __future__ import annotations + +import argparse +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from auditwheel.main_repair import ( # type: ignore[import-not-found] + configure_parser, + execute, +) +from auditwheel.repair import StripLevel # type: ignore[import-not-found] + + +class TestMainRepairDebugSymbols: + """Test CLI argument parsing for debug symbol functionality.""" + + def test_configure_parser_new_arguments(self): + """Test that new debug symbol arguments are configured correctly.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + configure_parser(subparsers) + + # Test parsing with new arguments + args = parser.parse_args( + [ + "repair", + "--strip-level=debug", + "--collect-debug-symbols", + "--debug-symbols-output=/path/to/debug.zip", + "test.whl", + ] + ) + + assert args.STRIP_LEVEL == "debug" + assert args.COLLECT_DEBUG_SYMBOLS is True + assert Path("/path/to/debug.zip") == args.DEBUG_SYMBOLS_OUTPUT + + def test_strip_level_choices(self): + """Test that strip-level accepts all valid choices.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + configure_parser(subparsers) + + for level in StripLevel: + args = parser.parse_args( + ["repair", f"--strip-level={level.value}", "test.whl"] + ) + assert level.value == args.STRIP_LEVEL + + def test_strip_level_invalid_choice(self): + """Test that invalid strip-level raises error.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + configure_parser(subparsers) + + with pytest.raises(SystemExit): + parser.parse_args(["repair", "--strip-level=invalid", "test.whl"]) + + def test_default_values(self): + """Test default values for new arguments.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + configure_parser(subparsers) + + args = parser.parse_args(["repair", "test.whl"]) + + assert args.STRIP_LEVEL == "none" + assert args.COLLECT_DEBUG_SYMBOLS is False + assert args.DEBUG_SYMBOLS_OUTPUT is None + assert args.STRIP is False # Backward compatibility + + def test_deprecated_strip_help_text(self): + """Test that deprecated strip option shows deprecation in help.""" + # Test by creating a standalone parser with just the repair subcommand + main_parser = argparse.ArgumentParser() + subparsers = main_parser.add_subparsers() + configure_parser(subparsers) + + # Alternative: Test that the deprecated argument is still accepted + # and verify the help text is configured correctly by parsing arguments + args = main_parser.parse_args(["repair", "--strip", "test.whl"]) + assert args.STRIP is True + + # The deprecation text is in the help, but we'll just test functionality + # since accessing subparser internals is brittle + + +class TestMainRepairExecute: + """Test the execute function with debug symbol arguments.""" + + @patch("auditwheel.main_repair.repair_wheel") + @patch("auditwheel.main_repair.analyze_wheel_abi") + @patch("auditwheel.main_repair.Patchelf") + def test_execute_with_strip_level_debug(self, mock_analyze, mock_repair): + """Test execute function with strip-level=debug.""" + + # Mock objects + mock_wheel_abi = MagicMock() + mock_wheel_abi.full_external_refs = {"ext.so": {}} + mock_wheel_abi.policies = MagicMock() + mock_wheel_abi.policies.lowest.name = "manylinux_2_17_x86_64" + mock_wheel_abi.overall_policy = mock_wheel_abi.policies.lowest + mock_wheel_abi.sym_policy = mock_wheel_abi.policies.lowest + mock_wheel_abi.ucs_policy = mock_wheel_abi.policies.lowest + mock_wheel_abi.blacklist_policy = mock_wheel_abi.policies.lowest + mock_wheel_abi.machine_policy = mock_wheel_abi.policies.lowest + + mock_analyze.return_value = mock_wheel_abi + mock_repair.return_value = Path("output.whl") + + # Create mock args + args = MagicMock() + args.WHEEL_FILE = [Path("test.whl")] + args.WHEEL_DIR = Path("wheelhouse") + args.LIB_SDIR = ".libs" + args.PLAT = "auto" + args.UPDATE_TAGS = True + args.ONLY_PLAT = False + args.EXCLUDE = [] + args.DISABLE_ISA_EXT_CHECK = False + args.ZIP_COMPRESSION_LEVEL = 6 + args.STRIP = False + args.STRIP_LEVEL = "debug" + args.COLLECT_DEBUG_SYMBOLS = True + args.DEBUG_SYMBOLS_OUTPUT = Path("debug.zip") + + parser = MagicMock() + + with ( + patch("auditwheel.main_repair.Path.mkdir"), + patch("auditwheel.main_repair.Path.exists", return_value=True), + patch("auditwheel.main_repair.Path.is_file", return_value=True), + patch("auditwheel.main_repair.get_wheel_architecture"), + patch("auditwheel.main_repair.get_wheel_libc"), + ): + result = execute(args, parser) + + assert result == 0 + mock_repair.assert_called_once() + + # Verify repair_wheel was called with correct arguments + call_args = mock_repair.call_args + assert ( + call_args[1]["strip"] is None + ) # strip should be None when using strip_level + assert call_args[1]["strip_level"] == StripLevel.DEBUG + assert call_args[1]["collect_debug_symbols"] is True + assert call_args[1]["debug_symbols_output"] == Path("debug.zip") + + @patch("auditwheel.main_repair.repair_wheel") + @patch("auditwheel.main_repair.analyze_wheel_abi") + @patch("auditwheel.main_repair.Patchelf") + def test_execute_with_deprecated_strip(self, mock_analyze, mock_repair): + """Test execute function with deprecated --strip flag.""" + + # Mock objects + mock_wheel_abi = MagicMock() + mock_wheel_abi.full_external_refs = {"ext.so": {}} + mock_wheel_abi.policies = MagicMock() + mock_wheel_abi.policies.lowest.name = "manylinux_2_17_x86_64" + mock_wheel_abi.overall_policy = mock_wheel_abi.policies.lowest + mock_wheel_abi.sym_policy = mock_wheel_abi.policies.lowest + mock_wheel_abi.ucs_policy = mock_wheel_abi.policies.lowest + mock_wheel_abi.blacklist_policy = mock_wheel_abi.policies.lowest + mock_wheel_abi.machine_policy = mock_wheel_abi.policies.lowest + + mock_analyze.return_value = mock_wheel_abi + mock_repair.return_value = Path("output.whl") + + # Create mock args + args = MagicMock() + args.WHEEL_FILE = [Path("test.whl")] + args.WHEEL_DIR = Path("wheelhouse") + args.LIB_SDIR = ".libs" + args.PLAT = "auto" + args.UPDATE_TAGS = True + args.ONLY_PLAT = False + args.EXCLUDE = [] + args.DISABLE_ISA_EXT_CHECK = False + args.ZIP_COMPRESSION_LEVEL = 6 + args.STRIP = True + args.STRIP_LEVEL = "none" + args.COLLECT_DEBUG_SYMBOLS = False + args.DEBUG_SYMBOLS_OUTPUT = None + + parser = MagicMock() + + with ( + patch("auditwheel.main_repair.Path.mkdir"), + patch("auditwheel.main_repair.Path.exists", return_value=True), + patch("auditwheel.main_repair.Path.is_file", return_value=True), + patch("auditwheel.main_repair.get_wheel_architecture"), + patch("auditwheel.main_repair.get_wheel_libc"), + patch("auditwheel.main_repair.warnings.warn") as mock_warn, + ): + result = execute(args, parser) + + assert result == 0 + mock_repair.assert_called_once() + + # Verify deprecation warning was issued + mock_warn.assert_called_once_with( + "The --strip option is deprecated. Use --strip-level=all instead.", + DeprecationWarning, + stacklevel=2, + ) + + # Verify repair_wheel was called with correct arguments + call_args = mock_repair.call_args + assert call_args[1]["strip"] is True + assert call_args[1]["strip_level"] == StripLevel("none") + + def test_execute_conflicting_strip_arguments(self): + """Test that conflicting strip arguments cause an error.""" + + # Create mock args with conflicting strip options + args = MagicMock() + args.WHEEL_FILE = [Path("test.whl")] + args.STRIP = True + args.STRIP_LEVEL = "debug" # Conflicts with STRIP=True + + parser = MagicMock() + + with patch("auditwheel.main_repair.Path.is_file", return_value=True): + execute(args, parser) + + # Should call parser.error + parser.error.assert_called_once_with( + "Cannot specify both --strip and --strip-level" + ) + + def test_execute_collect_debug_without_stripping(self): + """Test that collect-debug-symbols without stripping causes an error.""" + + # Create mock args + args = MagicMock() + args.WHEEL_FILE = [Path("test.whl")] + args.STRIP = False + args.STRIP_LEVEL = "none" + args.COLLECT_DEBUG_SYMBOLS = True + + parser = MagicMock() + + with patch("auditwheel.main_repair.Path.is_file", return_value=True): + execute(args, parser) + + # Should call parser.error + parser.error.assert_called_once_with( + "--collect-debug-symbols requires stripping to be enabled. Use --strip-level or --strip." + ) diff --git a/tests/unit/test_repair.py b/tests/unit/test_repair.py index 5e0bce36..7866a47f 100644 --- a/tests/unit/test_repair.py +++ b/tests/unit/test_repair.py @@ -3,8 +3,10 @@ from pathlib import Path from unittest.mock import call, patch -from auditwheel.patcher import Patchelf -from auditwheel.repair import append_rpath_within_wheel +from auditwheel.patcher import Patchelf # type: ignore[import-not-found] +from auditwheel.repair import ( # type: ignore[import-not-found] + append_rpath_within_wheel, +) @patch("auditwheel.patcher._verify_patchelf") diff --git a/tests/unit/test_repair_debug_symbols.py b/tests/unit/test_repair_debug_symbols.py new file mode 100644 index 00000000..fd58fbc5 --- /dev/null +++ b/tests/unit/test_repair_debug_symbols.py @@ -0,0 +1,258 @@ +from __future__ import annotations + +import zipfile +from pathlib import Path +from unittest.mock import call, patch + +import pytest + +from auditwheel.repair import ( # type: ignore[import-not-found] + StripLevel, + _collect_debug_symbols, + _get_strip_args, + process_symbols, + strip_symbols, +) + + +class TestStripLevel: + """Test the StripLevel enum.""" + + def test_strip_level_values(self): + """Test that StripLevel enum has correct values.""" + assert StripLevel.NONE.value == "none" + assert StripLevel.DEBUG.value == "debug" + assert StripLevel.UNNEEDED.value == "unneeded" + assert StripLevel.ALL.value == "all" + + def test_strip_level_from_string(self): + """Test creating StripLevel from string values.""" + assert StripLevel("none") == StripLevel.NONE + assert StripLevel("debug") == StripLevel.DEBUG + assert StripLevel("unneeded") == StripLevel.UNNEEDED + assert StripLevel("all") == StripLevel.ALL + + def test_strip_level_invalid_value(self): + """Test that invalid values raise ValueError.""" + with pytest.raises(ValueError, match="'invalid' is not a valid StripLevel"): + StripLevel("invalid") + + +class TestGetStripArgs: + """Test the _get_strip_args function.""" + + def test_get_strip_args_none(self): + """Test strip args for NONE level.""" + args = _get_strip_args(StripLevel.NONE) + assert args == [] + + def test_get_strip_args_debug(self): + """Test strip args for DEBUG level.""" + args = _get_strip_args(StripLevel.DEBUG) + assert args == ["-g"] + + def test_get_strip_args_unneeded(self): + """Test strip args for UNNEEDED level.""" + args = _get_strip_args(StripLevel.UNNEEDED) + assert args == ["--strip-unneeded"] + + def test_get_strip_args_all(self): + """Test strip args for ALL level.""" + args = _get_strip_args(StripLevel.ALL) + assert args == ["-s"] + + +@patch("auditwheel.repair.check_call") +class TestProcessSymbols: + """Test the process_symbols function.""" + + def test_process_symbols_none_level(self, mock_check_call): + """Test that NONE level doesn't call strip.""" + libraries = [Path("lib1.so"), Path("lib2.so")] + process_symbols(libraries, StripLevel.NONE) + mock_check_call.assert_not_called() + + def test_process_symbols_debug_level(self, mock_check_call): + """Test that DEBUG level calls strip with -g.""" + libraries = [Path("lib1.so"), Path("lib2.so")] + process_symbols(libraries, StripLevel.DEBUG) + + expected_calls = [ + call(["strip", "-g", "lib1.so"]), + call(["strip", "-g", "lib2.so"]), + ] + mock_check_call.assert_has_calls(expected_calls) + + def test_process_symbols_unneeded_level(self, mock_check_call): + """Test that UNNEEDED level calls strip with --strip-unneeded.""" + libraries = [Path("lib1.so")] + process_symbols(libraries, StripLevel.UNNEEDED) + + mock_check_call.assert_called_once_with( + ["strip", "--strip-unneeded", "lib1.so"] + ) + + def test_process_symbols_all_level(self, mock_check_call): + """Test that ALL level calls strip with -s.""" + libraries = [Path("lib1.so")] + process_symbols(libraries, StripLevel.ALL) + + mock_check_call.assert_called_once_with(["strip", "-s", "lib1.so"]) + + def test_process_symbols_empty_libraries(self, mock_check_call): + """Test that empty library list doesn't call strip.""" + process_symbols([], StripLevel.ALL) + mock_check_call.assert_not_called() + + @patch("auditwheel.repair._collect_debug_symbols") + def test_process_symbols_with_debug_collection(self, mock_collect, mock_check_call): + """Test that debug symbols are collected when requested.""" + libraries = [Path("lib1.so")] + debug_zip = Path("debug.zip") + + process_symbols( + libraries, + StripLevel.DEBUG, + collect_debug_symbols=True, + debug_symbols_zip=debug_zip, + ) + + mock_collect.assert_called_once_with(libraries, debug_zip) + mock_check_call.assert_called_once_with(["strip", "-g", "lib1.so"]) + + +@patch("auditwheel.repair.check_call") +class TestCollectDebugSymbols: + """Test the _collect_debug_symbols function.""" + + @patch("auditwheel.repair.zipfile.ZipFile") + @patch("auditwheel.repair.tempfile.TemporaryDirectory") + def test_collect_debug_symbols_success( + self, mock_tempdir, mock_zipfile, mock_check_call + ): + """Test successful debug symbol collection.""" + # Setup mocks + temp_path = Path("/tmp/test") + mock_tempdir.return_value.__enter__.return_value = str(temp_path) + + mock_zip_instance = mock_zipfile.return_value.__enter__.return_value + + libraries = [Path("lib1.so"), Path("lib2.so")] + debug_zip = Path("debug.zip") + + _collect_debug_symbols(libraries, debug_zip) + + # Verify objcopy calls for each library + expected_objcopy_calls = [ + call( + [ + "objcopy", + "--only-keep-debug", + "lib1.so", + str(temp_path / "lib1.so.debug"), + ] + ), + call( + [ + "objcopy", + f"--add-gnu-debuglink={temp_path / 'lib1.so.debug'}", + "lib1.so", + ] + ), + call( + [ + "objcopy", + "--only-keep-debug", + "lib2.so", + str(temp_path / "lib2.so.debug"), + ] + ), + call( + [ + "objcopy", + f"--add-gnu-debuglink={temp_path / 'lib2.so.debug'}", + "lib2.so", + ] + ), + ] + mock_check_call.assert_has_calls(expected_objcopy_calls) + + # Verify zip file creation + mock_zipfile.assert_called_once_with(debug_zip, "w", zipfile.ZIP_DEFLATED) + + # Verify files added to zip + expected_zip_calls = [ + call(temp_path / "lib1.so.debug", "lib1.so.debug"), + call(temp_path / "lib2.so.debug", "lib2.so.debug"), + ] + mock_zip_instance.write.assert_has_calls(expected_zip_calls, any_order=True) + + @patch("auditwheel.repair.zipfile.ZipFile") + @patch("auditwheel.repair.tempfile.TemporaryDirectory") + def test_collect_debug_symbols_objcopy_failure( + self, mock_tempdir, mock_zipfile, mock_check_call + ): + """Test handling objcopy failure.""" + temp_path = Path("/tmp/test") + mock_tempdir.return_value.__enter__.return_value = str(temp_path) + + mock_zip_instance = mock_zipfile.return_value.__enter__.return_value + + # Make objcopy fail for first library + def side_effect(args): + if "lib1.so" in args[2]: + msg = "objcopy failed" + raise RuntimeError(msg) + + mock_check_call.side_effect = side_effect + + libraries = [Path("lib1.so"), Path("lib2.so")] + debug_zip = Path("debug.zip") + + # Should not raise exception + _collect_debug_symbols(libraries, debug_zip) + + # Only lib2.so should be in the zip + mock_zip_instance.write.assert_called_once_with( + temp_path / "lib2.so.debug", "lib2.so.debug" + ) + + def test_collect_debug_symbols_empty_libraries(self, mock_check_call): + """Test with empty library list.""" + _collect_debug_symbols([], Path("debug.zip")) + mock_check_call.assert_not_called() + + @patch("auditwheel.repair.zipfile.ZipFile") + @patch("auditwheel.repair.tempfile.TemporaryDirectory") + def test_collect_debug_symbols_relative_path(self, mock_tempdir, mock_zipfile): + """Test that relative paths are handled correctly in zip archive.""" + temp_path = Path("/tmp/test") + mock_tempdir.return_value.__enter__.return_value = str(temp_path) + + mock_zip_instance = mock_zipfile.return_value.__enter__.return_value + + libraries = [Path("./lib1.so")] # Relative path + debug_zip = Path("debug.zip") + + _collect_debug_symbols(libraries, debug_zip) + + # Should strip the "./" prefix in the zip archive + mock_zip_instance.write.assert_called_once_with( + temp_path / "lib1.so.debug", "lib1.so.debug" + ) + + +@patch("auditwheel.repair.check_call") +class TestStripSymbolsBackwardCompatibility: + """Test the backward compatibility strip_symbols function.""" + + def test_strip_symbols_calls_process_symbols(self, mock_check_call): + """Test that strip_symbols calls process_symbols with ALL level.""" + libraries = [Path("lib1.so"), Path("lib2.so")] + strip_symbols(libraries) + + expected_calls = [ + call(["strip", "-s", "lib1.so"]), + call(["strip", "-s", "lib2.so"]), + ] + mock_check_call.assert_has_calls(expected_calls) diff --git a/tests/unit/test_repair_wheel_integration.py b/tests/unit/test_repair_wheel_integration.py new file mode 100644 index 00000000..3b5845cd --- /dev/null +++ b/tests/unit/test_repair_wheel_integration.py @@ -0,0 +1,330 @@ +from __future__ import annotations + +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from auditwheel.repair import StripLevel, repair_wheel # type: ignore[import-not-found] + + +class TestRepairWheelIntegration: + """Integration tests for repair_wheel with debug symbol functionality.""" + + @patch("auditwheel.repair.process_symbols") + @patch("auditwheel.repair.InWheelCtx") + def test_repair_wheel_with_strip_level_debug( + self, mock_wheel_ctx, mock_process_symbols + ): + """Test repair_wheel with strip_level=DEBUG.""" + # Mock wheel context + mock_ctx = MagicMock() + mock_wheel_ctx.return_value.__enter__.return_value = mock_ctx + mock_ctx.out_wheel = Path("output.whl") + mock_ctx.name = Path("wheel_name") + + # Mock wheel ABI + mock_wheel_abi = MagicMock() + mock_wheel_abi.full_external_refs = { + Path("ext.so"): {"manylinux_2_17_x86_64": MagicMock(libs={})} + } + + # Mock patcher + mock_patcher = MagicMock() + + with tempfile.TemporaryDirectory() as temp_dir: + out_dir = Path(temp_dir) + + result = repair_wheel( + wheel_abi=mock_wheel_abi, + wheel_path=Path("package-1.0-py3-none-linux_x86_64.whl"), + abis=["manylinux_2_17_x86_64"], + lib_sdir=".libs", + out_dir=out_dir, + update_tags=False, + patcher=mock_patcher, + strip_level=StripLevel.DEBUG, + collect_debug_symbols=False, + debug_symbols_output=None, + ) + + # Should call process_symbols with correct arguments + mock_process_symbols.assert_called_once() + args, kwargs = mock_process_symbols.call_args + libraries, strip_level, collect_debug_symbols, debug_symbols_zip = args + + assert list(libraries) == [Path("ext.so")] + assert strip_level == StripLevel.DEBUG + assert collect_debug_symbols is False + assert debug_symbols_zip is None + + assert result == Path("output.whl") + + @patch("auditwheel.repair.process_symbols") + @patch("auditwheel.repair.InWheelCtx") + @patch("auditwheel.repair.WHEEL_INFO_RE") + def test_repair_wheel_with_debug_collection( + self, mock_wheel_info, mock_wheel_ctx, mock_process_symbols + ): + """Test repair_wheel with debug symbol collection.""" + # Mock wheel filename parsing + mock_match = MagicMock() + mock_match.group.return_value = "package-1.0.0" + mock_wheel_info.return_value = mock_match + + # Mock wheel context + mock_ctx = MagicMock() + mock_wheel_ctx.return_value.__enter__.return_value = mock_ctx + mock_ctx.out_wheel = Path("output.whl") + mock_ctx.name = Path("wheel_name") + + # Mock wheel ABI + mock_wheel_abi = MagicMock() + mock_wheel_abi.full_external_refs = { + Path("ext.so"): {"manylinux_2_17_x86_64": MagicMock(libs={})} + } + + # Mock patcher + mock_patcher = MagicMock() + + with tempfile.TemporaryDirectory() as temp_dir: + out_dir = Path(temp_dir) + + repair_wheel( + wheel_abi=mock_wheel_abi, + wheel_path=Path("package-1.0-py3-none-linux_x86_64.whl"), + abis=["manylinux_2_17_x86_64"], + lib_sdir=".libs", + out_dir=out_dir, + update_tags=False, + patcher=mock_patcher, + strip_level=StripLevel.DEBUG, + collect_debug_symbols=True, + debug_symbols_output=None, # Should use default + ) + + # Should call process_symbols with debug collection enabled + mock_process_symbols.assert_called_once() + args, kwargs = mock_process_symbols.call_args + libraries, strip_level, collect_debug_symbols, debug_symbols_zip = args + + assert list(libraries) == [Path("ext.so")] + assert strip_level == StripLevel.DEBUG + assert collect_debug_symbols is True + assert debug_symbols_zip == out_dir / "package-1.0.0_debug_symbols.zip" + + @patch("auditwheel.repair.process_symbols") + @patch("auditwheel.repair.InWheelCtx") + def test_repair_wheel_with_custom_debug_output( + self, mock_wheel_ctx, mock_process_symbols + ): + """Test repair_wheel with custom debug symbols output path.""" + # Mock wheel context + mock_ctx = MagicMock() + mock_wheel_ctx.return_value.__enter__.return_value = mock_ctx + mock_ctx.out_wheel = Path("output.whl") + mock_ctx.name = Path("wheel_name") + + # Mock wheel ABI + mock_wheel_abi = MagicMock() + mock_wheel_abi.full_external_refs = { + Path("ext.so"): {"manylinux_2_17_x86_64": MagicMock(libs={})} + } + + # Mock patcher + mock_patcher = MagicMock() + custom_debug_path = Path("/custom/debug.zip") + + with tempfile.TemporaryDirectory() as temp_dir: + out_dir = Path(temp_dir) + + repair_wheel( + wheel_abi=mock_wheel_abi, + wheel_path=Path("package-1.0-py3-none-linux_x86_64.whl"), + abis=["manylinux_2_17_x86_64"], + lib_sdir=".libs", + out_dir=out_dir, + update_tags=False, + patcher=mock_patcher, + strip_level=StripLevel.DEBUG, + collect_debug_symbols=True, + debug_symbols_output=custom_debug_path, + ) + + # Should use custom debug symbols path + mock_process_symbols.assert_called_once() + args, kwargs = mock_process_symbols.call_args + libraries, strip_level, collect_debug_symbols, debug_symbols_zip = args + + assert debug_symbols_zip == custom_debug_path + + @patch("auditwheel.repair.process_symbols") + @patch("auditwheel.repair.InWheelCtx") + def test_repair_wheel_backward_compatibility_strip_true( + self, mock_wheel_ctx, mock_process_symbols + ): + """Test repair_wheel backward compatibility with strip=True.""" + # Mock wheel context + mock_ctx = MagicMock() + mock_wheel_ctx.return_value.__enter__.return_value = mock_ctx + mock_ctx.out_wheel = Path("output.whl") + mock_ctx.name = Path("wheel_name") + + # Mock wheel ABI + mock_wheel_abi = MagicMock() + mock_wheel_abi.full_external_refs = { + Path("ext.so"): {"manylinux_2_17_x86_64": MagicMock(libs={})} + } + + # Mock patcher + mock_patcher = MagicMock() + + with tempfile.TemporaryDirectory() as temp_dir: + out_dir = Path(temp_dir) + + repair_wheel( + wheel_abi=mock_wheel_abi, + wheel_path=Path("package-1.0-py3-none-linux_x86_64.whl"), + abis=["manylinux_2_17_x86_64"], + lib_sdir=".libs", + out_dir=out_dir, + update_tags=False, + patcher=mock_patcher, + strip=True, # Old parameter + ) + + # Should map strip=True to StripLevel.ALL + mock_process_symbols.assert_called_once() + args, kwargs = mock_process_symbols.call_args + libraries, strip_level, collect_debug_symbols, debug_symbols_zip = args + + assert strip_level == StripLevel.ALL + + def test_repair_wheel_conflicting_strip_parameters(self): + """Test repair_wheel with conflicting strip parameters raises error.""" + mock_wheel_abi = MagicMock() + mock_wheel_abi.full_external_refs = {} # Pure wheel + mock_patcher = MagicMock() + + with pytest.raises( + ValueError, match="Cannot specify both 'strip' and 'strip_level' parameters" + ): + repair_wheel( + wheel_abi=mock_wheel_abi, + wheel_path=Path("package-1.0-py3-none-linux_x86_64.whl"), + abis=["manylinux_2_17_x86_64"], + lib_sdir=".libs", + out_dir=Path("/tmp"), + update_tags=False, + patcher=mock_patcher, + strip=True, + strip_level=StripLevel.DEBUG, # Conflicting parameters + ) + + @patch("auditwheel.repair.process_symbols") + @patch("auditwheel.repair.InWheelCtx") + def test_repair_wheel_no_stripping(self, mock_wheel_ctx, mock_process_symbols): + """Test repair_wheel with no stripping (strip_level=NONE).""" + # Mock wheel context + mock_ctx = MagicMock() + mock_wheel_ctx.return_value.__enter__.return_value = mock_ctx + mock_ctx.out_wheel = Path("output.whl") + mock_ctx.name = Path("wheel_name") + + # Mock wheel ABI + mock_wheel_abi = MagicMock() + mock_wheel_abi.full_external_refs = { + Path("ext.so"): {"manylinux_2_17_x86_64": MagicMock(libs={})} + } + + # Mock patcher + mock_patcher = MagicMock() + + with tempfile.TemporaryDirectory() as temp_dir: + out_dir = Path(temp_dir) + + repair_wheel( + wheel_abi=mock_wheel_abi, + wheel_path=Path("package-1.0-py3-none-linux_x86_64.whl"), + abis=["manylinux_2_17_x86_64"], + lib_sdir=".libs", + out_dir=out_dir, + update_tags=False, + patcher=mock_patcher, + strip_level=StripLevel.NONE, + ) + + # Should not call process_symbols when strip_level is NONE + mock_process_symbols.assert_not_called() + + def test_repair_wheel_pure_wheel_no_processing(self): + """Test repair_wheel returns None for pure wheels (no external refs).""" + mock_wheel_abi = MagicMock() + mock_wheel_abi.full_external_refs = {} # Pure wheel + mock_patcher = MagicMock() + + result = repair_wheel( + wheel_abi=mock_wheel_abi, + wheel_path=Path("package-1.0-py3-none-linux_x86_64.whl"), + abis=["manylinux_2_17_x86_64"], + lib_sdir=".libs", + out_dir=Path("/tmp"), + update_tags=False, + patcher=mock_patcher, + strip_level=StripLevel.DEBUG, + ) + + assert result is None + + @patch("auditwheel.repair.process_symbols") + @patch("auditwheel.repair.InWheelCtx") + @patch("auditwheel.repair.copylib") + def test_repair_wheel_with_external_libraries( + self, mock_copylib, mock_wheel_ctx, mock_process_symbols + ): + """Test repair_wheel processes both external libs and extensions.""" + # Mock wheel context + mock_ctx = MagicMock() + mock_wheel_ctx.return_value.__enter__.return_value = mock_ctx + mock_ctx.out_wheel = Path("output.whl") + mock_ctx.name = Path("wheel_name") + + # Mock copylib to return library paths + mock_copylib.return_value = ("libfoo.so.1", Path("dest/libfoo.so.1")) + + # Mock wheel ABI with external libraries + mock_libs = MagicMock() + mock_libs.libs = {"libfoo.so.1": Path("/system/libfoo.so.1")} + mock_wheel_abi = MagicMock() + mock_wheel_abi.full_external_refs = { + Path("ext.so"): {"manylinux_2_17_x86_64": mock_libs} + } + + # Mock patcher + mock_patcher = MagicMock() + + with tempfile.TemporaryDirectory() as temp_dir: + out_dir = Path(temp_dir) + + repair_wheel( + wheel_abi=mock_wheel_abi, + wheel_path=Path("package-1.0-py3-none-linux_x86_64.whl"), + abis=["manylinux_2_17_x86_64"], + lib_sdir=".libs", + out_dir=out_dir, + update_tags=False, + patcher=mock_patcher, + strip_level=StripLevel.DEBUG, + ) + + # Should process both external libraries and extensions + mock_process_symbols.assert_called_once() + args, kwargs = mock_process_symbols.call_args + libraries, strip_level, collect_debug_symbols, debug_symbols_zip = args + + # Should include both the copied library and the extension + library_paths = list(libraries) + assert Path("dest/libfoo.so.1") in library_paths + assert Path("ext.so") in library_paths + assert len(library_paths) == 2 From 36e3d43429daf70c224f71c7be5110ed8e9a4262 Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Mon, 18 Aug 2025 09:26:18 -0400 Subject: [PATCH 5/5] Address pre-commit errors --- tests/unit/test_main_repair_debug_symbols.py | 4 ++-- tests/unit/test_repair.py | 4 ++-- tests/unit/test_repair_debug_symbols.py | 2 +- tests/unit/test_repair_wheel_integration.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_main_repair_debug_symbols.py b/tests/unit/test_main_repair_debug_symbols.py index 2124ebdb..4eeed003 100644 --- a/tests/unit/test_main_repair_debug_symbols.py +++ b/tests/unit/test_main_repair_debug_symbols.py @@ -6,11 +6,11 @@ import pytest -from auditwheel.main_repair import ( # type: ignore[import-not-found] +from auditwheel.main_repair import ( configure_parser, execute, ) -from auditwheel.repair import StripLevel # type: ignore[import-not-found] +from auditwheel.repair import StripLevel class TestMainRepairDebugSymbols: diff --git a/tests/unit/test_repair.py b/tests/unit/test_repair.py index 7866a47f..895f2c87 100644 --- a/tests/unit/test_repair.py +++ b/tests/unit/test_repair.py @@ -3,8 +3,8 @@ from pathlib import Path from unittest.mock import call, patch -from auditwheel.patcher import Patchelf # type: ignore[import-not-found] -from auditwheel.repair import ( # type: ignore[import-not-found] +from auditwheel.patcher import Patchelf +from auditwheel.repair import ( append_rpath_within_wheel, ) diff --git a/tests/unit/test_repair_debug_symbols.py b/tests/unit/test_repair_debug_symbols.py index fd58fbc5..3acc4b63 100644 --- a/tests/unit/test_repair_debug_symbols.py +++ b/tests/unit/test_repair_debug_symbols.py @@ -6,7 +6,7 @@ import pytest -from auditwheel.repair import ( # type: ignore[import-not-found] +from auditwheel.repair import ( StripLevel, _collect_debug_symbols, _get_strip_args, diff --git a/tests/unit/test_repair_wheel_integration.py b/tests/unit/test_repair_wheel_integration.py index 3b5845cd..7c0fdeee 100644 --- a/tests/unit/test_repair_wheel_integration.py +++ b/tests/unit/test_repair_wheel_integration.py @@ -6,7 +6,7 @@ import pytest -from auditwheel.repair import StripLevel, repair_wheel # type: ignore[import-not-found] +from auditwheel.repair import StripLevel, repair_wheel class TestRepairWheelIntegration: