From 4e91f4d5e48db62c944d852e9e0fa2e2a77b4104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20Kiss=20Koll=C3=A1r?= Date: Sat, 21 Mar 2026 20:42:45 +0000 Subject: [PATCH 1/4] Skip internal non-Python references Non-extension shared objects that are already reached through a wheel ELF are being analysed again as standalone repair roots. This drops the load context from the parent ELF and can turn vendored wheel libraries into unresolved external refs. To fix this, only collect external refs for non-extension ELFs that are true roots. --- src/auditwheel/repair.py | 2 +- src/auditwheel/wheel_abi.py | 55 ++++++++-- tests/unit/test_wheel_abi.py | 200 +++++++++++++++++++++++++++++++++++ 3 files changed, 245 insertions(+), 12 deletions(-) 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..b495b374 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,29 @@ 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 +236,7 @@ def get_wheel_elfdata( policies, full_elftree, full_external_refs, + repair_external_refs, versioned_symbols, uses_ucs2_symbols, uses_pyfpe_jbuf, @@ -380,6 +411,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 +502,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..c269939b 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,205 @@ 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"), + } + 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) From 116209c68399c8ae458f2a71e26b9b0c12f8b50f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:47:38 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/auditwheel/wheel_abi.py | 4 +--- tests/unit/test_wheel_abi.py | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/auditwheel/wheel_abi.py b/src/auditwheel/wheel_abi.py index b495b374..fa14e4c5 100644 --- a/src/auditwheel/wheel_abi.py +++ b/src/auditwheel/wheel_abi.py @@ -206,9 +206,7 @@ def get_wheel_elfdata( log.warning("couldn't detect wheel libc, defaulting to %s", str(libc)) policies = WheelPolicies(libc=libc, arch=architecture) - wheel_sonames = frozenset( - fn.name for fn in itertools.chain(full_elftree, nonpy_elftree) - ) + 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) diff --git a/tests/unit/test_wheel_abi.py b/tests/unit/test_wheel_abi.py index c269939b..7cce9ec9 100644 --- a/tests/unit/test_wheel_abi.py +++ b/tests/unit/test_wheel_abi.py @@ -182,7 +182,9 @@ def iter_files(self): 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 == { + assert result.repair_external_refs[Path("pkg/libinner.so")][ + "manylinux_2_17_x86_64" + ].libs == { "libc.so.6": Path("/lib64/libc.so.6"), } assert Path("pkg/libinner.so") not in result.full_elftree From e1d80949a1da638651310b9190d23381ae982289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asif=20Saif=20Uddin=20=7B=22Auvi=22=3A=22=E0=A6=85?= =?UTF-8?q?=E0=A6=AD=E0=A6=BF=22=7D?= Date: Mon, 30 Mar 2026 11:11:11 +0600 Subject: [PATCH 3/4] Update tests/unit/test_wheel_abi.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/unit/test_wheel_abi.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/test_wheel_abi.py b/tests/unit/test_wheel_abi.py index 7cce9ec9..4e33ff6b 100644 --- a/tests/unit/test_wheel_abi.py +++ b/tests/unit/test_wheel_abi.py @@ -187,6 +187,11 @@ def iter_files(self): ].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( From 72c0563fb6ea9d76e3cd31626b90572237266ce9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 05:11:25 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/unit/test_wheel_abi.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_wheel_abi.py b/tests/unit/test_wheel_abi.py index 4e33ff6b..32293297 100644 --- a/tests/unit/test_wheel_abi.py +++ b/tests/unit/test_wheel_abi.py @@ -189,9 +189,10 @@ def iter_files(self): } # 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 ( + "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(