From d4570da1e54ea60dcfbfdea6f27c61e2d12e8c87 Mon Sep 17 00:00:00 2001 From: mayeut Date: Sat, 1 Feb 2025 12:10:51 +0100 Subject: [PATCH 01/10] chore: use a dataclass for ldd result --- src/auditwheel/json.py | 19 ++++ src/auditwheel/lddtree.py | 162 ++++++++++++++++-------------- src/auditwheel/main_lddtree.py | 7 +- src/auditwheel/main_show.py | 4 +- src/auditwheel/policy/__init__.py | 21 ++-- src/auditwheel/wheel_abi.py | 15 ++- tests/unit/test_policy.py | 18 +++- 7 files changed, 142 insertions(+), 104 deletions(-) create mode 100644 src/auditwheel/json.py diff --git a/src/auditwheel/json.py b/src/auditwheel/json.py new file mode 100644 index 00000000..cf11fabe --- /dev/null +++ b/src/auditwheel/json.py @@ -0,0 +1,19 @@ +import dataclasses +import json +from enum import Enum +from typing import Any + + +def _encode_value(value: Any) -> Any: + if dataclasses.is_dataclass(value) and not isinstance(value, type): + return dataclasses.asdict(value) + if isinstance(value, frozenset): + return sorted(value) + if isinstance(value, Enum): + return repr(value) + msg = f"object of type {value.__class__.__name__!r} can't be encoded to JSON" + raise TypeError(msg) + + +def dumps(obj: Any): + return json.dumps(obj, indent=4, default=_encode_value) diff --git a/src/auditwheel/lddtree.py b/src/auditwheel/lddtree.py index ffd31a1a..0f15bd33 100644 --- a/src/auditwheel/lddtree.py +++ b/src/auditwheel/lddtree.py @@ -8,8 +8,7 @@ """Read the ELF dependency tree This does not work like `ldd` in that we do not execute/load code (only read -files on disk), and we parse the dependency structure as a tree rather than - a flat list. +files on disk). """ from __future__ import annotations @@ -19,16 +18,35 @@ import glob import logging import os +from dataclasses import dataclass from fnmatch import fnmatch from pathlib import Path -from typing import Any from elftools.elf.elffile import ELFFile from .libc import Libc, get_libc log = logging.getLogger(__name__) -__all__ = ["lddtree"] +__all__ = ["DynamicExecutable", "DynamicLibrary", "ldd"] + + +@dataclass(frozen=True) +class DynamicLibrary: + soname: str + path: str | None + realpath: str | None + needed: frozenset[str] = frozenset() + + +@dataclass(frozen=True) +class DynamicExecutable: + interpreter: str | None + path: str + realpath: str + needed: frozenset[str] + rpath: tuple[str, ...] + runpath: tuple[str, ...] + libraries: dict[str, DynamicLibrary] def normpath(path: str) -> str: @@ -282,20 +300,20 @@ def find_lib( with open(target, "rb") as f: libelf = ELFFile(f) if compatible_elfs(elf, libelf): - return (target, path) + return target, path - return (None, None) + return None, None -def lddtree( +def ldd( path: str, root: str = "/", prefix: str = "", ldpaths: dict[str, list[str]] | None = None, display: str | None = None, exclude: frozenset[str] = frozenset(), - _all_libs: dict | None = None, -) -> dict: + _all_libs: dict[str, DynamicLibrary] | None = None, +) -> DynamicExecutable: """Parse the ELF dependency tree of the specified file Parameters @@ -343,17 +361,13 @@ def lddtree( if _all_libs is None: _all_libs = {} - ret: dict[str, Any] = { - "interp": None, - "path": path if display is None else display, - "realpath": path, - "needed": [], - "rpath": [], - "runpath": [], - "libs": _all_libs, - } + log.debug("ldd(%s)", path) - log.debug("lddtree(%s)", path) + interpreter: str | None = None + needed: set[str] = set() + rpaths: list[str] = [] + runpaths: list[str] = [] + _excluded_libs: set[str] = set() with open(path, "rb") as f: elf = ELFFile(f) @@ -366,12 +380,7 @@ def lddtree( interp = segment.get_interp_name() log.debug(" interp = %s", interp) - ret["interp"] = normpath(root + interp) - ret["libs"][os.path.basename(interp)] = { - "path": ret["interp"], - "realpath": readlink(ret["interp"], root, prefixed=True), - "needed": [], - } + interpreter = normpath(root + interp) # XXX: Should read it and scan for /lib paths. ldpaths["interp"] = [ normpath(root + os.path.dirname(interp)), @@ -383,10 +392,6 @@ def lddtree( break # Parse the ELF's dynamic tags. - libs: list[str] = [] - rpaths: list[str] = [] - runpaths: list[str] = [] - _excluded_libs: set[str] = set() for segment in elf.iter_segments(): if segment.header.p_type != "PT_DYNAMIC": continue @@ -397,13 +402,7 @@ def lddtree( elif t.entry.d_tag == "DT_RUNPATH": runpaths = parse_ld_paths(t.runpath, path=path, root=root) elif t.entry.d_tag == "DT_NEEDED": - if t.needed in _excluded_libs or any( - fnmatch(t.needed, e) for e in exclude - ): - log.info("Excluding %s", t.needed) - _excluded_libs.add(t.needed) - else: - libs.append(t.needed) + needed.add(t.needed) if runpaths: # If both RPATH and RUNPATH are set, only the latter is used. rpaths = [] @@ -411,6 +410,7 @@ def lddtree( # XXX: We assume there is only one PT_DYNAMIC. This is # probably fine since the runtime ldso does the same. break + if _first: # Propagate the rpaths used by the main ELF since those will be # used at runtime to locate things. @@ -418,50 +418,62 @@ def lddtree( ldpaths["runpath"] = runpaths log.debug(" ldpaths[rpath] = %s", rpaths) log.debug(" ldpaths[runpath] = %s", runpaths) - ret["rpath"] = rpaths - ret["runpath"] = runpaths # Search for the libs this ELF uses. - all_ldpaths: list[str] | None = None - for lib in libs: - if lib in _all_libs: + all_ldpaths = ( + ldpaths["rpath"] + + rpaths + + runpaths + + ldpaths["env"] + + ldpaths["runpath"] + + ldpaths["conf"] + + ldpaths["interp"] + ) + for soname in needed: + if soname in _all_libs: + continue + if soname in _excluded_libs: + continue + if any(fnmatch(soname, e) for e in exclude): + log.info("Excluding %s", soname) + _excluded_libs.add(soname) continue - if all_ldpaths is None: - all_ldpaths = ( - ldpaths["rpath"] - + rpaths - + runpaths - + ldpaths["env"] - + ldpaths["runpath"] - + ldpaths["conf"] - + ldpaths["interp"] - ) - realpath, fullpath = find_lib(elf, lib, all_ldpaths, root) - if lib in _excluded_libs or ( - realpath is not None and any(fnmatch(realpath, e) for e in exclude) - ): + # TODO we should avoid keeping elf here, related to compat + realpath, fullpath = find_lib(elf, soname, all_ldpaths, root) + if realpath is not None and any(fnmatch(realpath, e) for e in exclude): log.info("Excluding %s", realpath) - _excluded_libs.add(lib) + _excluded_libs.add(soname) continue - _all_libs[lib] = { - "realpath": realpath, - "path": fullpath, - "needed": [], - } - if realpath and fullpath: - lret = lddtree( - realpath, - root, - prefix, - ldpaths, - display=fullpath, - exclude=exclude, - _all_libs=_all_libs, - ) - _all_libs[lib]["needed"] = lret["needed"] + _all_libs[soname] = DynamicLibrary(soname, fullpath, realpath) + if realpath is None or fullpath is None: + continue + lret = ldd( + realpath, + root, + prefix, + ldpaths, + display=fullpath, + exclude=exclude, + _all_libs=_all_libs, + ) + _all_libs[soname] = DynamicLibrary( + soname, fullpath, realpath, lret.needed + ) del elf - ret["needed"] = [lib for lib in libs if lib not in _excluded_libs] - - return ret + if interpreter is not None: + soname = os.path.basename(interpreter) + _all_libs[soname] = DynamicLibrary( + soname, interpreter, readlink(interpreter, root, prefixed=True) + ) + + return DynamicExecutable( + interpreter, + path if display is None else display, + path, + frozenset(needed - _excluded_libs), + tuple(rpaths), + tuple(runpaths), + _all_libs, + ) diff --git a/src/auditwheel/main_lddtree.py b/src/auditwheel/main_lddtree.py index 29e13ce8..d0c9a212 100644 --- a/src/auditwheel/main_lddtree.py +++ b/src/auditwheel/main_lddtree.py @@ -14,8 +14,7 @@ def configure_subparser(sub_parsers): def execute(args, p: argparse.ArgumentParser): # noqa: ARG001 - import json + from . import json + from .lddtree import ldd - from .lddtree import lddtree - - logger.info(json.dumps(lddtree(args.file), indent=4)) + logger.info(json.dumps(ldd(args.file))) diff --git a/src/auditwheel/main_show.py b/src/auditwheel/main_show.py index 7d736342..4b90efbe 100644 --- a/src/auditwheel/main_show.py +++ b/src/auditwheel/main_show.py @@ -23,9 +23,9 @@ def printp(text: str) -> None: def execute(args, parser: argparse.ArgumentParser): - import json from os.path import basename, isfile + from . import json from .wheel_abi import NonPlatformWheel, analyze_wheel_abi wheel_policy = WheelPolicies() @@ -99,7 +99,7 @@ def execute(args, parser: argparse.ArgumentParser): printp("The wheel requires no external shared libraries! :)") else: printp("The following external shared libraries are required by the wheel:") - print(json.dumps(dict(sorted(libs.items())), indent=4)) + print(json.dumps(dict(sorted(libs.items())))) for p in sorted(wheel_policy.policies, key=lambda p: p["priority"]): if p["priority"] > wheel_policy.get_priority_by_name(winfo.overall_tag): diff --git a/src/auditwheel/policy/__init__.py b/src/auditwheel/policy/__init__.py index 5d6f2032..023d50d0 100644 --- a/src/auditwheel/policy/__init__.py +++ b/src/auditwheel/policy/__init__.py @@ -14,6 +14,7 @@ from auditwheel.elfutils import filter_undefined_symbols, is_subdir +from ..lddtree import DynamicExecutable from ..libc import Libc, get_libc from ..musllinux import find_musl_libc, get_musl_version @@ -159,10 +160,10 @@ def policy_is_satisfied( return max(matching_policies) - def lddtree_external_references(self, lddtree: dict, wheel_path: str) -> dict: - # XXX: Document the lddtree structure, or put it in something - # more stable than a big nested dict - def filter_libs(libs: set[str], whitelist: set[str]) -> Generator[str]: + def lddtree_external_references( + self, lddtree: DynamicExecutable, wheel_path: str + ) -> dict: + def filter_libs(libs: frozenset[str], whitelist: set[str]) -> Generator[str]: for lib in libs: if "ld-linux" in lib or lib in ["ld64.so.2", "ld64.so.1"]: # always exclude ELF dynamic linker/loader @@ -185,7 +186,7 @@ def get_req_external(libs: set[str], whitelist: set[str]) -> set[str]: while libs: lib = libs.pop() reqs.add(lib) - for dep in filter_libs(lddtree["libs"][lib]["needed"], whitelist): + for dep in filter_libs(lddtree.libraries[lib].needed, whitelist): if dep not in reqs: libs.add(dep) return reqs @@ -201,23 +202,23 @@ def get_req_external(libs: set[str], whitelist: set[str]) -> set[str]: # whitelist is the complete set of all libraries. so nothing # is considered "external" that needs to be copied in. whitelist = set(p["lib_whitelist"]) - blacklist_libs = set(p["blacklist"].keys()) & set(lddtree["needed"]) + blacklist_libs = set(p["blacklist"].keys()) & lddtree.needed blacklist = {k: p["blacklist"][k] for k in blacklist_libs} - blacklist = filter_undefined_symbols(lddtree["realpath"], blacklist) + blacklist = filter_undefined_symbols(lddtree.realpath, blacklist) needed_external_libs = get_req_external( - set(filter_libs(lddtree["needed"], whitelist)), whitelist + set(filter_libs(lddtree.needed, whitelist)), whitelist ) pol_ext_deps = {} for lib in needed_external_libs: - if is_subdir(lddtree["libs"][lib]["realpath"], wheel_path): + if is_subdir(lddtree.libraries[lib].realpath, wheel_path): # we didn't filter libs that resolved via RPATH out # earlier because we wanted to make sure to pick up # our elf's indirect dependencies. But now we want to # filter these ones out, since they're not "external". logger.debug("RPATH FTW: %s", lib) continue - pol_ext_deps[lib] = lddtree["libs"][lib]["realpath"] + pol_ext_deps[lib] = lddtree.libraries[lib].realpath ret[p["name"]] = { "libs": pol_ext_deps, "priority": p["priority"], diff --git a/src/auditwheel/wheel_abi.py b/src/auditwheel/wheel_abi.py index 2e953655..99fe3cdc 100644 --- a/src/auditwheel/wheel_abi.py +++ b/src/auditwheel/wheel_abi.py @@ -2,7 +2,6 @@ import functools import itertools -import json import logging import os from collections import defaultdict, namedtuple @@ -10,6 +9,7 @@ from copy import deepcopy from os.path import basename +from . import json from .elfutils import ( elf_file_filter, elf_find_ucs2_symbols, @@ -18,7 +18,7 @@ elf_references_PyFPE_jbuf, ) from .genericpkgctx import InGenericPkgCtx -from .lddtree import lddtree +from .lddtree import ldd from .policy import WheelPolicies log = logging.getLogger(__name__) @@ -82,7 +82,7 @@ def get_wheel_elfdata( # to fail and there's no need to do further checks if not shared_libraries_in_purelib: log.debug("processing: %s", fn) - elftree = lddtree(fn, exclude=exclude) + elftree = ldd(fn, exclude=exclude) for key, value in elf_find_versioned_symbols(elf): log.debug("key %s, value %s", key, value) @@ -127,7 +127,7 @@ def get_wheel_elfdata( needed_libs = { lib for elf in itertools.chain(full_elftree.values(), nonpy_elftree.values()) - for lib in elf["needed"] + for lib in elf.needed } for fn, elf_tree in nonpy_elftree.items(): @@ -144,10 +144,9 @@ def get_wheel_elfdata( elf_tree, ctx.path ) - log.debug("full_elftree:\n%s", json.dumps(full_elftree, indent=4)) + log.debug("full_elftree:\n%s", json.dumps(full_elftree)) log.debug( - "full_external_refs (will be repaired):\n%s", - json.dumps(full_external_refs, indent=4), + "full_external_refs (will be repaired):\n%s", json.dumps(full_external_refs) ) return ( @@ -247,7 +246,7 @@ def analyze_wheel_abi( update(external_refs, external_refs_by_fn[fn]) log.debug("external reference info") - log.debug(json.dumps(external_refs, indent=4)) + log.debug(json.dumps(external_refs)) external_libs = get_external_libs(external_refs) external_versioned_symbols = get_versioned_symbols(external_libs) diff --git a/tests/unit/test_policy.py b/tests/unit/test_policy.py index 427d2d80..6684f586 100644 --- a/tests/unit/test_policy.py +++ b/tests/unit/test_policy.py @@ -9,6 +9,7 @@ import pytest from auditwheel.error import InvalidLibc +from auditwheel.lddtree import DynamicExecutable, DynamicLibrary from auditwheel.libc import Libc from auditwheel.policy import ( WheelPolicies, @@ -262,11 +263,18 @@ def test_filter_libs(self): unfiltered_libs = ["libfoo.so.1.0", "libbar.so.999.999.999"] libs = filtered_libs + unfiltered_libs - lddtree = { - "realpath": "/path/to/lib", - "needed": libs, - "libs": {lib: {"needed": [], "realpath": "/path/to/lib"} for lib in libs}, - } + lddtree = DynamicExecutable( + interpreter=None, + path="/path/to/lib", + realpath="/path/to/lib", + needed=frozenset(libs), + libraries={ + lib: DynamicLibrary(lib, f"/path/to/{lib}", f"/path/to/{lib}") + for lib in libs + }, + rpath=(), + runpath=(), + ) wheel_policy = WheelPolicies() full_external_refs = wheel_policy.lddtree_external_references( lddtree, "/path/to/wheel" From 43504082e66b0215eeff5b02b9e77d8734adf3d1 Mon Sep 17 00:00:00 2001 From: mayeut Date: Sun, 2 Feb 2025 14:16:03 +0100 Subject: [PATCH 02/10] feat: add GNU_PROPERTY_X86_ISA_1_NEEDED detection ISA extensions usage is not defined by a PEP yet. This first implementation fails to repair the wheel if the usage of x86-64-v[2-4] is required. The check can be disabled with `--disable-isa-ext-check`. The detection being related to a declaration when building, it will not detect the requirement for binaries where the declaration is missing. All executables built on a manylinux_2_34 image will be detected as x86-64-v2. --- src/auditwheel/architecture.py | 71 +++++++ src/auditwheel/lddtree.py | 242 +++++++++++++++-------- src/auditwheel/main_repair.py | 18 +- src/auditwheel/main_show.py | 19 +- src/auditwheel/policy/__init__.py | 80 +++----- src/auditwheel/wheel_abi.py | 126 +++++++++--- tests/integration/test_bundled_wheels.py | 9 +- tests/integration/test_manylinux.py | 26 ++- tests/unit/test_architecture.py | 93 +++++++++ tests/unit/test_policy.py | 112 +++-------- 10 files changed, 542 insertions(+), 254 deletions(-) create mode 100644 src/auditwheel/architecture.py create mode 100644 tests/unit/test_architecture.py diff --git a/src/auditwheel/architecture.py b/src/auditwheel/architecture.py new file mode 100644 index 00000000..dfa64aeb --- /dev/null +++ b/src/auditwheel/architecture.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import functools +import platform +import struct +import sys +from enum import Enum + + +class Architecture(Enum): + value: str + + aarch64 = "aarch64" + armv7l = "armv7l" + i686 = "i686" + loongarch64 = "loongarch64" + ppc64 = "ppc64" + ppc64le = "ppc64le" + riscv64 = "riscv64" + s390x = "s390x" + x86_64 = "x86_64" + x86_64_v2 = "x86_64_v2" + x86_64_v3 = "x86_64_v3" + x86_64_v4 = "x86_64_v4" + + def __str__(self): + return self.value + + @property + def baseline(self): + if self.value.startswith("x86_64"): + return Architecture.x86_64 + return self + + @classmethod + @functools.lru_cache(None) + def _member_list(cls) -> list[Architecture]: + return list(cls) + + def is_subset(self, other: Architecture) -> bool: + if self.baseline != other.baseline: + return False + member_list = Architecture._member_list() + return member_list.index(self) <= member_list.index(other) + + def is_superset(self, other: Architecture) -> bool: + if self.baseline != other.baseline: + return False + return other.is_subset(self) + + @staticmethod + def get_native_architecture(*, bits: int | None = None) -> Architecture: + machine = platform.machine() + if sys.platform.startswith("win"): + machine = {"AMD64": "x86_64", "ARM64": "aarch64", "x86": "i686"}.get( + machine, machine + ) + elif sys.platform.startswith("darwin"): + machine = {"arm64": "aarch64"}.get(machine, machine) + + if bits is None: + # c.f. https://github.com/pypa/packaging/pull/711 + bits = 8 * struct.calcsize("P") + + if machine in {"x86_64", "i686"}: + machine = {64: "x86_64", 32: "i686"}[bits] + elif machine in {"aarch64", "armv8l"}: + # use armv7l policy for 64-bit arm kernel in 32-bit mode (armv8l) + machine = {64: "aarch64", 32: "armv7l"}[bits] + + return Architecture(machine) diff --git a/src/auditwheel/lddtree.py b/src/auditwheel/lddtree.py index 0f15bd33..f0a69b7b 100644 --- a/src/auditwheel/lddtree.py +++ b/src/auditwheel/lddtree.py @@ -22,19 +22,58 @@ from fnmatch import fnmatch from pathlib import Path +from elftools.elf.constants import E_FLAGS from elftools.elf.elffile import ELFFile +from elftools.elf.sections import NoteSection +from .architecture import Architecture from .libc import Libc, get_libc log = logging.getLogger(__name__) __all__ = ["DynamicExecutable", "DynamicLibrary", "ldd"] +@dataclass(frozen=True) +class Platform: + _elf_osabi: str + _elf_class: int + _elf_little_endian: bool + _elf_machine: str + _base_arch: Architecture | None + _ext_arch: Architecture | None + _error_msg: str | None + + def is_compatible(self, other: Platform) -> bool: + os_abis = frozenset((self._elf_osabi, other._elf_osabi)) + compat_sets = ( + frozenset(f"ELFOSABI_{x}" for x in ("NONE", "SYSV", "GNU", "LINUX")), + ) + return ( + (len(os_abis) == 1 or any(os_abis.issubset(x) for x in compat_sets)) + and self._elf_class == other._elf_class + and self._elf_little_endian == other._elf_little_endian + and self._elf_machine == other._elf_machine + ) + + @property + def baseline_architecture(self) -> Architecture: + if self._base_arch is not None: + return self._base_arch + raise ValueError(self._error_msg) + + @property + def extended_architecture(self) -> Architecture | None: + if self._error_msg is not None: + raise ValueError(self._error_msg) + return self._ext_arch + + @dataclass(frozen=True) class DynamicLibrary: soname: str path: str | None realpath: str | None + platform: Platform | None = None needed: frozenset[str] = frozenset() @@ -43,12 +82,80 @@ class DynamicExecutable: interpreter: str | None path: str realpath: str + platform: Platform needed: frozenset[str] rpath: tuple[str, ...] runpath: tuple[str, ...] libraries: dict[str, DynamicLibrary] +def _get_platform(elf: ELFFile) -> Platform: + elf_osabi = elf.header["e_ident"]["EI_OSABI"] + elf_class = elf.elfclass + elf_little_endian = elf.little_endian + elf_machine = elf["e_machine"] + base_arch = { + ("EM_386", 32, True): Architecture.i686, + ("EM_X86_64", 64, True): Architecture.x86_64, + ("EM_PPC64", 64, True): Architecture.ppc64le, + ("EM_PPC64", 64, False): Architecture.ppc64, + ("EM_RISCV", 64, True): Architecture.riscv64, + ("EM_AARCH64", 64, True): Architecture.aarch64, + ("EM_S390", 64, False): Architecture.s390x, + ("EM_ARM", 32, True): Architecture.armv7l, + ("EM_LOONGARCH", 64, True): Architecture.loongarch64, + }.get((elf_machine, elf_class, elf_little_endian), None) + ext_arch: Architecture | None = None + error_msg: str | None = None + flags = elf["e_flags"] + assert base_arch is None or base_arch.baseline == base_arch + if base_arch is None: + error_msg = "Unknown architecture" + elif base_arch == Architecture.x86_64: + for section in elf.iter_sections(): + if not isinstance(section, NoteSection): + continue + for note in section.iter_notes(): + if note["n_type"] != "NT_GNU_PROPERTY_TYPE_0": + continue + if note["n_name"] != "GNU": + continue + for prop in note["n_desc"]: + if prop.pr_type != "GNU_PROPERTY_X86_ISA_1_NEEDED": + continue + if prop.pr_datasz != 4: + continue + data = prop.pr_data + data -= data & 1 # clear baseline + if data & 8 == 8: + ext_arch = Architecture.x86_64_v4 + break + if data & 4 == 4: + ext_arch = Architecture.x86_64_v3 + break + if data & 2 == 2: + ext_arch = Architecture.x86_64_v2 + break + if data != 0: + error_msg = "unknown x86_64 ISA" + break + elif base_arch == Architecture.armv7l: + if (flags & E_FLAGS.EF_ARM_EABIMASK) != E_FLAGS.EF_ARM_EABI_VER5: + error_msg = "Invalid ARM EABI version for armv7l" + elif (flags & E_FLAGS.EF_ARM_ABI_FLOAT_HARD) != E_FLAGS.EF_ARM_ABI_FLOAT_HARD: + error_msg = "armv7l shall use hard-float" + + return Platform( + elf_osabi, + elf_class, + elf_little_endian, + elf_machine, + base_arch, + ext_arch, + error_msg, + ) + + def normpath(path: str) -> str: """Normalize a path @@ -243,43 +350,16 @@ def load_ld_paths(root: str = "/", prefix: str = "") -> dict[str, list[str]]: return ldpaths -def compatible_elfs(elf1: ELFFile, elf2: ELFFile) -> bool: - """See if two ELFs are compatible - - This compares the aspects of the ELF to see if they're compatible: - bit size, endianness, machine type, and operating system. - - Parameters - ---------- - elf1 : ELFFile - elf2 : ELFFile - - Returns - ------- - True if compatible, False otherwise - """ - osabis = frozenset(e.header["e_ident"]["EI_OSABI"] for e in (elf1, elf2)) - compat_sets = ( - frozenset(f"ELFOSABI_{x}" for x in ("NONE", "SYSV", "GNU", "LINUX")), - ) - return ( - (len(osabis) == 1 or any(osabis.issubset(x) for x in compat_sets)) - and elf1.elfclass == elf2.elfclass - and elf1.little_endian == elf2.little_endian - and elf1.header["e_machine"] == elf2.header["e_machine"] - ) - - def find_lib( - elf: ELFFile, lib: str, ldpaths: list[str], root: str = "/" + platform: Platform, lib: str, ldpaths: list[str], root: str = "/" ) -> tuple[str | None, str | None]: """Try to locate a ``lib`` that is compatible to ``elf`` in the given ``ldpaths`` Parameters ---------- - elf : ELFFile - The elf which the library should be compatible with (ELF wise) + platform : Platform + The platform which the library should be compatible with (ELF wise) lib : str The library (basename) to search for ldpaths : list[str] @@ -299,7 +379,7 @@ def find_lib( if os.path.exists(target): with open(target, "rb") as f: libelf = ELFFile(f) - if compatible_elfs(elf, libelf): + if platform.is_compatible(_get_platform(libelf)): return target, path return None, None @@ -371,7 +451,6 @@ def ldd( with open(path, "rb") as f: elf = ELFFile(f) - # If this is the first ELF, extract the interpreter. if _first: for segment in elf.iter_segments(): @@ -391,6 +470,9 @@ def ldd( log.debug(" ldpaths[interp] = %s", ldpaths["interp"]) break + # get the platform + platform = _get_platform(elf) + # Parse the ELF's dynamic tags. for segment in elf.iter_segments(): if segment.header.p_type != "PT_DYNAMIC": @@ -411,67 +493,63 @@ def ldd( # probably fine since the runtime ldso does the same. break - if _first: - # Propagate the rpaths used by the main ELF since those will be - # used at runtime to locate things. - ldpaths["rpath"] = rpaths - ldpaths["runpath"] = runpaths - log.debug(" ldpaths[rpath] = %s", rpaths) - log.debug(" ldpaths[runpath] = %s", runpaths) - - # Search for the libs this ELF uses. - all_ldpaths = ( - ldpaths["rpath"] - + rpaths - + runpaths - + ldpaths["env"] - + ldpaths["runpath"] - + ldpaths["conf"] - + ldpaths["interp"] - ) - for soname in needed: - if soname in _all_libs: - continue - if soname in _excluded_libs: - continue - if any(fnmatch(soname, e) for e in exclude): - log.info("Excluding %s", soname) - _excluded_libs.add(soname) - continue - # TODO we should avoid keeping elf here, related to compat - realpath, fullpath = find_lib(elf, soname, all_ldpaths, root) - if realpath is not None and any(fnmatch(realpath, e) for e in exclude): - log.info("Excluding %s", realpath) - _excluded_libs.add(soname) - continue - _all_libs[soname] = DynamicLibrary(soname, fullpath, realpath) - if realpath is None or fullpath is None: - continue - lret = ldd( - realpath, - root, - prefix, - ldpaths, - display=fullpath, - exclude=exclude, - _all_libs=_all_libs, - ) - _all_libs[soname] = DynamicLibrary( - soname, fullpath, realpath, lret.needed - ) - del elf + if _first: + # Propagate the rpaths used by the main ELF since those will be + # used at runtime to locate things. + ldpaths["rpath"] = rpaths + ldpaths["runpath"] = runpaths + log.debug(" ldpaths[rpath] = %s", rpaths) + log.debug(" ldpaths[runpath] = %s", runpaths) + + # Search for the libs this ELF uses. + all_ldpaths = ( + ldpaths["rpath"] + + rpaths + + runpaths + + ldpaths["env"] + + ldpaths["runpath"] + + ldpaths["conf"] + + ldpaths["interp"] + ) + for soname in needed: + if soname in _all_libs: + continue + if soname in _excluded_libs: + continue + if any(fnmatch(soname, e) for e in exclude): + log.info("Excluding %s", soname) + _excluded_libs.add(soname) + continue + realpath, fullpath = find_lib(platform, soname, all_ldpaths, root) + if realpath is not None and any(fnmatch(realpath, e) for e in exclude): + log.info("Excluding %s", realpath) + _excluded_libs.add(soname) + continue + _all_libs[soname] = DynamicLibrary(soname, fullpath, realpath) + if realpath is None or fullpath is None: + continue + dependency = ldd(realpath, root, prefix, ldpaths, fullpath, exclude, _all_libs) + _all_libs[soname] = DynamicLibrary( + soname, + fullpath, + realpath, + dependency.platform, + dependency.needed, + ) + if interpreter is not None: soname = os.path.basename(interpreter) _all_libs[soname] = DynamicLibrary( - soname, interpreter, readlink(interpreter, root, prefixed=True) + soname, interpreter, readlink(interpreter, root, prefixed=True), platform ) return DynamicExecutable( interpreter, path if display is None else display, path, + platform, frozenset(needed - _excluded_libs), tuple(rpaths), tuple(runpaths), diff --git a/src/auditwheel/main_repair.py b/src/auditwheel/main_repair.py index f2bbb64b..a1051bc0 100644 --- a/src/auditwheel/main_repair.py +++ b/src/auditwheel/main_repair.py @@ -101,6 +101,13 @@ def configure_parser(sub_parsers): help="Do not check for higher policy compatibility", default=False, ) + p.add_argument( + "--disable-isa-ext-check", + dest="DISABLE_ISA_EXT_CHECK", + action="store_true", + help="Do not check for extended ISA compatibility (e.g. x86_64_v2)", + default=False, + ) p.set_defaults(func=execute) @@ -123,7 +130,9 @@ def execute(args, parser: argparse.ArgumentParser): os.makedirs(args.WHEEL_DIR) try: - wheel_abi = analyze_wheel_abi(wheel_policy, wheel_file, exclude) + wheel_abi = analyze_wheel_abi( + wheel_policy, wheel_file, exclude, args.DISABLE_ISA_EXT_CHECK + ) except NonPlatformWheel: logger.info(NonPlatformWheel.LOG_MESSAGE) return 1 @@ -155,6 +164,13 @@ def execute(args, parser: argparse.ArgumentParser): ) parser.error(msg) + if reqd_tag > wheel_policy.get_priority_by_name(wheel_abi.machine_tag): + msg = ( + f'cannot repair "{wheel_file}" to "{args.PLAT}" ABI because it ' + "depends on unsupported ISA extensions." + ) + parser.error(msg) + abis = [policy["name"]] + policy["aliases"] if (not args.ONLY_PLAT) and reqd_tag < wheel_policy.get_priority_by_name( wheel_abi.overall_tag diff --git a/src/auditwheel/main_show.py b/src/auditwheel/main_show.py index 4b90efbe..c88537bf 100644 --- a/src/auditwheel/main_show.py +++ b/src/auditwheel/main_show.py @@ -12,6 +12,13 @@ def configure_parser(sub_parsers): help = "Audit a wheel for external shared library dependencies." p = sub_parsers.add_parser("show", help=help, description=help) p.add_argument("WHEEL_FILE", help="Path to wheel file.") + p.add_argument( + "--disable-isa-ext-check", + dest="DISABLE_ISA_EXT_CHECK", + action="store_true", + help="Do not check for extended ISA compatibility (e.g. x86_64_v2)", + default=False, + ) p.set_defaults(func=execute) @@ -36,7 +43,9 @@ def execute(args, parser: argparse.ArgumentParser): parser.error(f"cannot access {args.WHEEL_FILE}. No such file") try: - winfo = analyze_wheel_abi(wheel_policy, args.WHEEL_FILE, frozenset()) + winfo = analyze_wheel_abi( + wheel_policy, args.WHEEL_FILE, frozenset(), args.DISABLE_ISA_EXT_CHECK + ) except NonPlatformWheel: logger.info(NonPlatformWheel.LOG_MESSAGE) return 1 @@ -70,6 +79,14 @@ def execute(args, parser: argparse.ArgumentParser): if args.verbose < 1: return None + if ( + wheel_policy.get_priority_by_name(winfo.machine_tag) + < wheel_policy.priority_highest + ): + printp("This wheel depends on unsupported ISA extensions.") + if args.verbose < 1: + return None + if len(libs_with_versions) == 0: printp( "The wheel references no external versioned symbols from " diff --git a/src/auditwheel/policy/__init__.py b/src/auditwheel/policy/__init__.py index 023d50d0..805c3935 100644 --- a/src/auditwheel/policy/__init__.py +++ b/src/auditwheel/policy/__init__.py @@ -2,10 +2,7 @@ import json import logging -import platform as _platform_module import re -import struct -import sys from collections import defaultdict from collections.abc import Generator from os.path import abspath, dirname, join @@ -14,6 +11,7 @@ from auditwheel.elfutils import filter_undefined_symbols, is_subdir +from ..architecture import Architecture from ..lddtree import DynamicExecutable from ..libc import Libc, get_libc from ..musllinux import find_musl_libc, get_musl_version @@ -36,7 +34,7 @@ def __init__( *, libc: Libc | None = None, musl_policy: str | None = None, - arch: str | None = None, + arch: Architecture | None = None, ) -> None: if libc is None: libc = get_libc() if musl_policy is None else Libc.MUSL @@ -51,13 +49,14 @@ def __init__( msg = f"Invalid 'musl_policy': '{musl_policy}'" raise ValueError(msg) if arch is None: - arch = get_arch_name() + arch = Architecture.get_native_architecture() policies = json.loads(_POLICY_JSON_MAP[libc].read_text()) self._policies = [] - self._arch_name = arch + self._architecture = arch self._libc_variant = libc self._musl_policy = musl_policy + base_arch = arch.baseline.value _validate_pep600_compliance(policies) for policy in policies: if self._musl_policy is not None and policy["name"] not in { @@ -65,17 +64,12 @@ def __init__( self._musl_policy, }: continue - if ( - self._arch_name in policy["symbol_versions"] - or policy["name"] == "linux" - ): + if arch.value in policy["symbol_versions"] or policy["name"] == "linux": if policy["name"] != "linux": - policy["symbol_versions"] = policy["symbol_versions"][ - self._arch_name - ] - policy["name"] = policy["name"] + "_" + self._arch_name + policy["symbol_versions"] = policy["symbol_versions"][base_arch] + policy["name"] = policy["name"] + "_" + base_arch policy["aliases"] = [ - alias + "_" + self._arch_name for alias in policy["aliases"] + alias + "_" + base_arch for alias in policy["aliases"] ] policy["lib_whitelist"] = _fixup_musl_libc_soname( libc, arch, policy["lib_whitelist"] @@ -85,6 +79,10 @@ def __init__( if self._libc_variant == Libc.MUSL: assert len(self._policies) == 2, self._policies + @property + def architecture(self) -> Architecture: + return self._architecture + @property def policies(self): return self._policies @@ -97,29 +95,30 @@ def priority_highest(self): def priority_lowest(self): return min(p["priority"] for p in self._policies) - def get_policy_by_name(self, name: str) -> dict | None: + def get_policy_by_name(self, name: str) -> dict: matches = [ p for p in self._policies if p["name"] == name or name in p["aliases"] ] if len(matches) == 0: - return None + msg = f"no policy named {name!r} found" + raise LookupError(msg) if len(matches) > 1: msg = "Internal error. Policies should be unique" raise RuntimeError(msg) return matches[0] - def get_policy_name(self, priority: int) -> str | None: + def get_policy_name(self, priority: int) -> str: matches = [p["name"] for p in self._policies if p["priority"] == priority] if len(matches) == 0: - return None + msg = f"no policy with priority {priority} found" + raise LookupError(msg) if len(matches) > 1: msg = "Internal error. priorities should be unique" raise RuntimeError(msg) return matches[0] - def get_priority_by_name(self, name: str) -> int | None: - policy = self.get_policy_by_name(name) - return None if policy is None else policy["priority"] + def get_priority_by_name(self, name: str) -> int: + return self.get_policy_by_name(name)["priority"] def versioned_symbols_policy(self, versioned_symbols: dict[str, set[str]]) -> int: def policy_is_satisfied( @@ -227,23 +226,6 @@ def get_req_external(libs: set[str], whitelist: set[str]) -> set[str]: return ret -def get_arch_name(*, bits: int | None = None) -> str: - machine = _platform_module.machine() - if sys.platform == "darwin" and machine == "arm64": - return "aarch64" - - if bits is None: - # c.f. https://github.com/pypa/packaging/pull/711 - bits = 8 * struct.calcsize("P") - - if machine in {"x86_64", "i686"}: - return {64: "x86_64", 32: "i686"}[bits] - if machine in {"aarch64", "armv8l"}: - # use armv7l policy for 64-bit arm kernel in 32-bit mode (armv8l) - return {64: "aarch64", 32: "armv7l"}[bits] - return machine - - def _validate_pep600_compliance(policies) -> None: symbol_versions: dict[str, dict[str, set[str]]] = {} lib_whitelist: set[str] = set() @@ -276,25 +258,25 @@ def _validate_pep600_compliance(policies) -> None: symbol_versions[arch] = symbol_versions_arch -def _fixup_musl_libc_soname(libc: Libc, arch: str, whitelist): +def _fixup_musl_libc_soname(libc: Libc, arch: Architecture, whitelist): if libc != Libc.MUSL: return whitelist soname_map = { "libc.so": { - "x86_64": "libc.musl-x86_64.so.1", - "i686": "libc.musl-x86.so.1", - "aarch64": "libc.musl-aarch64.so.1", - "s390x": "libc.musl-s390x.so.1", - "ppc64le": "libc.musl-ppc64le.so.1", - "armv7l": "libc.musl-armv7.so.1", - "riscv64": "libc.musl-riscv64.so.1", - "loongarch64": "libc.musl-loongarch64.so.1", + Architecture.x86_64: "libc.musl-x86_64.so.1", + Architecture.i686: "libc.musl-x86.so.1", + Architecture.aarch64: "libc.musl-aarch64.so.1", + Architecture.s390x: "libc.musl-s390x.so.1", + Architecture.ppc64le: "libc.musl-ppc64le.so.1", + Architecture.armv7l: "libc.musl-armv7.so.1", + Architecture.riscv64: "libc.musl-riscv64.so.1", + Architecture.loongarch64: "libc.musl-loongarch64.so.1", } } new_whitelist = [] for soname in whitelist: if soname in soname_map: - new_soname = soname_map[soname][arch] + new_soname = soname_map[soname][arch.baseline] logger.debug("Replacing whitelisted '%s' by '%s'", soname, new_soname) new_whitelist.append(new_soname) else: diff --git a/src/auditwheel/wheel_abi.py b/src/auditwheel/wheel_abi.py index 99fe3cdc..51da38fc 100644 --- a/src/auditwheel/wheel_abi.py +++ b/src/auditwheel/wheel_abi.py @@ -4,10 +4,12 @@ import itertools import logging import os -from collections import defaultdict, namedtuple +from collections import defaultdict from collections.abc import Mapping from copy import deepcopy +from dataclasses import dataclass from os.path import basename +from typing import Any from . import json from .elfutils import ( @@ -18,23 +20,23 @@ elf_references_PyFPE_jbuf, ) from .genericpkgctx import InGenericPkgCtx -from .lddtree import ldd +from .lddtree import DynamicExecutable, ldd from .policy import WheelPolicies log = logging.getLogger(__name__) -WheelAbIInfo = namedtuple( # noqa: PYI024 - "WheelAbIInfo", - [ - "overall_tag", - "external_refs", - "ref_tag", - "versioned_symbols", - "sym_tag", - "ucs_tag", - "pyfpe_tag", - "blacklist_tag", - ], -) + + +@dataclass(frozen=True) +class WheelAbIInfo: + overall_tag: str + external_refs: dict[str, Any] + ref_tag: str + versioned_symbols: dict[str, set[str]] + sym_tag: str + ucs_tag: str + pyfpe_tag: str + blacklist_tag: str + machine_tag: str class WheelAbiError(Exception): @@ -64,19 +66,19 @@ def get_wheel_elfdata( with InGenericPkgCtx(wheel_fn) as ctx: shared_libraries_in_purelib = [] + shared_libraries_with_invalid_machine = [] platform_wheel = False for fn, elf in elf_file_filter(ctx.iter_files()): - platform_wheel = True - # Check for invalid binary wheel format: no shared library should # be found in purelib so_path_split = fn.split(os.sep) + so_name = so_path_split[-1] # If this is in purelib, add it to the list of shared libraries in # purelib if "purelib" in so_path_split: - shared_libraries_in_purelib.append(so_path_split[-1]) + shared_libraries_in_purelib.append(so_name) # If at least one shared library exists in purelib, this is going # to fail and there's no need to do further checks @@ -84,6 +86,19 @@ def get_wheel_elfdata( log.debug("processing: %s", fn) elftree = ldd(fn, exclude=exclude) + try: + arch = elftree.platform.baseline_architecture + if arch != wheel_policy.architecture.baseline: + shared_libraries_with_invalid_machine.append(so_name) + log.warning("ignoring: %s with %s architecture", so_name, arch) + continue + except ValueError: + shared_libraries_with_invalid_machine.append(so_name) + log.warning("ignoring: %s with unknown architecture", so_name) + continue + + platform_wheel = True + for key, value in elf_find_versioned_symbols(elf): log.debug("key %s, value %s", key, value) versioned_symbols[key].add(value) @@ -109,9 +124,6 @@ def get_wheel_elfdata( # its internal references later. nonpy_elftree[fn] = elftree - if not platform_wheel: - raise NonPlatformWheel - # If at least one shared library exists in purelib, raise an error if shared_libraries_in_purelib: libraries = "\n\t".join(shared_libraries_in_purelib) @@ -123,6 +135,16 @@ def get_wheel_elfdata( ) raise RuntimeError(msg) + if not platform_wheel: + if not shared_libraries_with_invalid_machine: + raise NonPlatformWheel + libraries = "\n\t".join(shared_libraries_with_invalid_machine) + msg = ( + "Invalid binary wheel, found the following shared library/libraries " + f"with a different target architecture:\n\t{libraries}\n" + ) + raise NonPlatformWheel(msg) + # Get a list of all external libraries needed by ELFs in the wheel. needed_libs = { lib @@ -226,8 +248,50 @@ def get_symbol_policies( return result +def _get_machine_policy( + wheel_policy: WheelPolicies, + elftree_by_fn: dict[str, DynamicExecutable], + external_so_names: frozenset[str], +) -> int: + result = wheel_policy.priority_highest + machine_to_check = {} + for fn, dynamic_executable in elftree_by_fn.items(): + if fn in machine_to_check: + continue + machine_to_check[fn] = dynamic_executable.platform.extended_architecture + for dependency in dynamic_executable.libraries.values(): + if dependency.soname not in external_so_names: + continue + if dependency.realpath is None: + continue + assert dependency.platform is not None + if dependency.realpath in machine_to_check: + continue + machine_to_check[dependency.realpath] = ( + dependency.platform.extended_architecture + ) + + for fn, extended_architecture in machine_to_check.items(): + if extended_architecture is None: + continue + if wheel_policy.architecture.is_superset(extended_architecture): + continue + log.warning( + "ELF file %r requires %r instruction set, not in %r", + fn, + extended_architecture.value, + wheel_policy.architecture.value, + ) + result = wheel_policy.priority_lowest + + return result + + def analyze_wheel_abi( - wheel_policy: WheelPolicies, wheel_fn: str, exclude: frozenset[str] + wheel_policy: WheelPolicies, + wheel_fn: str, + exclude: frozenset[str], + disable_isa_ext_check: bool, ) -> WheelAbIInfo: external_refs = { p["name"]: {"libs": {}, "blacklist": {}, "priority": p["priority"]} @@ -271,6 +335,13 @@ def analyze_wheel_abi( default=wheel_policy.priority_lowest, ) + if disable_isa_ext_check: + machine_policy = wheel_policy.priority_highest + else: + machine_policy = _get_machine_policy( + wheel_policy, elftree_by_fn, frozenset(external_libs.values()) + ) + if has_ucs2: ucs_policy = wheel_policy.priority_lowest else: @@ -286,8 +357,16 @@ def analyze_wheel_abi( ucs_tag = wheel_policy.get_policy_name(ucs_policy) pyfpe_tag = wheel_policy.get_policy_name(pyfpe_policy) blacklist_tag = wheel_policy.get_policy_name(blacklist_policy) + machine_tag = wheel_policy.get_policy_name(machine_policy) overall_tag = wheel_policy.get_policy_name( - min(symbol_policy, ref_policy, ucs_policy, pyfpe_policy, blacklist_policy) + min( + symbol_policy, + ref_policy, + ucs_policy, + pyfpe_policy, + blacklist_policy, + machine_policy, + ) ) return WheelAbIInfo( @@ -299,6 +378,7 @@ def analyze_wheel_abi( ucs_tag, pyfpe_tag, blacklist_tag, + machine_tag, ) diff --git a/tests/integration/test_bundled_wheels.py b/tests/integration/test_bundled_wheels.py index b4d14a0f..2310bb0e 100644 --- a/tests/integration/test_bundled_wheels.py +++ b/tests/integration/test_bundled_wheels.py @@ -16,6 +16,7 @@ import pytest from auditwheel import lddtree, main_repair +from auditwheel.architecture import Architecture from auditwheel.libc import Libc from auditwheel.policy import WheelPolicies from auditwheel.wheel_abi import analyze_wheel_abi @@ -67,8 +68,8 @@ def test_analyze_wheel_abi(file, external_libs, exclude): cp.setenv("LD_LIBRARY_PATH", f"{HERE}") importlib.reload(lddtree) - wheel_policies = WheelPolicies(libc=Libc.GLIBC, arch="x86_64") - winfo = analyze_wheel_abi(wheel_policies, str(HERE / file), exclude) + wheel_policies = WheelPolicies(libc=Libc.GLIBC, arch=Architecture.x86_64) + winfo = analyze_wheel_abi(wheel_policies, str(HERE / file), exclude, False) assert ( set(winfo.external_refs["manylinux_2_5_x86_64"]["libs"]) == external_libs ), f"{HERE}, {exclude}, {os.environ}" @@ -78,11 +79,12 @@ def test_analyze_wheel_abi(file, external_libs, exclude): def test_analyze_wheel_abi_pyfpe(): - wheel_policies = WheelPolicies(libc=Libc.GLIBC, arch="x86_64") + wheel_policies = WheelPolicies(libc=Libc.GLIBC, arch=Architecture.x86_64) winfo = analyze_wheel_abi( wheel_policies, str(HERE / "fpewheel-0.0.0-cp35-cp35m-linux_x86_64.whl"), frozenset(), + False, ) assert ( winfo.sym_tag == "manylinux_2_5_x86_64" @@ -120,6 +122,7 @@ def test_wheel_source_date_epoch(tmp_path, monkeypatch): WHEEL_DIR=str(wheel_output_path), WHEEL_FILE=[str(wheel_path)], EXCLUDE=[], + DISABLE_ISA_EXT_CHECK=False, cmd="repair", func=Mock(), prog="auditwheel", diff --git a/tests/integration/test_manylinux.py b/tests/integration/test_manylinux.py index f5b10503..9414602b 100644 --- a/tests/integration/test_manylinux.py +++ b/tests/integration/test_manylinux.py @@ -16,12 +16,13 @@ import pytest from elftools.elf.elffile import ELFFile -from auditwheel.policy import WheelPolicies, get_arch_name +from auditwheel.architecture import Architecture +from auditwheel.policy import WheelPolicies logger = logging.getLogger(__name__) ENCODING = "utf-8" -PLATFORM = get_arch_name() +PLATFORM = Architecture.get_native_architecture().value MANYLINUX1_IMAGE_ID = f"quay.io/pypa/manylinux1_{PLATFORM}:latest" MANYLINUX2010_IMAGE_ID = f"quay.io/pypa/manylinux2010_{PLATFORM}:latest" MANYLINUX2014_IMAGE_ID = f"quay.io/pypa/manylinux2014_{PLATFORM}:latest" @@ -189,11 +190,14 @@ def tmp_docker_image(base, commands, setup_env=None): client.images.remove(image.id) -def assert_show_output(manylinux_ctr, wheel, expected_tag, strict): - output = docker_exec(manylinux_ctr, f"auditwheel show /io/{wheel}") +def assert_show_output(manylinux_ctr, wheel, expected_tag, strict, isa_ext_check=True): + isa_ext_check_arg = "" if isa_ext_check else "--disable-isa-ext-check" + output = docker_exec( + manylinux_ctr, f"auditwheel show {isa_ext_check_arg} /io/{wheel}" + ) output = output.replace("\n", " ") match = SHOW_RE.match(output) - assert match + assert match, f"{SHOW_RE.pattern!r} not found in:\n{output}" assert match["wheel"] == wheel if strict or "musllinux" in expected_tag: assert match["tag"] == expected_tag @@ -218,7 +222,7 @@ def build_numpy(container, policy, output_dir): # https://github.com/numpy/numpy/issues/27932 fix_hwcap = "echo '#define HWCAP_S390_VX 2048' >> /usr/include/bits/hwcap.h" docker_exec(container, f'sh -c "{fix_hwcap}"') - elif policy.startswith("manylinux_2_28_"): + elif policy.startswith(("manylinux_2_28_", "manylinux_2_34_")): docker_exec(container, "dnf install -y openblas-devel") else: if tuple(int(part) for part in NUMPY_VERSION.split(".")[:2]) >= (1, 26): @@ -391,16 +395,18 @@ def test_build_wheel_with_binary_executable( orig_wheel = filenames[0] assert "manylinux" not in orig_wheel + # manylinux_2_34_x86_64 uses x86_64_v2 for this test + isa_ext_check = policy != "manylinux_2_34_x86_64" + isa_ext_check_arg = "" if isa_ext_check else "--disable-isa-ext-check" + # Repair the wheel using the appropriate manylinux container - repair_command = ( - f"auditwheel repair --plat {policy} --only-plat -w /io /io/{orig_wheel}" - ) + repair_command = f"auditwheel repair --plat {policy} {isa_ext_check_arg} --only-plat -w /io /io/{orig_wheel}" docker_exec(manylinux_ctr, repair_command) filenames = os.listdir(io_folder) assert len(filenames) == 2 repaired_wheel = f"testpackage-0.0.1-py3-none-{tag}.whl" assert repaired_wheel in filenames - assert_show_output(manylinux_ctr, repaired_wheel, policy, False) + assert_show_output(manylinux_ctr, repaired_wheel, policy, False, isa_ext_check) docker_exec(docker_python, "pip install /io/" + repaired_wheel) output = docker_exec( diff --git a/tests/unit/test_architecture.py b/tests/unit/test_architecture.py new file mode 100644 index 00000000..56d248bc --- /dev/null +++ b/tests/unit/test_architecture.py @@ -0,0 +1,93 @@ +import platform +import struct +import sys + +import pytest + +from auditwheel.architecture import Architecture + + +@pytest.mark.parametrize( + ("reported_arch", "expected_arch"), + [ + ("armv7l", Architecture.armv7l), + ("armv8l", Architecture.armv7l), + ("aarch64", Architecture.armv7l), + ("i686", Architecture.i686), + ("x86_64", Architecture.i686), + ], +) +def test_32bits_arch_name(reported_arch, expected_arch, monkeypatch): + monkeypatch.setattr(platform, "machine", lambda: reported_arch) + machine = Architecture.get_native_architecture(bits=32) + assert machine == expected_arch + + +@pytest.mark.parametrize( + ("reported_arch", "expected_arch"), + [ + ("armv8l", Architecture.aarch64), + ("aarch64", Architecture.aarch64), + ("ppc64le", Architecture.ppc64le), + ("i686", Architecture.x86_64), + ("x86_64", Architecture.x86_64), + ], +) +def test_64bits_arch_name(reported_arch, expected_arch, monkeypatch): + monkeypatch.setattr(platform, "machine", lambda: reported_arch) + machine = Architecture.get_native_architecture(bits=64) + assert machine == expected_arch + + +@pytest.mark.parametrize( + ("maxsize", "sizeof_voidp", "expected"), + [ + # 64-bit + (9223372036854775807, 8, Architecture.x86_64), + # 32-bit + (2147483647, 4, Architecture.i686), + # 64-bit w/ 32-bit sys.maxsize: GraalPy, IronPython, Jython + (2147483647, 8, Architecture.x86_64), + ], +) +def test_arch_name_bits(maxsize, sizeof_voidp, expected, monkeypatch): + def _calcsize(fmt): + assert fmt == "P" + return sizeof_voidp + + monkeypatch.setattr(platform, "machine", lambda: "x86_64") + monkeypatch.setattr(sys, "maxsize", maxsize) + monkeypatch.setattr(struct, "calcsize", _calcsize) + machine = Architecture.get_native_architecture() + assert machine == expected + + +@pytest.mark.parametrize( + ("smaller", "larger"), + [ + (Architecture.x86_64, Architecture.x86_64_v4), + (Architecture.x86_64, Architecture.x86_64), + (Architecture.x86_64, Architecture.x86_64_v2), + (Architecture.x86_64_v2, Architecture.x86_64_v3), + (Architecture.x86_64_v3, Architecture.x86_64_v4), + ], +) +def test_order_valid(smaller, larger): + assert smaller.is_subset(larger) + assert larger.is_superset(smaller) + + +@pytest.mark.parametrize( + ("smaller", "larger"), + [ + (Architecture.x86_64, Architecture.x86_64_v4), + (Architecture.x86_64, Architecture.x86_64_v2), + (Architecture.x86_64_v2, Architecture.x86_64_v3), + (Architecture.x86_64_v3, Architecture.x86_64_v4), + (Architecture.aarch64, Architecture.x86_64), + (Architecture.x86_64, Architecture.aarch64), + ], +) +def test_order_invalid(smaller, larger): + assert not smaller.is_superset(larger) + assert not larger.is_subset(smaller) diff --git a/tests/unit/test_policy.py b/tests/unit/test_policy.py index 6684f586..a65f477d 100644 --- a/tests/unit/test_policy.py +++ b/tests/unit/test_policy.py @@ -1,20 +1,17 @@ from __future__ import annotations -import platform import re -import struct -import sys from contextlib import nullcontext as does_not_raise import pytest +from auditwheel.architecture import Architecture from auditwheel.error import InvalidLibc -from auditwheel.lddtree import DynamicExecutable, DynamicLibrary +from auditwheel.lddtree import DynamicExecutable, DynamicLibrary, Platform from auditwheel.libc import Libc from auditwheel.policy import ( WheelPolicies, _validate_pep600_compliance, - get_arch_name, get_libc, get_replace_platforms, ) @@ -36,62 +33,6 @@ def raises(exception, match=None, escape=True): return pytest.raises(exception, match=match) -@pytest.mark.parametrize( - ("reported_arch", "expected_arch"), - [ - ("armv6l", "armv6l"), - ("armv7l", "armv7l"), - ("armv8l", "armv7l"), - ("aarch64", "armv7l"), - ("i686", "i686"), - ("x86_64", "i686"), - ], -) -def test_32bits_arch_name(reported_arch, expected_arch, monkeypatch): - monkeypatch.setattr(platform, "machine", lambda: reported_arch) - machine = get_arch_name(bits=32) - assert machine == expected_arch - - -@pytest.mark.parametrize( - ("reported_arch", "expected_arch"), - [ - ("armv8l", "aarch64"), - ("aarch64", "aarch64"), - ("ppc64le", "ppc64le"), - ("i686", "x86_64"), - ("x86_64", "x86_64"), - ], -) -def test_64bits_arch_name(reported_arch, expected_arch, monkeypatch): - monkeypatch.setattr(platform, "machine", lambda: reported_arch) - machine = get_arch_name(bits=64) - assert machine == expected_arch - - -@pytest.mark.parametrize( - ("maxsize", "sizeof_voidp", "expected"), - [ - # 64-bit - (9223372036854775807, 8, "x86_64"), - # 32-bit - (2147483647, 4, "i686"), - # 64-bit w/ 32-bit sys.maxsize: GraalPy, IronPython, Jython - (2147483647, 8, "x86_64"), - ], -) -def test_arch_name_bits(maxsize, sizeof_voidp, expected, monkeypatch): - def _calcsize(fmt): - assert fmt == "P" - return sizeof_voidp - - monkeypatch.setattr(platform, "machine", lambda: "x86_64") - monkeypatch.setattr(sys, "maxsize", maxsize) - monkeypatch.setattr(struct, "calcsize", _calcsize) - machine = get_arch_name() - assert machine == expected - - @pytest.mark.parametrize( ("name", "expected"), [ @@ -196,19 +137,20 @@ def test_pep600_compliance(): class TestPolicyAccess: def test_get_by_priority(self): - _arch = get_arch_name() + arch = Architecture.get_native_architecture() wheel_policy = WheelPolicies() - assert wheel_policy.get_policy_name(65) == f"manylinux_2_27_{_arch}" - assert wheel_policy.get_policy_name(70) == f"manylinux_2_24_{_arch}" - assert wheel_policy.get_policy_name(80) == f"manylinux_2_17_{_arch}" - if _arch in {"x86_64", "i686"}: - assert wheel_policy.get_policy_name(90) == f"manylinux_2_12_{_arch}" - assert wheel_policy.get_policy_name(100) == f"manylinux_2_5_{_arch}" - assert wheel_policy.get_policy_name(0) == f"linux_{_arch}" + assert wheel_policy.get_policy_name(65) == f"manylinux_2_27_{arch}" + assert wheel_policy.get_policy_name(70) == f"manylinux_2_24_{arch}" + assert wheel_policy.get_policy_name(80) == f"manylinux_2_17_{arch}" + if arch in {Architecture.x86_64, Architecture.i686}: + assert wheel_policy.get_policy_name(90) == f"manylinux_2_12_{arch}" + assert wheel_policy.get_policy_name(100) == f"manylinux_2_5_{arch}" + assert wheel_policy.get_policy_name(0) == f"linux_{arch}" def test_get_by_priority_missing(self): wheel_policy = WheelPolicies() - assert wheel_policy.get_policy_name(101) is None + with pytest.raises(LookupError): + wheel_policy.get_policy_name(101) def test_get_by_priority_duplicate(self): wheel_policy = WheelPolicies() @@ -220,21 +162,22 @@ def test_get_by_priority_duplicate(self): wheel_policy.get_policy_name(0) def test_get_by_name(self): - _arch = get_arch_name() + arch = Architecture.get_native_architecture() wheel_policy = WheelPolicies() - assert wheel_policy.get_priority_by_name(f"manylinux_2_27_{_arch}") == 65 - assert wheel_policy.get_priority_by_name(f"manylinux_2_24_{_arch}") == 70 - assert wheel_policy.get_priority_by_name(f"manylinux2014_{_arch}") == 80 - assert wheel_policy.get_priority_by_name(f"manylinux_2_17_{_arch}") == 80 - if _arch in {"x86_64", "i686"}: - assert wheel_policy.get_priority_by_name(f"manylinux2010_{_arch}") == 90 - assert wheel_policy.get_priority_by_name(f"manylinux_2_12_{_arch}") == 90 - assert wheel_policy.get_priority_by_name(f"manylinux1_{_arch}") == 100 - assert wheel_policy.get_priority_by_name(f"manylinux_2_5_{_arch}") == 100 + assert wheel_policy.get_priority_by_name(f"manylinux_2_27_{arch}") == 65 + assert wheel_policy.get_priority_by_name(f"manylinux_2_24_{arch}") == 70 + assert wheel_policy.get_priority_by_name(f"manylinux2014_{arch}") == 80 + assert wheel_policy.get_priority_by_name(f"manylinux_2_17_{arch}") == 80 + if arch in {Architecture.x86_64, Architecture.i686}: + assert wheel_policy.get_priority_by_name(f"manylinux2010_{arch}") == 90 + assert wheel_policy.get_priority_by_name(f"manylinux_2_12_{arch}") == 90 + assert wheel_policy.get_priority_by_name(f"manylinux1_{arch}") == 100 + assert wheel_policy.get_priority_by_name(f"manylinux_2_5_{arch}") == 100 def test_get_by_name_missing(self): wheel_policy = WheelPolicies() - assert wheel_policy.get_priority_by_name("nosuchpolicy") is None + with pytest.raises(LookupError): + wheel_policy.get_priority_by_name("nosuchpolicy") def test_get_by_name_duplicate(self): wheel_policy = WheelPolicies() @@ -262,11 +205,11 @@ def test_filter_libs(self): ] unfiltered_libs = ["libfoo.so.1.0", "libbar.so.999.999.999"] libs = filtered_libs + unfiltered_libs - lddtree = DynamicExecutable( interpreter=None, path="/path/to/lib", realpath="/path/to/lib", + platform=Platform("", 64, True, "EM_X86_64", "x86_64", None, None), needed=frozenset(libs), libraries={ lib: DynamicLibrary(lib, f"/path/to/{lib}", f"/path/to/{lib}") @@ -293,7 +236,7 @@ def test_filter_libs(self): (Libc.GLIBC, None, None, does_not_raise()), (Libc.MUSL, "musllinux_1_1", None, does_not_raise()), (None, "musllinux_1_1", None, does_not_raise()), - (None, None, "aarch64", does_not_raise()), + (None, None, Architecture.aarch64, does_not_raise()), # invalid ( Libc.GLIBC, @@ -303,7 +246,6 @@ def test_filter_libs(self): ), (Libc.MUSL, "manylinux_1_1", None, raises(ValueError, "Invalid 'musl_policy'")), (Libc.MUSL, "musllinux_5_1", None, raises(AssertionError)), - (Libc.MUSL, "musllinux_1_1", "foo", raises(AssertionError)), # platform dependant ( Libc.MUSL, @@ -322,4 +264,4 @@ def test_wheel_policies_args(libc, musl_policy, arch, exception): if musl_policy is not None: assert wheel_policies._musl_policy == musl_policy if arch is not None: - assert wheel_policies._arch_name == arch + assert wheel_policies.architecture == arch From 18286795dd196089f6e86582afc29f3afe1c0574 Mon Sep 17 00:00:00 2001 From: mayeut Date: Sun, 2 Feb 2025 16:03:20 +0100 Subject: [PATCH 03/10] add missing unit tests --- tests/unit/test_architecture.py | 36 ++++++++++++++++----------- tests/unit/test_json.py | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 tests/unit/test_json.py diff --git a/tests/unit/test_architecture.py b/tests/unit/test_architecture.py index 56d248bc..ea7b8dfa 100644 --- a/tests/unit/test_architecture.py +++ b/tests/unit/test_architecture.py @@ -8,32 +8,40 @@ @pytest.mark.parametrize( - ("reported_arch", "expected_arch"), + ("sys_platform", "reported_arch", "expected_arch"), [ - ("armv7l", Architecture.armv7l), - ("armv8l", Architecture.armv7l), - ("aarch64", Architecture.armv7l), - ("i686", Architecture.i686), - ("x86_64", Architecture.i686), + ("linux", "armv7l", Architecture.armv7l), + ("linux", "armv8l", Architecture.armv7l), + ("linux", "aarch64", Architecture.armv7l), + ("linux", "i686", Architecture.i686), + ("linux", "x86_64", Architecture.i686), + ("win32", "x86", Architecture.i686), + ("win32", "AMD64", Architecture.i686), ], ) -def test_32bits_arch_name(reported_arch, expected_arch, monkeypatch): +def test_32bits_arch_name(sys_platform, reported_arch, expected_arch, monkeypatch): + monkeypatch.setattr(sys, "platform", sys_platform) monkeypatch.setattr(platform, "machine", lambda: reported_arch) machine = Architecture.get_native_architecture(bits=32) assert machine == expected_arch @pytest.mark.parametrize( - ("reported_arch", "expected_arch"), + ("sys_platform", "reported_arch", "expected_arch"), [ - ("armv8l", Architecture.aarch64), - ("aarch64", Architecture.aarch64), - ("ppc64le", Architecture.ppc64le), - ("i686", Architecture.x86_64), - ("x86_64", Architecture.x86_64), + ("linux", "armv8l", Architecture.aarch64), + ("linux", "aarch64", Architecture.aarch64), + ("linux", "ppc64le", Architecture.ppc64le), + ("linux", "i686", Architecture.x86_64), + ("linux", "x86_64", Architecture.x86_64), + ("darwin", "arm64", Architecture.aarch64), + ("darwin", "x86_64", Architecture.x86_64), + ("win32", "ARM64", Architecture.aarch64), + ("win32", "AMD64", Architecture.x86_64), ], ) -def test_64bits_arch_name(reported_arch, expected_arch, monkeypatch): +def test_64bits_arch_name(sys_platform, reported_arch, expected_arch, monkeypatch): + monkeypatch.setattr(sys, "platform", sys_platform) monkeypatch.setattr(platform, "machine", lambda: reported_arch) machine = Architecture.get_native_architecture(bits=64) assert machine == expected_arch diff --git a/tests/unit/test_json.py b/tests/unit/test_json.py new file mode 100644 index 00000000..ad23f72f --- /dev/null +++ b/tests/unit/test_json.py @@ -0,0 +1,44 @@ +from dataclasses import dataclass +from enum import Enum +from json import loads + +import pytest + +from auditwheel.json import dumps + + +def test_dataclass(): + + @dataclass(frozen=True) + class Dummy: + first: str = "val0" + second: int = 2 + + assert {"first": "val0", "second": 2} == loads(dumps(Dummy())) + + +def test_enum(): + + class Dummy(Enum): + value: str + + TEST = "dummy" + + def __repr__(self): + return self.value + + assert Dummy.TEST.value == loads(dumps(Dummy.TEST)) + + +def test_frozenset(): + obj = frozenset((3, 9, 6, 5, 21)) + data = loads(dumps(obj)) + assert data == sorted(obj) + + +def test_invalid_type(): + + class Dummy: + pass + with pytest.raises(TypeError): + dumps(Dummy()) From dcd0d0410c308a00f2c19a91fc861607e248bf63 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:03:49 +0000 Subject: [PATCH 04/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/unit/test_json.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_json.py b/tests/unit/test_json.py index ad23f72f..a5504fe8 100644 --- a/tests/unit/test_json.py +++ b/tests/unit/test_json.py @@ -8,17 +8,15 @@ def test_dataclass(): - @dataclass(frozen=True) class Dummy: first: str = "val0" second: int = 2 - assert {"first": "val0", "second": 2} == loads(dumps(Dummy())) + assert loads(dumps(Dummy())) == {"first": "val0", "second": 2} def test_enum(): - class Dummy(Enum): value: str @@ -37,8 +35,8 @@ def test_frozenset(): def test_invalid_type(): - class Dummy: pass + with pytest.raises(TypeError): dumps(Dummy()) From 12a2b7fee1d720c24d34e82c6df8545535cad52b Mon Sep 17 00:00:00 2001 From: mayeut Date: Sun, 2 Feb 2025 17:12:36 +0100 Subject: [PATCH 05/10] add tests --- tests/integration/test_manylinux.py | 53 ++++++++++++++++++++- tests/integration/testdependencies/setup.py | 8 +++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_manylinux.py b/tests/integration/test_manylinux.py index 9414602b..a2d4f7bf 100644 --- a/tests/integration/test_manylinux.py +++ b/tests/integration/test_manylinux.py @@ -90,7 +90,7 @@ NUMPY_VERSION = NUMPY_VERSION_MAP[PYTHON_ABI_MAJ_MIN] ORIGINAL_NUMPY_WHEEL = f"numpy-{NUMPY_VERSION}-{PYTHON_ABI}-linux_{PLATFORM}.whl" SHOW_RE = re.compile( - r'[\s](?P\S+) is consistent with the following platform tag: "(?P\S+)"', + r'.*[\s](?P\S+) is consistent with the following platform tag: "(?P\S+)".*', flags=re.DOTALL, ) TAG_RE = re.compile(r"^manylinux_(?P[0-9]+)_(?P[0-9]+)_(?P\S+)$") @@ -818,6 +818,57 @@ def test_glibcxx_3_4_25(self, any_manylinux_container, docker_python, io_folder) ], ) + @pytest.mark.skipif( + PLATFORM != "x86_64", reason="ISA extension only implemented on x86_64" + ) + @pytest.mark.parametrize("isa_ext", ["x86-64-v2", "x86-64-v3", "x86-64-v4"]) + def test_isa_variants(self, any_manylinux_container, io_folder, isa_ext): + policy, tag, manylinux_ctr = any_manylinux_container + if policy.startswith(("manylinux_2_5_", "manylinux_2_12_", "manylinux_2_17_")): + pytest.skip("skip old gcc") + build_command = ( + "cd /auditwheel_src/tests/integration/testdependencies && " + "if [ -d ./build ]; then rm -rf ./build ./*.egg-info; fi && " + f"WITH_DEPENDENCY=1 WITH_ARCH={isa_ext} python -m pip wheel --no-deps -w /io ." + ) + docker_exec( + manylinux_ctr, + [ + "bash", + "-c", + build_command, + ], + ) + + filenames = os.listdir(io_folder) + orig_wheel = filenames[0] + assert "manylinux" not in orig_wheel + + # repair failure with ISA check + repair_command = ( + "LD_LIBRARY_PATH=" + "/auditwheel_src/tests/integration/testdependencies:$LD_LIBRARY_PATH " + f"auditwheel repair --plat {policy} -w /io /io/{orig_wheel}" + ) + with pytest.raises(CalledProcessError): + docker_exec(manylinux_ctr, ["bash", "-c", repair_command]) + + repair_command = ( + "LD_LIBRARY_PATH=" + "/auditwheel_src/tests/integration/testdependencies:$LD_LIBRARY_PATH " + f"auditwheel repair --disable-isa-ext-check --plat {policy} -w /io /io/{orig_wheel}" + ) + docker_exec(manylinux_ctr, ["bash", "-c", repair_command]) + + filenames = os.listdir(io_folder) + assert len(filenames) == 2 + repaired_wheel = f"testdependencies-0.0.1-{PYTHON_ABI}-{policy}.whl" + assert repaired_wheel in filenames + assert_show_output(manylinux_ctr, repaired_wheel, policy, True, False) + + # with ISA check, we shall not report a manylinux/musllinux policy + assert_show_output(manylinux_ctr, repaired_wheel, f"linux_{PLATFORM}", True) + class TestManylinux(Anylinux): @pytest.fixture(scope="session") diff --git a/tests/integration/testdependencies/setup.py b/tests/integration/testdependencies/setup.py index 9dcf8216..b4f76b3e 100644 --- a/tests/integration/testdependencies/setup.py +++ b/tests/integration/testdependencies/setup.py @@ -9,18 +9,23 @@ define_macros = [("_GNU_SOURCE", None)] libraries = [] library_dirs = [] +extra_compile_args = [] if getenv("WITH_DEPENDENCY", "0") == "1": libraries.append("dependency") library_dirs.append(path.abspath(path.dirname(__file__))) define_macros.append(("WITH_DEPENDENCY", "1")) +if getenv("WITH_ARCH", "") != "": + extra_compile_args.extend((f"-march={getenv('WITH_ARCH')}", "-mneeded")) + libraries.extend(["m", "c"]) class BuildExt(build_ext): def run(self) -> None: - cmd = "gcc -shared -fPIC -D_GNU_SOURCE dependency.c -o libdependency.so -lm -lc" + cflags = ("-shared", "-fPIC", "-D_GNU_SOURCE", *extra_compile_args) + cmd = f"gcc {' '.join(cflags)} dependency.c -o libdependency.so -lm -lc" subprocess.check_call(cmd.split()) super().run() @@ -33,6 +38,7 @@ def run(self) -> None: Extension( "testdependencies", sources=["testdependencies.c"], + extra_compile_args=extra_compile_args, define_macros=define_macros, libraries=libraries, library_dirs=library_dirs, From 7fb0446aef261de1516865aa544cd9b47cc11788 Mon Sep 17 00:00:00 2001 From: mayeut Date: Sun, 2 Feb 2025 18:17:59 +0100 Subject: [PATCH 06/10] fix tests on musllinux --- .../integration/testdependencies/dependency.c | 12 +++++++++-- .../testdependencies/testdependencies.c | 20 +++++++++++++------ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/tests/integration/testdependencies/dependency.c b/tests/integration/testdependencies/dependency.c index d1be1444..09055cb8 100644 --- a/tests/integration/testdependencies/dependency.c +++ b/tests/integration/testdependencies/dependency.c @@ -4,13 +4,17 @@ #include #include #include -#if defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 28) +#if defined(__GLIBC_PREREQ) +#if __GLIBC_PREREQ(2, 28) #include #endif +#endif int dep_run() { -#if defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 34) +#if defined(__GLIBC_PREREQ) + +#if __GLIBC_PREREQ(2, 34) // pthread_mutexattr_init was moved to libc.so.6 in manylinux_2_34+ pthread_mutexattr_t attr; int sts = pthread_mutexattr_init(&attr); @@ -29,4 +33,8 @@ int dep_run() #else return 0; #endif + +#else + return 0; +#endif } diff --git a/tests/integration/testdependencies/testdependencies.c b/tests/integration/testdependencies/testdependencies.c index b201f4a9..e28a0108 100644 --- a/tests/integration/testdependencies/testdependencies.c +++ b/tests/integration/testdependencies/testdependencies.c @@ -6,10 +6,12 @@ #include #include #include -#if defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 28) +#if defined(__GLIBC_PREREQ) +#if __GLIBC_PREREQ(2, 28) #include #endif #endif +#endif #include static __thread int tres = 0; @@ -24,21 +26,27 @@ run(PyObject *self, PyObject *args) #ifdef WITH_DEPENDENCY res = dep_run(); -#elif defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 34) +#elif defined(__GLIBC_PREREQ) + +#if __GLIBC_PREREQ(2, 34) // pthread_mutexattr_init was moved to libc.so.6 in manylinux_2_34+ pthread_mutexattr_t attr; res = pthread_mutexattr_init(&attr); if (res == 0) { pthread_mutexattr_destroy(&attr); } -#elif defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 28) +#elif __GLIBC_PREREQ(2, 28) res = thrd_equal(thrd_current(), thrd_current()) ? 0 : 1; -#elif defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 24) +#elif __GLIBC_PREREQ(2, 24) res = (int)nextupf(0.0F); -#elif defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 17) +#elif __GLIBC_PREREQ(2, 17) res = (int)(intptr_t)secure_getenv("NON_EXISTING_ENV_VARIABLE"); -#elif defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 10) +#elif __GLIBC_PREREQ(2, 10) res = malloc_info(0, stdout); +#else + res = 0; +#endif + #else res = 0; #endif From d16b99fbb47a6dff6c0d2b8093aff9c279eee6ce Mon Sep 17 00:00:00 2001 From: mayeut Date: Sun, 2 Feb 2025 19:13:36 +0100 Subject: [PATCH 07/10] Add a test when no ELF binary matches the policy architecture --- src/auditwheel/main_repair.py | 2 +- src/auditwheel/main_show.py | 2 +- src/auditwheel/wheel_abi.py | 35 ++++++++++++++++-------- tests/integration/test_bundled_wheels.py | 13 ++++++++- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/auditwheel/main_repair.py b/src/auditwheel/main_repair.py index a1051bc0..41268dfe 100644 --- a/src/auditwheel/main_repair.py +++ b/src/auditwheel/main_repair.py @@ -134,7 +134,7 @@ def execute(args, parser: argparse.ArgumentParser): wheel_policy, wheel_file, exclude, args.DISABLE_ISA_EXT_CHECK ) except NonPlatformWheel: - logger.info(NonPlatformWheel.LOG_MESSAGE) + logger.info(NonPlatformWheel.message) return 1 policy = wheel_policy.get_policy_by_name(args.PLAT) diff --git a/src/auditwheel/main_show.py b/src/auditwheel/main_show.py index c88537bf..1b585bb5 100644 --- a/src/auditwheel/main_show.py +++ b/src/auditwheel/main_show.py @@ -47,7 +47,7 @@ def execute(args, parser: argparse.ArgumentParser): wheel_policy, args.WHEEL_FILE, frozenset(), args.DISABLE_ISA_EXT_CHECK ) except NonPlatformWheel: - logger.info(NonPlatformWheel.LOG_MESSAGE) + logger.info(NonPlatformWheel.message) return 1 libs_with_versions = [ diff --git a/src/auditwheel/wheel_abi.py b/src/auditwheel/wheel_abi.py index 51da38fc..2032c76f 100644 --- a/src/auditwheel/wheel_abi.py +++ b/src/auditwheel/wheel_abi.py @@ -12,6 +12,7 @@ from typing import Any from . import json +from .architecture import Architecture from .elfutils import ( elf_file_filter, elf_find_ucs2_symbols, @@ -46,11 +47,26 @@ class WheelAbiError(Exception): class NonPlatformWheel(WheelAbiError): """No ELF binaries in the wheel""" - LOG_MESSAGE = ( - "This does not look like a platform wheel, no ELF executable " - "or shared library file (including compiled Python C extension) " - "found in the wheel archive" - ) + def __init__(self, architecture: Architecture, libraries: list[str]) -> None: + if not libraries: + msg = ( + "This does not look like a platform wheel, no ELF executable " + "or shared library file (including compiled Python C extension) " + "found in the wheel archive" + ) + else: + libraries_str = "\n\t".join(libraries) + msg = ( + "Invalid binary wheel: no ELF executable or shared library file " + "(including compiled Python C extension) with a " + f"{architecture.value!r} architecure found. The following " + f"ELF files were found:\n\t{libraries_str}\n" + ) + super().__init__(msg) + + @property + def message(self): + return self.args[0] @functools.lru_cache @@ -136,14 +152,9 @@ def get_wheel_elfdata( raise RuntimeError(msg) if not platform_wheel: - if not shared_libraries_with_invalid_machine: - raise NonPlatformWheel - libraries = "\n\t".join(shared_libraries_with_invalid_machine) - msg = ( - "Invalid binary wheel, found the following shared library/libraries " - f"with a different target architecture:\n\t{libraries}\n" + raise NonPlatformWheel( + wheel_policy.architecture, shared_libraries_with_invalid_machine ) - raise NonPlatformWheel(msg) # Get a list of all external libraries needed by ELFs in the wheel. needed_libs = { diff --git a/tests/integration/test_bundled_wheels.py b/tests/integration/test_bundled_wheels.py index 2310bb0e..7b9fb18a 100644 --- a/tests/integration/test_bundled_wheels.py +++ b/tests/integration/test_bundled_wheels.py @@ -19,7 +19,7 @@ from auditwheel.architecture import Architecture from auditwheel.libc import Libc from auditwheel.policy import WheelPolicies -from auditwheel.wheel_abi import analyze_wheel_abi +from auditwheel.wheel_abi import NonPlatformWheel, analyze_wheel_abi HERE = Path(__file__).parent.resolve() @@ -94,6 +94,17 @@ def test_analyze_wheel_abi_pyfpe(): ) # but for having the pyfpe reference, it gets just linux +def test_analyze_wheel_abi_bad_architecture(): + wheel_policies = WheelPolicies(libc=Libc.GLIBC, arch=Architecture.aarch64) + with pytest.raises(NonPlatformWheel): + analyze_wheel_abi( + wheel_policies, + str(HERE / "fpewheel-0.0.0-cp35-cp35m-linux_x86_64.whl"), + frozenset(), + False, + ) + + @pytest.mark.skipif(platform.machine() != "x86_64", reason="only checked on x86_64") def test_wheel_source_date_epoch(tmp_path, monkeypatch): wheel_build_path = tmp_path / "wheel" From 2a7f4a232d48c0359891669e83fdc0658958ec40 Mon Sep 17 00:00:00 2001 From: mayeut Date: Sun, 2 Feb 2025 19:52:38 +0100 Subject: [PATCH 08/10] fix non platform wheel tests --- src/auditwheel/main_repair.py | 4 ++-- src/auditwheel/main_show.py | 4 ++-- src/auditwheel/wheel_abi.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/auditwheel/main_repair.py b/src/auditwheel/main_repair.py index 41268dfe..49255853 100644 --- a/src/auditwheel/main_repair.py +++ b/src/auditwheel/main_repair.py @@ -133,8 +133,8 @@ def execute(args, parser: argparse.ArgumentParser): wheel_abi = analyze_wheel_abi( wheel_policy, wheel_file, exclude, args.DISABLE_ISA_EXT_CHECK ) - except NonPlatformWheel: - logger.info(NonPlatformWheel.message) + except NonPlatformWheel as e: + logger.info(e.message) return 1 policy = wheel_policy.get_policy_by_name(args.PLAT) diff --git a/src/auditwheel/main_show.py b/src/auditwheel/main_show.py index 1b585bb5..45b50ea6 100644 --- a/src/auditwheel/main_show.py +++ b/src/auditwheel/main_show.py @@ -46,8 +46,8 @@ def execute(args, parser: argparse.ArgumentParser): winfo = analyze_wheel_abi( wheel_policy, args.WHEEL_FILE, frozenset(), args.DISABLE_ISA_EXT_CHECK ) - except NonPlatformWheel: - logger.info(NonPlatformWheel.message) + except NonPlatformWheel as e: + logger.info(e.message) return 1 libs_with_versions = [ diff --git a/src/auditwheel/wheel_abi.py b/src/auditwheel/wheel_abi.py index 2032c76f..85718d32 100644 --- a/src/auditwheel/wheel_abi.py +++ b/src/auditwheel/wheel_abi.py @@ -65,7 +65,7 @@ def __init__(self, architecture: Architecture, libraries: list[str]) -> None: super().__init__(msg) @property - def message(self): + def message(self) -> str: return self.args[0] From 46663f739d2c76a18ea59f1f5d28964916384405 Mon Sep 17 00:00:00 2001 From: mayeut Date: Wed, 5 Feb 2025 19:47:46 +0100 Subject: [PATCH 09/10] Add more architecture tests --- scripts/create-arch-wheels.sh | 27 ++++++++++++ src/auditwheel/lddtree.py | 2 + ...simple-0.0.1-cp313-cp313-linux_aarch64.whl | Bin 0 -> 8018 bytes ...tsimple-0.0.1-cp313-cp313-linux_armv5l.whl | Bin 0 -> 7694 bytes ...tsimple-0.0.1-cp313-cp313-linux_armv7l.whl | Bin 0 -> 7741 bytes ...estsimple-0.0.1-cp313-cp313-linux_i686.whl | Bin 0 -> 7262 bytes ...tsimple-0.0.1-cp313-cp313-linux_mips64.whl | Bin 0 -> 7371 bytes ...simple-0.0.1-cp313-cp313-linux_ppc64le.whl | Bin 0 -> 7938 bytes ...simple-0.0.1-cp313-cp313-linux_riscv64.whl | Bin 0 -> 7422 bytes ...stsimple-0.0.1-cp313-cp313-linux_s390x.whl | Bin 0 -> 7406 bytes ...tsimple-0.0.1-cp313-cp313-linux_x86_64.whl | Bin 0 -> 7429 bytes tests/integration/test_nonplatform_wheel.py | 40 +++++++++++++++++- 12 files changed, 68 insertions(+), 1 deletion(-) create mode 100755 scripts/create-arch-wheels.sh create mode 100644 tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_aarch64.whl create mode 100644 tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_armv5l.whl create mode 100644 tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_armv7l.whl create mode 100644 tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_i686.whl create mode 100644 tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_mips64.whl create mode 100644 tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_ppc64le.whl create mode 100644 tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_riscv64.whl create mode 100644 tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_s390x.whl create mode 100644 tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_x86_64.whl diff --git a/scripts/create-arch-wheels.sh b/scripts/create-arch-wheels.sh new file mode 100755 index 00000000..55c7f982 --- /dev/null +++ b/scripts/create-arch-wheels.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +# This script is used to create wheels for unsupported architectures +# in order to extend coverage and check errors with those. + +set -eux + +SCRIPT_DIR="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P)" +INTEGRATION_TEST_DIR="${SCRIPT_DIR}/../tests/integration" +mkdir -p "${INTEGRATION_TEST_DIR}/arch-wheels" + +# "mips64le" built with buildpack-deps:bookworm and renamed cp313-cp313 +# "386" "amd64" "arm/v5" "arm/v7" "arm64/v8" +for ARCH in "ppc64le" "riscv64" "s390x"; do + docker run --platform linux/${ARCH} -i --rm -v "${INTEGRATION_TEST_DIR}:/tests" debian:trixie-20250203 << "EOF" +# for, "arm/v5" QEMU will report armv7l, running on aarch64 will report aarch64, force armv5l/armv7l +case "$(dpkg --print-architecture)" in + armel) export _PYTHON_HOST_PLATFORM="linux-armv5l";; + armhf) export _PYTHON_HOST_PLATFORM="linux-armv7l";; + *) ;; +esac +DEBIAN_FRONTEND=noninteractive apt-get update +DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends gcc python3-pip python3-dev +python3 -m pip wheel --no-deps -w /tests/arch-wheels /tests/testsimple +EOF + +done diff --git a/src/auditwheel/lddtree.py b/src/auditwheel/lddtree.py index f0a69b7b..eaee9eb3 100644 --- a/src/auditwheel/lddtree.py +++ b/src/auditwheel/lddtree.py @@ -144,6 +144,8 @@ def _get_platform(elf: ELFFile) -> Platform: error_msg = "Invalid ARM EABI version for armv7l" elif (flags & E_FLAGS.EF_ARM_ABI_FLOAT_HARD) != E_FLAGS.EF_ARM_ABI_FLOAT_HARD: error_msg = "armv7l shall use hard-float" + if error_msg is not None: + base_arch = None return Platform( elf_osabi, diff --git a/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_aarch64.whl b/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_aarch64.whl new file mode 100644 index 0000000000000000000000000000000000000000..2c27726119cd50788e23ef6c4e859509fd049080 GIT binary patch literal 8018 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 P@_6Pv$~*7*zgGVb)S}XV literal 0 HcmV?d00001 diff --git a/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_armv5l.whl b/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_armv5l.whl new file mode 100644 index 0000000000000000000000000000000000000000..7c907faa548db09997422814436d0eac36e212af GIT binary patch literal 7694 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 NJL_58dL(~7{RhtnP8R?G literal 0 HcmV?d00001 diff --git a/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_armv7l.whl b/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_armv7l.whl new file mode 100644 index 0000000000000000000000000000000000000000..92da3c7d07181ee70025d5fec7af7ee9cbcdd528 GIT binary patch literal 7741 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 JqWss>{{kR*dbj`p literal 0 HcmV?d00001 diff --git a/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_mips64.whl b/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_mips64.whl new file mode 100644 index 0000000000000000000000000000000000000000..2e4992a3b79994a3b622d0a2f56550c4ed3a3531 GIT binary patch literal 7371 zcmb7}2T&B-x9$fdIAj4qBuT-d>H>FgvbPqnbn zxksKrKZtPCvbN#!wRMDfmKb6xU@2Lx>}XL58_J20?6g~|(>)Z24x{q0RVP*bUz?YM zTI&tmy|%Eo@nhe!2j9Fv+6!Wfvd*)t5C`1X5)+0B7!C7Qt1hpJnQndqK~gI zB~Q2a4vKC!1d>CZI8d%|FS{>(_F=7zw@;G8P2L51tC#--svCpK3~9e{t$8L~tIxV) z{1iqF>OYA^lz4$kjd?QC7iLgabw!f}aobb^=3GHKf%Y-_&HC$Whtqol3%!}Cj2H9k z_mIYn*Dmxw8q}pi&72J-CWcAa69j2UhP4wNx?lA*e?_o6B#&y&)r=m$!hU>N{fdk$ zUTT`IOu?d1g{tpaee8fWQf#qv;e!RMWw`+D$qq$pWo#Yk@)!F@8G>7nCt;c~o0^u& zi<%4DUA#7>ZridBri1t~EkEQ0JdnhQVf-AYul&Zuhwr~~b7c<-ou3AWj+!j_-atpk zF%J0`#lMA@`y6Grt$i&_9OyePjDKXMLIfWwsGRoS{A#4J9=DR!e6jRhs!#h{s(OFl zv}#LVf~t3rM2TCyu(YhayS`<$d#DbYnYLoxyQa&Qs6D$w!r|puS;`W6sMV0kOz{@F zil0WyZQ4t@G2+67mQ`x)&$nnN?8&qw!35rB7k!_6v?wFNuOmp;al9jhZWjW(Ej$0d zI3m5UR#Bt9A^C3GTYUY6X%)I25w+@LPl#@>*tO-u1O;SFpIy#UabDM|#MtGYRgbxf z{o=gTKUdWGMQ~YTsOnkBvQrvWP+@wfoF;m!_oLJ9Zv5V*+I(KF22Bf&ROL@INB_2x z(42H7S&hQhx|*z5o&D?I89DE)1C~18%rBQRzt7T-X*u9+vNJ4i6q>7>(zKxpSqKb4 zhoDyu@FP|PTkb2YT#=1xq1n`QEXT6zQPdls`o~swDLywYkp7CPdxcyJ{Cq27wE>sE zzO3w!FF8kiM51QtXmwXn7j0UjqR4E}0^Y8zhyM&JAAN~YAD+p;XLK}G&d_zyJ-yW^ zJv_O_{EB>uJ5X>~T-=S!(weGs9D!D*N@ocp^>bVl5Ptmh1^gABybhc;Ov1Uw+oPjuqdjsO3D(OyM{qlRu zl!!1VnbdR9Ct9N?NLJ#T9aUeA+lwv+VsRLQF#+oMyAUdVVxP@p$v||4I_*U2D3|#} z@ErS4_Pxc;P5pkLtpM;y5Uv$3g=mk}CdCk&vVw*>6hGh60&BF3*JS5sgi++N9}q?? zn=oy?wfB=AEZ_JF@r8>=aYxr+k4-yqDS$aFEDs^Hdi)b?1JYNkRF*7!BvIgfX-xrX zTXxnG7Vu&tNskrTH0$AjXm5k1b-Uh)#Rx(^q> zUg&x}jd8E!c$IpHKaR33XV2|{(^};Ts$7TLCi+o#D%4eS|9sbuJJrrnxWC4XceGUx zd~>j=05~#U_C$Kp*&@6mirwdWhbMP>T^&yJ_ee<0py*5$OhKk?9Cq}a?6p0 zG6Ok%KJAm~7aO;3VuijhG=B>Z!0SSAv ztkMa@c!yyS%kH=3fD50}D7*EAy`p0iCrh;0~L zJNu=8&5&mh4;CReC)jF?|Infx+|V8&Y*L?e7wU5$z9Q$Ue?V3Hb3>R0#|x zzwG`bgfy=3hQybkW7_!+k0U7e+ghVe{#aCR0G@C7Gsq z3xMo|&7U0j!wn;)`IVycT{P{dXNmwvbjjmn>P{fy(SR7_7v&PYk<$ff+6cRQIu9_mC`^G71nu(>ty8u$%gTi8$YTgp+<|aAx;8s1 z(a($|=2)PaH~mO6qsS^~^x8=BtYE6VOelPB>3J+%M4DItlj^SgV3v`6Ba8gm>hHV0 zqUPV_s47b-0co_~U-7$c;ORSrT1x;g!0k9*j>+wrG4_qhfae$>ph4w;D8dD;QFSH0 zPmhORkQdXxTh-n1L8;MqY0DUK1CI5}cadd@F4f50AOk5`L3z$B;ymvG1dI3ZJ7~GR z148Vc##A2HSB*mHqyfZrln$l1BrY>22~X4(j4(+qO;Ss~bkaz`YpB7D7`Dt^iZq7TmC~ zuPLx>i+HnZn}fPru~ z2MndyPAbIB)O&ioAbw5AS##!34cIrAmz1L1eo@B)W!~=KtI5Ln^FQz1_GvgDutq51E8FfxPhE|D-fSk>D@+a`~-1aqielTTK z*m1+OkzEOdrpB+^ndQucMvNFZI!c2&9Jia@wS(G4On6+?AQ3E&8fkzb4EG)oQxtTV z>Q!Y8qpxr%f>{W&j-ckInI3M2H#Sfi6AJ6wxJ?GW6F0{2NKmAkkO2F@vL z_e>Ft9oZk2T%|-;ANASzxLuL-ExouhiF2=UaW712xXKSs=hEV4cAKzLTRwHSFq2Mz zF7@c24wkxlU2*17gtI3eE?wVZZn zHj(+PVHy>c7LkxRCE*s2=i`APa*K+567k|`xhapIUR%x8uh18XZL{%5Eq3>I5v~){ zh<05RW+KksyQ8U{=W3?mE)A^OQH4!<|{p!6nTE2qU4_F9>1G12}JF|V7 zY9CwGyryPTzrc%zX2Y6&Ti-@a5>LmPxT`q#Di*6afgPd}xwOosZE<2utEb)C+S<%e zS|G0lztXY!oi#+(YGJ73gs1m(t*EpwSJWHv`EJvN4t-SmEp>B>!6#V+mmgK1aQ{Vc zCOT%!PQKz;rldu-JW|PM1;yyoSqtilX3Dscn=$KUXfAu?N;f^WWaIg|FxDcZFb;&;ge=;>vXAf_4KsnH-F9Wr)K-FZdlmW2Yg@tk;#!8{E;e1ljG;+Gz0|ek zzv{x2@-y!V0hxyGM_99GeNQXm_@Y6E>NM_1(@Eb-+NOA|5@DS@P8XsCjpg8H&*4d` z35g7*oO@uRcD@xM6X()Tq2`nG&F6qW)5}E!ZK2ZYKQCuLKwk)~%{Z3rve% z+v`ZzXLzv85vWCH&H=Z1w)sGpQ%aD(Av2LpO!2xi$V@SanoPkC(_!eg5TM`$2Pd59Y*BnYPSMe&@ z1k%!q&mA{fl%G^rE2Hc)WwOrjuxsAlvzrb3BKMP(HWRoOkPuYEsbsrx*T-0PE;hH5J<&IP_|H-yzq?J&*v|`sdHXT9CYV$X6JfTSbLRzBXkmO&#HB< zw(&6+0*dNhoJ}z-{><~CWxC)B?3POmYLlZ zZTlReQFbre_5*A37xVcm;FQU?=qH7O8MvJ686SCVwe%~dwyFqA*^FGDa0EM%C*NBS zNMmyxxb@^pLBS{J7^cieHGNdMwM&s4ofs@Imd6ssX-VyL&0~2vj zZR@n=^om~b5Qnpaq&caG&s~BZ3OL}WRds5R&Ijl4$b=4I)x!RpX=;NbI=$wVbnek|EV$P?}Kx15i$(h4&}UHy7$tBmeR<|s9oQ{=)=b?*(n`ULCW_- zhwBz~i52$MuNJ~1%Y|G$D3pUeg4ub+uhdJc-?%tBSI{Npr{qH<-GbkQ)e0_s@hE$r z%{CR2Np7>|Q6elwhu3?M)nHQ){5x^TPf1@$1Xw+Om*8Wj+?Ft2ls>mx43Ck1s|KKr${XS!` z^M0PYq2be&#p2ANPh%o)XoT@NEmP7iAp&*=`(a1I@Xyk;Qfi>6hYwylu6(jc?HH zL%vAU)(&UKG8%<5Xj~9^&5Cpio)}H~@$ZHpavf;&=dTh*2lr;y+3i)$d?NOs(Os%0 zr2_3qvn|}Y)%8P%n7YgJ<_mzRcogHOh{M!3g`GN%{yCF?)kT0pEe^u)&ds0G2`SQ} z60y$ltibMHZNJ`Ay_H?5m32F2WkYuh-1vmQU*(p9?WCU89-k9U<~&>_*N`%+^jYTJ zDcK(UPWsCaGW4#4R-SM3i}2a^>8c(D0W=@-nxj3=ubsE=C(1?cnwiMExUwJzDAkeS zG#>zq7_T4l)jPXUna~+MQ2^e|$f|-)W=syB1mrX$3cgkMgRu5cZ1$>WE2gc}pcNyh zSb&3&K>F#R$)MQwja$-R=A|_bi(iN*y!STUo_6=G6&mM8jpTL*#tq(207{Ebum(d5 zR8{C!(wvJOiZ|NMc&mWcQA$mj4@5=+wfZOWgtIE6P9gFND*Ohq3ruzpyOm;emd#e< zeb^7LCaP;AN9bI~ka68RP66$6B>jSgJ>9ovA7Y$G$Zw71IV+GT&F~$9-$p!q><;&L z6LldQbA<~zA-yBUwdT{u*7T8DWk!xBVy{UAUbLGhz5DE)#N8C@8E==VH6*r~&zO^fYmci?C~_1+e5!Akk?$Q^~D#HpAfd)b~Zvh?4#t2ks1u zcj#u^SSuM2qsqoAEGaK?`6}DrycD;XQd^6%jr)3cZK_g9N6_>M8nsqW9&?LPJxtp?7 zml7zt>?HjS%r@a#&i*sDN34kt;Z~_v(cFM!y4c14ZJYZMmW9=CK?dBu!O&#fWYCzu z*K=4#O`*=QsS&U&X8o3N>#E;f2WJ9^I^?JNdF4a-WJFglY;o0U4aM?C8~lE7>sY#Ogesfz-o!S4 zf&D3MqH*&*_IacXVqEJug4Hd0C&|x4c*!5O>$lR|(cv6(v3XvG;tbb2FLOZ!yz=$_ zel)DxAu~>mm8H#YS&~?o5er9EZ63O;eMc*d^^m7<(xxkKG5>7bciy`mC%EyA$BrZ? zoOCSvD}Z2H*)HJ5t2lwP%kg+5AJ&Zs6HRafzWQ0y9rC^(>pFONKHa$Kx8kKoI^0&Tk;eI*oeiM+ovzVpSxFS-!BPf zbzv(=Fa4G`zTB4lz`~yWKVu>KkFkLB&Y0vcLqYscu>TKZ0VE_KBmfq$g1fka;BRdl z1+|p*pC~=ie{!M)Qqxr9SDPW#0P&5DjMwT4O^K`@Ym8{s>GG(71bD|rKm%%9fx{1X z=yvFwaR7g3{L9Li$hZYr|H<_FPq6%ZMk9!_vL^93L~C^N?_4t?Kb&<&H9+e8gMu}> zJlY^$5YOQFIIrP^ki=~5gvi7u(a^Zy5Eq>Q*w;s5}-e}eVjk-ExHb##?{$6CAsg#eG&zT2`U@;qUXW=UDEAhNh) zcKPg1-v>O%r>PQ0TA6EHtkg#cC;Yomeop!mGdx17yHB*};c3A*DxQ9X_YWi3>om=X zw4fisHBj>YH>>NxAQAtWZ%Hd`yY4xaw8|1h;ykId^A=?6%G$a?+cV;-KNavvzE9*K zigj^@G|)9@s(u$rRKWOo(GEPDf56+FN`JvM^6B_?Vf3TV&KWBCCzX&n4XM|8};55&(!9Zrd#Y2e^e-}&D? zu|K=|U)O%XU$_5nhwPvD|I``(4F>=U15ExHaew3g*FO0t@IMLvZ{YC11OKZH{z>~! y8vUF0mhNB6`yYz^C-pzc^#4-71bqE>>VJ#&nFc<=U!UOr83lioPD%gQ?Ee63BH5e( literal 0 HcmV?d00001 diff --git a/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_ppc64le.whl b/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_ppc64le.whl new file mode 100644 index 0000000000000000000000000000000000000000..b799bc0ae395ad1ca98ef3afd90b7c5efb199669 GIT binary patch literal 7938 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 AKmY&$ literal 0 HcmV?d00001 diff --git a/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_riscv64.whl b/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_riscv64.whl new file mode 100644 index 0000000000000000000000000000000000000000..98b2a75998a04ced1bcdbf49ef308ad60b98280c GIT binary patch literal 7422 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 literal 0 HcmV?d00001 diff --git a/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_s390x.whl b/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_s390x.whl new file mode 100644 index 0000000000000000000000000000000000000000..a8fc3f72fea960ccdd072479dadf5519b28a1bbf GIT binary patch literal 7406 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 Z{~3T*%Y literal 0 HcmV?d00001 diff --git a/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_x86_64.whl b/tests/integration/arch-wheels/testsimple-0.0.1-cp313-cp313-linux_x86_64.whl new file mode 100644 index 0000000000000000000000000000000000000000..85b0408bf2c7c13d5ab3593b9ff0867aae92d626 GIT binary patch literal 7429 zcmai(1yCIAwyp;V?yd>$41+racXtaGY;YMoIKd?$xCcmp-~@Mfn878u4icQ;;bfmv zxBk0x>+IX#TJ=?TRX=NW*So5^`qflHdO-vL0MG!BALWesLIh@w$N&JzHvjf#V}J$2R9wL;puX7MZM(;|WsTqG8Rbgri8BQ^n4i1W_FzU(_ zyZ#d8(&~Ep@@7TsQM2#%(euQq<>>%z%hD&%dighpk~l}-eOvI&?5NMQXNW3pt+;dJ zbx+=8br^NI_G_*8xRqhm-6f$fhI{=nt4Oiy#L^aYm$-CVS;u6}+Q6EVEUi3g{Hk*6 ze2eKbx;8ov3?+unyiLOgBPn88dO9t~#2Dmkl{2&4ev%6ph5+BnEPfjuZdLkh&<@^P z%`qa+*p(7?RvjbLSAv^S+*KAyMZeKbA?+n~X5He6{+98@g!z~2Tctjpk8M=fL1w|# zMV&DV)NZqw;yCUgDeAhR68c8}^vd6*m20o^6Mr1C?_s8gOBmUer#W4}(pG8E{lDVs zS(LH*t}>;4@^T!JC1@+eNYIPHZvwYCYV4;D>2I>EuakjTc~ie$vCxVosxc>NNQvA$ zplzo;?%<;w0g_JGFS~;7<}kT0O+%L+*7&leRuo!?M>bwTqGcCr;q1F)-GtH zN1)}#hBu|%#Sd{<-3b|}VZCu4y>An8U?XWF73_gXA>3|%SWHcW=K(x|c6FXuDOaJ4 ze$sK(P(!EGH>^x^DEoin;8}khdbas=%Zdn9^p{FE?letnX0mrqmAKIcYg6oeoKTh@4;8e3hl zZDjO2XAl&5zg-2^SDUwj={sHDdfc>})NMy)eedK|s(^BMs05tT{yUyDnB1#%SLm@* zDJwX}d*-NI1)>I?W`(Ip?ApHja-Nhm3NM=n2PpTwqwLCIJ+WfC`=a5V?uqM)iMX=+2YP=(PFlvn#W^`D$j` z#W_XB0RdlTC$1^~fZyPd(MkjHO)xa&Y_!`vocRPhE}&OOEasudPTBGPK5anMfSrCR zg~Gg^p;oB2#&K%vu6?*F!GzNSyBMynKY7^+*f(dY9)R?Gao>Vpe}xf^@SFI07$_f% z;(r@JX3Xy1h2h(2|6{4UJ85@PQgiy_4{X>gv^idvQ0@VSP&c^#4ha`tbi@e?4au-Q zm>`;`UjGoeS2CWJg877<_HtmkN_yV#1ZzAUC*7!P$VyE^Y9H~`Mm!ZRS75U7xag=l zWd;+04Hz2(h0*1aDgy1pBj@w!kDNnDFxO~1kh_f@mb@O;5uh7Qlu3S>J!TOo<~rG= zZ2PyH?v&W8G=VQm?Xeakey?rYJm1{n%tpBEh#w&7M7UVR_Jcbjy4x2rL@T5>E^Db( z;5e6|JXh~Cc5z;Z+QHNA4HzI%MKpX@1NTNjkSNjGU6qOIsHvALik>Fcu%r`BN#W9;q3QN(pnN zE-(Rp+*gD&q$5WQ7VO4Eo^JFPtg-j# zY6yrO+9i5Zy9LDJCf%$f_=;SlP)7~5p(^Ib+#?BztyLqOIo&PJ6`UH4TS(_MG+UfEV=~_17AQM7%xxU4+9D8g9eN-W&_CV z6S?yx4Xii0_ymGHGy5$gX+~UJT z?DHgZ!wG!B(&yZx!7C^c0`xpu2$)jmEY2JXak&Q-xHsn_h(oAMwp7Cz+&7OC3J^I- zRM(|jUUKFrpNK1Zc|$}6NVtXz$2&AjWgbQJt)|8s@C~r9I?jY^q4f8aaL5C@rcyT1 z%Bdff^3>bz+co(VQt=mCr>?!m1P^RtFq^{%GM${qp7{;;oKYuZ)cmA^#)vgk#U4@) z6{)iqso|hjvj}BqFygZ1G7s)T-uKN-ne(_nQ{1hCuzd|MW~8e@zI~T&y=U<{;8Dm~ ziGI3Svb7a4=I!rKsCFt>{{4@l)J?GTl7U=ID6{>TV9isIlR_%4dQ(&HrPA^{vLE?G z$!4r7(oS1F!I|P}^ue;lf)O7wtQEG78Qi+;z{S|hrY{MGOqnJMd?P20;Q*^3NsCc! z#$?J8yaX48;xNh9#`u2IMNhI==oDN+wpEK}RY%@R3^;DLdCpD|fXCexwoCm9o__XbKZ?XnvihA~_f3vsFSu_^g#`s>d&LdqhHofU4g3tbbZtLI5vZQtGte4krA02X zSmhpa!$F8zpWfR8xO_*X;?)|gf3zo!eiU~$i&SJsp0+{giAjCM`D3twB+paan)`g^|;hVm3lu711a7%%0+Cx7$zf{L@-B|# zFqw3^v!G7EBLFPXZ5#>LPWhf#yCw}9oOAzx@B=QWHR+EMKhx1H;c6inDiO&igOyRd zTKWT(ylM3c+jJw!2(T)XPFwS_;kZatGkhX`&o;JX3Km7?M!A}3Y(5&&RvJs;WZB(u z1&wY@y}m-ok`bo11){?Bi|;5=s0xKnrKv5u@%@ny)44ZTp76ali?D>$&+VpR0w_5J zLwLY}5S=TPXo21k!HyxRDL(2~uDDmamf|7Tet3Lj+kM-)m{@~%XLvU%r5f`MxRD}4 zM9U}d#j!i6cXxZuZF5NIKkOqt@lnV-3k?ee5+fx18Bv+Vr?7@E#reHyFDI&NLOXF0 zavL~`!%J)M`)=pdo)N&Y@m{zC)Nf=&$(xG3hB4KeB}jUA>2!_XJ6HlR-t^4f3s79Y zHLWbEAq@TzeNH%^s2@=U;Rhr<@EWeo-CMG>S^M6?cA5K#KT$|+Ss}2zYQ2{tnoG(G z%*jmB#QOBvu^HF~qpY%#j1R&I)sUT!S|WL(oxS^1#g2e6C>@|qS40SR3&^%^1UtxF zX*2{4E}sl)4SH26Xz~*s_A2f5BDh)lyiq*um5Ic>x#JWQY3_=%3zH|of1)1L34xe7 z1w??pXqSu`D}(xUCc^9y1b<(#prxkCR;Ueu&LS>4xqk7hZ#hMU?6~YO0zD^^EV>mw zF39oU_a*(CHa{CrP>Jk;WvgE+qpDyFqHl8I=T+DESm&{}Gae)_9a};5V|Eo+cO>K= zr<(>Z$!Lw;{G5j<+C8{%ZSsaj%<8SRA4Q@gW7z9BeBI9idm0i}rQkJD70wZ_6qIUY zdXw7*A`v$8LSOd8iIW`LV2McQAYmm`XXXh8IMctEKcFJOszPg}gY(Q1qoV-j7fArp zBb>sEihco6)cDfFsF<4##Z}ke^tpbvWx7mp8HAP-70nVDtRwuy6zO)s&JCGHQKck= z>^fBiE3hAWp>HqMN1PXsB#Bt8CV>klxA!j57G?c2v?RJq$pinlvc6G8h9v=YHrl? zeS;&vQ6cObHv8Ni%K9aVn~W@Jk54U~cPV!%UV1!CPFR;h_kupQ)0kW?M@fzOE~nz_ z1s0>t@SX1G4|+pFDLl#rjKfje3mL*VI~|7fL`V#9wNXB=uoa_pk1P=qyQD$~>mgl6 z!lBb)Cvl)XVJuFCK)h*{WN-t%-wToOZ$@>_E0?N9;nqa-mEkN*mi9+kzpp)>&(0z( z=c)Vmp77})Z^cB7Y5B~@Gnb^RWWB>w)ig>xga^kpXX9mDt6Zdb>ukN5c~zM(Ws8b# z@7Bzv=U}#vaP*Pe3wn_b|c@1qZsn3S6eebvuhy?qagS(DiK0Qqs>&SajDu z3{5i>5uX#2Ss;RRI4|njdb@Qh_h-$fnbCl<kkH>CE#W6n99egjgs6rH#IGzrkpLUFCw6C2T{dk7Ba+ZVzSfxgm^*q z8pY#R&B?X5yLfsA7#O94&6HMO)8<+Blk3kt)uJ0+e4yfw^`rjY=q;-$Bz=% zCM59sV<#us8{_=s%Th@&(ur+L(acL?_F=8z<}2~=CRMcI3B&Ug$a|Y68qN@pywWIy z?oIUFAH8g>7HK&HB=g3hALb!P_|~1Rs0@~oKGnkXb4#_Obdj#c#_!R=NRKJv3vF08 z)nHHW)C-@Q!jFZ0;XSu4!1dGZn+Qn={6a04IcO_dTez`eg=GDiP=wi*UC!q6I3}7P(!V;3~ z!$Oxy1}V@LO=Fjo5_GA>qo7MT1vJ+mM@o*aX3JyPwG8a3>#`F+0x^ip`p>hU#tinb)n7l7LHP#-MUdy z`;M<^%0>-ifidv_RdC)fL~$ zBC?HLr#KOHJ_#M}`!r^)HDkNV#nBfQHQL%gJdOGq!-~qJlAV!kS|kiKIM8=P)Vj}s z4tut{3$H^tvs_5IJJ_r^*yC#6EUFieAff zycYSKIr`s_dvhYCKkJ5U^kPI~q((bW__MRn58L{aTXWZ95i6&46hxcUg6X2M2N3sz zBwiy&??kRX+4|%{15sDsu~(l@tGR&Yomx|KXpK(GANs&u?%Q|Y6JHDq44t0fn{`38 z)_abUgbqPgJXs?#d0@YcdqZLUP7T`W-5iZ^cda1izNEBMyJ*N;;wRVrcRBmVqU8BZ zbd!hM?Z1P}K#7lHeb)GyHQ7k;cOY2RZe5yqiW{oU<$YSFdQ z6~y29!S#Cnx+C22p}>E}BEK|+HxdOiKdUGGw047Y%*{Ue;Zu%Ki+=`JZcMxFYNTGr z(GE5e66J&7NVdO03?sh4GUlL4ir%;c__r7WwA zfkuQ`K6c6UD|87}$tqIO2?YnxZ9hd+ z*=XCwGNiamcx;mgQq(C{($=%ck~0)vEb&Ptrs2gwqb)zY5Yl>uLUm|SB!R-f>V+ki z7;faH%PyD@4~e0)AF`d3A1R(+LYb zefQ>Pba)Vpex~SkYB-JGawS;^U=puoOg%A zKIe60&km>Ml}yk6s84CWHbIq0>tHAp-x132SpN8Xxqtjt{m(y?{L%!o<8=Su%J)yj z7pq0PH%OlT=Vlg7;p*|*EVcI__&|U+Mgi$V0%1<)CDs+{zj`3rPYfQG zI-T9unk?tYpUF2gWL03Wd-RQ85Z=!$aIKaGUD(-f#EdizkLJG1Z3gLm?uwpy7Pz0h zD)|P>Uv=DNrbX)1@IK2$tLCHgbFgiJbh(dYd3QUJIuSGQn1;i6W)dqeyr7sQ!s z^n8!4AH?+VW4}|JQTPdeEjlQqc7^=8Hh|N^-{EEB#gko4a3Jx$f}ru;d%J>XkmkP` z6bSBySloY%34uRJ|0n-3D1cm?T%6pT)?iNv5bR>>3R08Pm6DaxmAX^|Dyb@QD9vK4 z0NEzTq2IN+rg^t6RL0dBv{{sZoUD`Mz+t7`urUfaIh@=B0r2k?F+=;|Hh(JF|4Ewv zT+u*LPEHjQs;D+G^-rx?-VG0p2^F9+#|WrSn?)VS3S=39LRs};T!M4oVZ5*%j8Q0P z>Y8I7n+2#mF``FaHijvKi?oEw&%7?rlx0D!H!(xyc92tgDXOU1*8S*Bv%@>_5)*h^DB-m62dP*3Jm!2>_4EPs_zUddr> zv`KKWu!OVPH^Z?@`^Dufy|xk-wh36naMV{{%7CS7|5HH04*JwXIE@ z{3*!+Rp50ke8$~aO#HcQSH-tcm<-|`agX*C+q$es76)DOtKf7ctMrSvVtll4Huii& z*ZJw@3`0-l3vvn6x|PnFw=N)w%oOdG%hl0$^nzg|W)p*~3m#|f|K%E0ObBqeC&smL} zW?*$p!I~-vh(t*LyQ%i)b^iD9GxT5EKMMZ;S!{oE|DJ{ap#lKKp`ZU)c>i$!*U9)B z__x&m0TPn`N8o=8|8Lsgs{Du6_@8P2rqREte+%>fQ0qg{DgI-R|07dP6=alu-GcmQ N<^0jM4&}ej{tLM^wO9ZE literal 0 HcmV?d00001 diff --git a/tests/integration/test_nonplatform_wheel.py b/tests/integration/test_nonplatform_wheel.py index 614643ac..c66c062e 100644 --- a/tests/integration/test_nonplatform_wheel.py +++ b/tests/integration/test_nonplatform_wheel.py @@ -5,11 +5,13 @@ import pytest +from auditwheel.architecture import Architecture + HERE = pathlib.Path(__file__).parent.resolve() @pytest.mark.parametrize("mode", ["repair", "show"]) -def test_non_platform_wheel_repair(mode): +def test_non_platform_wheel_pure(mode): wheel = HERE / "plumbum-1.6.8-py2.py3-none-any.whl" proc = subprocess.run( ["auditwheel", mode, str(wheel)], @@ -20,3 +22,39 @@ def test_non_platform_wheel_repair(mode): assert proc.returncode == 1 assert "This does not look like a platform wheel" in proc.stderr assert "AttributeError" not in proc.stderr + + +@pytest.mark.parametrize("mode", ["repair", "show"]) +@pytest.mark.parametrize("arch", ["armv5l", "mips64"]) +def test_non_platform_wheel_unknown_arch(mode, arch): + wheel = HERE / "arch-wheels" / f"testsimple-0.0.1-cp313-cp313-linux_{arch}.whl" + proc = subprocess.run( + ["auditwheel", mode, str(wheel)], + stderr=subprocess.PIPE, + text=True, + check=False, + ) + assert proc.returncode == 1 + assert "Invalid binary wheel: no ELF executable or" in proc.stderr + assert "unknown architecture" in proc.stderr + assert "AttributeError" not in proc.stderr + + +@pytest.mark.parametrize("mode", ["repair", "show"]) +@pytest.mark.parametrize( + "arch", ["aarch64", "armv7l", "i686", "x86_64", "ppc64le", "s390x"] +) +def test_non_platform_wheel_bad_arch(mode, arch): + if Architecture.get_native_architecture().value == arch: + pytest.skip("host architecture") + wheel = HERE / "arch-wheels" / f"testsimple-0.0.1-cp313-cp313-linux_{arch}.whl" + proc = subprocess.run( + ["auditwheel", mode, str(wheel)], + stderr=subprocess.PIPE, + text=True, + check=False, + ) + assert proc.returncode == 1 + assert "Invalid binary wheel: no ELF executable or" in proc.stderr + assert f"{arch} architecture" in proc.stderr + assert "AttributeError" not in proc.stderr From 037c72a0a2bf6febfcf994254afa90190ded59c0 Mon Sep 17 00:00:00 2001 From: mayeut Date: Sun, 9 Feb 2025 16:31:38 +0100 Subject: [PATCH 10/10] review --- src/auditwheel/json.py | 4 +++- src/auditwheel/lddtree.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/auditwheel/json.py b/src/auditwheel/json.py index cf11fabe..a4b8e2f7 100644 --- a/src/auditwheel/json.py +++ b/src/auditwheel/json.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import dataclasses import json from enum import Enum @@ -15,5 +17,5 @@ def _encode_value(value: Any) -> Any: raise TypeError(msg) -def dumps(obj: Any): +def dumps(obj: Any) -> str: return json.dumps(obj, indent=4, default=_encode_value) diff --git a/src/auditwheel/lddtree.py b/src/auditwheel/lddtree.py index eaee9eb3..b521130f 100644 --- a/src/auditwheel/lddtree.py +++ b/src/auditwheel/lddtree.py @@ -449,7 +449,6 @@ def ldd( needed: set[str] = set() rpaths: list[str] = [] runpaths: list[str] = [] - _excluded_libs: set[str] = set() with open(path, "rb") as f: elf = ELFFile(f) @@ -515,6 +514,7 @@ def ldd( + ldpaths["conf"] + ldpaths["interp"] ) + _excluded_libs: set[str] = set() for soname in needed: if soname in _all_libs: continue