diff --git a/src/auditwheel/repair.py b/src/auditwheel/repair.py index 5f2fb60a..378d82cc 100644 --- a/src/auditwheel/repair.py +++ b/src/auditwheel/repair.py @@ -40,7 +40,7 @@ def repair_wheel( strip: bool, zip_compression_level: int, ) -> Path | None: - external_refs_by_fn = wheel_abi.full_external_refs + external_refs_by_fn = wheel_abi.repair_external_refs # Do not repair a pure wheel, i.e. has no external refs if not external_refs_by_fn: return None diff --git a/src/auditwheel/wheel_abi.py b/src/auditwheel/wheel_abi.py index 8e78d472..fa14e4c5 100644 --- a/src/auditwheel/wheel_abi.py +++ b/src/auditwheel/wheel_abi.py @@ -35,6 +35,7 @@ class WheelAbIInfo: policies: WheelPolicies full_external_refs: dict[Path, dict[str, ExternalReference]] + repair_external_refs: dict[Path, dict[str, ExternalReference]] overall_policy: Policy external_refs: dict[str, ExternalReference] ref_policy: Policy @@ -52,11 +53,34 @@ class WheelElfData: policies: WheelPolicies full_elftree: dict[Path, DynamicExecutable] full_external_refs: dict[Path, dict[str, ExternalReference]] + repair_external_refs: dict[Path, dict[str, ExternalReference]] versioned_symbols: dict[str, set[str]] uses_ucs2_symbols: bool uses_pyfpe_jbuf: bool +def _filter_repair_refs( + external_refs: dict[str, ExternalReference], + wheel_sonames: frozenset[str], +) -> dict[str, ExternalReference]: + """Return external refs with unresolved sonames already present in the + wheel removed. + + ``external_refs`` maps policy name to the libraries needed by one ELF. + ``wheel_sonames`` is the set of shared-library sonames already present in + the wheel. The returned mapping keeps only refs that still need repair. + """ + filtered_refs = {} + for name, external_ref in external_refs.items(): + libs = { + lib: path + for lib, path in external_ref.libs.items() + if path is not None or lib not in wheel_sonames + } + filtered_refs[name] = ExternalReference(libs, external_ref.blacklist, external_ref.policy) + return filtered_refs + + @functools.lru_cache def get_wheel_elfdata( libc: Libc | None, @@ -67,6 +91,7 @@ def get_wheel_elfdata( full_elftree: dict[Path, DynamicExecutable] = {} nonpy_elftree: dict[Path, DynamicExecutable] = {} full_external_refs: dict[Path, dict[str, ExternalReference]] = {} + repair_external_refs: dict[Path, dict[str, ExternalReference]] = {} versioned_symbols: dict[str, set[str]] = defaultdict(set) uses_ucs2_symbols = False uses_pyfpe_jbuf = False @@ -142,6 +167,7 @@ def get_wheel_elfdata( elftree, ctx.path, ) + repair_external_refs[fn] = full_external_refs[fn] else: # If the ELF is not a Python extension, it might be # included in the wheel already because auditwheel repair @@ -164,7 +190,6 @@ def get_wheel_elfdata( arch = None if architecture is None else architecture.value raise NonPlatformWheelError(arch, shared_libraries_with_invalid_machine) - # Get a list of all external libraries needed by ELFs in the wheel. needed_libs = { lib for elf in itertools.chain(full_elftree.values(), nonpy_elftree.values()) @@ -181,24 +206,27 @@ def get_wheel_elfdata( log.warning("couldn't detect wheel libc, defaulting to %s", str(libc)) policies = WheelPolicies(libc=libc, arch=architecture) - for fn, elf_tree in nonpy_elftree.items(): - # If a non-pyextension ELF file is not needed by something else - # inside the wheel, then it was not checked by the logic above and - # we should walk its elftree. - if fn.name not in needed_libs: - full_elftree[fn] = elf_tree + wheel_sonames = frozenset(fn.name for fn in itertools.chain(full_elftree, nonpy_elftree)) + + for fn, external_refs in full_external_refs.items(): + filtered_refs = _filter_repair_refs(external_refs, wheel_sonames) + if any(ref.libs or ref.blacklist for ref in filtered_refs.values()): + repair_external_refs[fn] = filtered_refs - # Even if a non-pyextension ELF file is not needed, we - # should include it as an external reference, because - # it might require additional external libraries. + for fn, elf_tree in nonpy_elftree.items(): full_external_refs[fn] = policies.lddtree_external_references( elf_tree, ctx.path, ) + if fn.name not in needed_libs: + full_elftree[fn] = elf_tree + filtered_refs = _filter_repair_refs(full_external_refs[fn], wheel_sonames) + if any(ref.libs or ref.blacklist for ref in filtered_refs.values()): + repair_external_refs[fn] = filtered_refs log.debug("full_elftree:\n%s", json.dumps(full_elftree)) log.debug( - "full_external_refs (will be repaired):\n%s", + "full_external_refs:\n%s", json.dumps(full_external_refs), ) @@ -206,6 +234,7 @@ def get_wheel_elfdata( policies, full_elftree, full_external_refs, + repair_external_refs, versioned_symbols, uses_ucs2_symbols, uses_pyfpe_jbuf, @@ -380,6 +409,7 @@ def analyze_wheel_abi( policies = data.policies elftree_by_fn = data.full_elftree external_refs_by_fn = data.full_external_refs + repair_external_refs_by_fn = data.repair_external_refs versioned_symbols = data.versioned_symbols external_refs: dict[str, ExternalReference] = { @@ -470,6 +500,7 @@ def analyze_wheel_abi( return WheelAbIInfo( policies, external_refs_by_fn, + repair_external_refs_by_fn, overall_policy, external_refs, ref_policy, diff --git a/tests/unit/test_wheel_abi.py b/tests/unit/test_wheel_abi.py index 5ad88b1b..32293297 100644 --- a/tests/unit/test_wheel_abi.py +++ b/tests/unit/test_wheel_abi.py @@ -8,6 +8,7 @@ from auditwheel import wheel_abi from auditwheel.architecture import Architecture +from auditwheel.lddtree import DynamicExecutable, Platform from auditwheel.libc import Libc from auditwheel.policy import ExternalReference, WheelPolicies @@ -66,6 +67,213 @@ def test_finds_shared_library_in_purelib( assert exec_info.value.args == (message,) + def test_filters_internal_nonpy_refs_for_repair( + self, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + class FakeCtx: + path = Path("/wheel") + + def __enter__(self): + return self + + def __exit__(self, *_args): + return None + + def iter_files(self): + return [Path("pkg/ext.so"), Path("pkg/libinner.so")] + + platform = Platform( + "", + 64, + True, + "EM_X86_64", + Architecture.x86_64, + None, + None, + ) + ext_tree = DynamicExecutable( + libc=Libc.GLIBC, + interpreter=None, + path="pkg/ext.so", + realpath=Path("/wheel/pkg/ext.so"), + platform=platform, + needed=("libinner.so",), + rpath=(), + runpath=(), + libraries={}, + ) + inner_tree = DynamicExecutable( + libc=Libc.GLIBC, + interpreter=None, + path="pkg/libinner.so", + realpath=Path("/wheel/pkg/libinner.so"), + platform=platform, + needed=(), + rpath=(), + runpath=(), + libraries={}, + ) + + external_ref = { + "manylinux_2_17_x86_64": ExternalReference( + { + "libinner.so": None, + "libc.so.6": Path("/lib64/libc.so.6"), + }, + {}, + pretend.stub(priority=80), + ), + } + fake_policies = pretend.stub( + lddtree_external_references=lambda _elftree, _wheel_path: external_ref, + ) + + monkeypatch.setattr( + wheel_abi, + "InGenericPkgCtx", + pretend.stub(__call__=lambda _wheel: FakeCtx()), + ) + monkeypatch.setattr( + wheel_abi, + "elf_file_filter", + lambda _files: [ + (Path("pkg/ext.so"), pretend.stub()), + (Path("pkg/libinner.so"), pretend.stub()), + ], + ) + monkeypatch.setattr( + wheel_abi, + "ldd", + lambda fn, **_kwargs: { + Path("pkg/ext.so"): ext_tree, + Path("pkg/libinner.so"): inner_tree, + }[fn], + ) + monkeypatch.setattr( + wheel_abi, + "elf_find_versioned_symbols", + lambda _elf: [], + ) + monkeypatch.setattr( + wheel_abi, + "elf_is_python_extension", + lambda fn, _elf: (fn == Path("pkg/ext.so"), 3), + ) + monkeypatch.setattr( + wheel_abi, + "elf_references_pyfpe_jbuf", + lambda _elf: False, + ) + monkeypatch.setattr( + wheel_abi, + "WheelPolicies", + lambda **_kwargs: fake_policies, + ) + + wheel_abi.get_wheel_elfdata.cache_clear() + result = wheel_abi.get_wheel_elfdata( + Libc.GLIBC, + Architecture.x86_64, + Path("/fakepath"), + frozenset(), + ) + + assert Path("pkg/ext.so") in result.full_external_refs + assert Path("pkg/libinner.so") in result.full_external_refs + assert Path("pkg/libinner.so") in result.repair_external_refs + assert result.repair_external_refs[Path("pkg/libinner.so")][ + "manylinux_2_17_x86_64" + ].libs == { + "libc.so.6": Path("/lib64/libc.so.6"), + } + # Ensure the Python extension itself does not list the internal + # non-Python ELF dependency as an external library to repair. + assert ( + "libinner.so" + not in result.repair_external_refs[Path("pkg/ext.so")]["manylinux_2_17_x86_64"].libs + ) + assert Path("pkg/libinner.so") not in result.full_elftree + + def test_keeps_nonpy_roots_in_analysis( + self, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + class FakeCtx: + path = Path("/wheel") + + def __enter__(self): + return self + + def __exit__(self, *_args): + return None + + def iter_files(self): + return [Path("pkg/tool.so")] + + platform = Platform( + "", + 64, + True, + "EM_X86_64", + Architecture.x86_64, + None, + None, + ) + tree = DynamicExecutable( + libc=Libc.GLIBC, + interpreter=None, + path="pkg/tool.so", + realpath=Path("/wheel/pkg/tool.so"), + platform=platform, + needed=(), + rpath=(), + runpath=(), + libraries={}, + ) + + external_ref = { + "manylinux_2_17_x86_64": ExternalReference( + {}, + {"libc.so.6": ["bad_symbol"]}, + pretend.stub(priority=80), + ), + } + fake_policies = pretend.stub( + lddtree_external_references=lambda _elftree, _wheel_path: external_ref, + ) + + monkeypatch.setattr( + wheel_abi, + "InGenericPkgCtx", + pretend.stub(__call__=lambda _wheel: FakeCtx()), + ) + monkeypatch.setattr( + wheel_abi, + "elf_file_filter", + lambda _files: [(Path("pkg/tool.so"), pretend.stub())], + ) + monkeypatch.setattr(wheel_abi, "ldd", lambda _fn, **_kwargs: tree) + monkeypatch.setattr(wheel_abi, "elf_find_versioned_symbols", lambda _elf: []) + monkeypatch.setattr(wheel_abi, "elf_is_python_extension", lambda *_args: (False, 3)) + monkeypatch.setattr(wheel_abi, "elf_references_pyfpe_jbuf", lambda _elf: False) + monkeypatch.setattr( + wheel_abi, + "WheelPolicies", + lambda **_kwargs: fake_policies, + ) + + wheel_abi.get_wheel_elfdata.cache_clear() + result = wheel_abi.get_wheel_elfdata( + Libc.GLIBC, + Architecture.x86_64, + Path("/fakepath"), + frozenset(), + ) + + assert Path("pkg/tool.so") in result.full_elftree + assert Path("pkg/tool.so") in result.repair_external_refs + def test_get_symbol_policies() -> None: policies = WheelPolicies(libc=Libc.GLIBC, arch=Architecture.x86_64)