Skip to content

Commit c3b99cf

Browse files
committed
feat: detect architecture/libc from wheel
1 parent 38ac457 commit c3b99cf

24 files changed

+765
-334
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,6 @@ wheelhoust-*
6565
tests/integration/testpackage/testpackage/testprogram
6666
tests/integration/testpackage/testpackage/testprogram_nodeps
6767
tests/integration/sample_extension/src/sample_extension.c
68+
69+
# Downloaded by test script
70+
tests/integration/patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.musllinux_1_1_x86_64.whl

noxfile.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,21 @@ def tests(session: nox.Session) -> None:
7373
dep_group = "coverage" if RUNNING_CI else "test"
7474
pyproject = nox.project.load_toml("pyproject.toml")
7575
deps = nox.project.dependency_groups(pyproject, dep_group)
76+
session.install("-U", "pip")
7677
session.install("-e", ".", *deps)
78+
# for tests/integration/test_bundled_wheels.py::test_analyze_wheel_abi_static_exe
79+
session.run(
80+
"pip",
81+
"download",
82+
"--only-binary",
83+
":all:",
84+
"--no-deps",
85+
"--platform",
86+
"manylinux1_x86_64",
87+
"-d",
88+
"./tests/integration/",
89+
"patchelf==0.17.2.1",
90+
)
7791
if RUNNING_CI:
7892
posargs.extend(["--cov", "auditwheel", "--cov-branch"])
7993
# pull manylinux images that will be used.

src/auditwheel/architecture.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def is_superset(self, other: Architecture) -> bool:
4949
return other.is_subset(self)
5050

5151
@staticmethod
52-
def get_native_architecture(*, bits: int | None = None) -> Architecture:
52+
def detect(*, bits: int | None = None) -> Architecture:
5353
machine = platform.machine()
5454
if sys.platform.startswith("win"):
5555
machine = {"AMD64": "x86_64", "ARM64": "aarch64", "x86": "i686"}.get(

src/auditwheel/error.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,39 @@
22

33

44
class AuditwheelException(Exception):
5-
pass
5+
def __init__(self, msg: str):
6+
super().__init__(msg)
7+
8+
@property
9+
def message(self) -> str:
10+
assert isinstance(self.args[0], str)
11+
return self.args[0]
612

713

814
class InvalidLibc(AuditwheelException):
915
pass
16+
17+
18+
class WheelToolsError(AuditwheelException):
19+
pass
20+
21+
22+
class NonPlatformWheel(AuditwheelException):
23+
"""No ELF binaries in the wheel"""
24+
25+
def __init__(self, architecture: str | None, libraries: list[str] | None) -> None:
26+
if architecture is None or not libraries:
27+
msg = (
28+
"This does not look like a platform wheel, no ELF executable "
29+
"or shared library file (including compiled Python C extension) "
30+
"found in the wheel archive"
31+
)
32+
else:
33+
libraries_str = "\n\t".join(libraries)
34+
msg = (
35+
"Invalid binary wheel: no ELF executable or shared library file "
36+
"(including compiled Python C extension) with a "
37+
f"{architecture!r} architecure found. The following "
38+
f"ELF files were found:\n\t{libraries_str}\n"
39+
)
40+
super().__init__(msg)

src/auditwheel/lddtree.py

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
from elftools.elf.sections import NoteSection
2828

2929
from .architecture import Architecture
30-
from .libc import Libc, get_libc
30+
from .error import InvalidLibc
31+
from .libc import Libc
3132

3233
log = logging.getLogger(__name__)
3334
__all__ = ["DynamicExecutable", "DynamicLibrary", "ldd"]
@@ -80,6 +81,7 @@ class DynamicLibrary:
8081
@dataclass(frozen=True)
8182
class DynamicExecutable:
8283
interpreter: str | None
84+
libc: Libc | None
8385
path: str
8486
realpath: Path
8587
platform: Platform
@@ -295,7 +297,9 @@ def parse_ld_so_conf(ldso_conf: str, root: str = "/", _first: bool = True) -> li
295297

296298

297299
@functools.lru_cache
298-
def load_ld_paths(root: str = "/", prefix: str = "") -> dict[str, list[str]]:
300+
def load_ld_paths(
301+
libc: Libc | None, root: str = "/", prefix: str = ""
302+
) -> dict[str, list[str]]:
299303
"""Load linker paths from common locations
300304
301305
This parses the ld.so.conf and LD_LIBRARY_PATH env var.
@@ -323,7 +327,6 @@ def load_ld_paths(root: str = "/", prefix: str = "") -> dict[str, list[str]]:
323327
# on a per-ELF basis so it can get turned into the right thing.
324328
ldpaths["env"] = parse_ld_paths(env_ldpath, path="")
325329

326-
libc = get_libc()
327330
if libc == Libc.MUSL:
328331
# from https://git.musl-libc.org/cgit/musl/tree/ldso
329332
# /dynlink.c?id=3f701faace7addc75d16dea8a6cd769fa5b3f260#n1063
@@ -436,31 +439,43 @@ def ldd(
436439
},
437440
}
438441
"""
439-
if not ldpaths:
440-
ldpaths = load_ld_paths().copy()
441-
442442
_first = _all_libs is None
443443
if _all_libs is None:
444444
_all_libs = {}
445445

446446
log.debug("ldd(%s)", path)
447447

448448
interpreter: str | None = None
449+
libc: Libc | None = None
449450
needed: set[str] = set()
450451
rpaths: list[str] = []
451452
runpaths: list[str] = []
452453

453454
with open(path, "rb") as f:
454455
elf = ELFFile(f)
456+
457+
# get the platform
458+
platform = _get_platform(elf)
459+
455460
# If this is the first ELF, extract the interpreter.
456461
if _first:
457462
for segment in elf.iter_segments():
458463
if segment.header.p_type != "PT_INTERP":
459464
continue
460-
461465
interp = segment.get_interp_name()
462466
log.debug(" interp = %s", interp)
463467
interpreter = normpath(root + interp)
468+
soname = os.path.basename(interpreter)
469+
_all_libs[soname] = DynamicLibrary(
470+
soname,
471+
interpreter,
472+
Path(readlink(interpreter, root, prefixed=True)),
473+
platform,
474+
)
475+
# if we have an interpreter and it's not MUSL, assume GLIBC
476+
libc = Libc.MUSL if soname.startswith("ld-musl-") else Libc.GLIBC
477+
if ldpaths is None:
478+
ldpaths = load_ld_paths(libc).copy()
464479
# XXX: Should read it and scan for /lib paths.
465480
ldpaths["interp"] = [
466481
normpath(root + os.path.dirname(interp)),
@@ -471,14 +486,10 @@ def ldd(
471486
log.debug(" ldpaths[interp] = %s", ldpaths["interp"])
472487
break
473488

474-
# get the platform
475-
platform = _get_platform(elf)
476-
477489
# Parse the ELF's dynamic tags.
478490
for segment in elf.iter_segments():
479491
if segment.header.p_type != "PT_DYNAMIC":
480492
continue
481-
482493
for t in segment.iter_tags():
483494
if t.entry.d_tag == "DT_RPATH":
484495
rpaths = parse_ld_paths(t.rpath, path=str(path), root=root)
@@ -497,14 +508,31 @@ def ldd(
497508
del elf
498509

499510
if _first:
511+
# get the libc based on dependencies
512+
for soname in needed:
513+
if soname.startswith("libc.musl-"):
514+
if libc is None:
515+
libc = Libc.MUSL
516+
if libc != Libc.MUSL:
517+
msg = f"found a dependency on MUSL but the libc is already set to {libc}"
518+
raise InvalidLibc(msg)
519+
elif soname == "libc.so.6":
520+
if libc is None:
521+
libc = Libc.GLIBC
522+
if libc != Libc.GLIBC:
523+
msg = f"found a dependency on GLIBC but the libc is already set to {libc}"
524+
raise InvalidLibc(msg)
525+
if ldpaths is None:
526+
ldpaths = load_ld_paths(libc).copy()
500527
# Propagate the rpaths used by the main ELF since those will be
501528
# used at runtime to locate things.
502529
ldpaths["rpath"] = rpaths
503530
ldpaths["runpath"] = runpaths
504531
log.debug(" ldpaths[rpath] = %s", rpaths)
505532
log.debug(" ldpaths[runpath] = %s", runpaths)
506533

507-
# Search for the libs this ELF uses.
534+
assert ldpaths is not None
535+
508536
all_ldpaths = (
509537
ldpaths["rpath"]
510538
+ rpaths
@@ -541,17 +569,9 @@ def ldd(
541569
dependency.needed,
542570
)
543571

544-
if interpreter is not None:
545-
soname = os.path.basename(interpreter)
546-
_all_libs[soname] = DynamicLibrary(
547-
soname,
548-
interpreter,
549-
Path(readlink(interpreter, root, prefixed=True)),
550-
platform,
551-
)
552-
553572
return DynamicExecutable(
554573
interpreter,
574+
libc,
555575
str(path) if display is None else display,
556576
path,
557577
platform,

src/auditwheel/libc.py

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,88 @@
11
from __future__ import annotations
22

33
import logging
4+
import os
5+
import re
6+
import subprocess
7+
from dataclasses import dataclass
48
from enum import IntEnum
9+
from pathlib import Path
510

611
from .error import InvalidLibc
7-
from .musllinux import find_musl_libc
812

913
logger = logging.getLogger(__name__)
1014

1115

16+
@dataclass(frozen=True, order=True)
17+
class LibcVersion:
18+
major: int
19+
minor: int
20+
21+
1222
class Libc(IntEnum):
1323
GLIBC = (1,)
1424
MUSL = (2,)
1525

26+
def get_current_version(self) -> LibcVersion:
27+
if self == Libc.MUSL:
28+
return _get_musl_version(_find_musl_libc())
29+
return _get_glibc_version()
30+
31+
@staticmethod
32+
def detect() -> Libc:
33+
# check musl first, default to GLIBC
34+
try:
35+
_find_musl_libc()
36+
logger.debug("Detected musl libc")
37+
return Libc.MUSL
38+
except InvalidLibc:
39+
logger.debug("Falling back to GNU libc")
40+
return Libc.GLIBC
41+
42+
43+
def _find_musl_libc() -> Path:
44+
try:
45+
(dl_path,) = list(Path("/lib").glob("libc.musl-*.so.1"))
46+
except ValueError:
47+
msg = "musl libc not detected"
48+
logger.debug("%s", msg)
49+
raise InvalidLibc(msg) from None
50+
51+
return dl_path
52+
1653

17-
def get_libc() -> Libc:
54+
def _get_musl_version(ld_path: Path) -> LibcVersion:
1855
try:
19-
find_musl_libc()
20-
logger.debug("Detected musl libc")
21-
return Libc.MUSL
22-
except InvalidLibc:
23-
logger.debug("Falling back to GNU libc")
24-
return Libc.GLIBC
56+
ld = subprocess.run(
57+
[ld_path], check=False, errors="strict", stderr=subprocess.PIPE
58+
).stderr
59+
except FileNotFoundError as err:
60+
msg = "failed to determine musl version"
61+
logger.exception("%s", msg)
62+
raise InvalidLibc(msg) from err
63+
64+
match = re.search(r"Version (?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)", ld)
65+
if not match:
66+
msg = f"failed to parse musl version from string {ld!r}"
67+
raise InvalidLibc(msg) from None
68+
69+
return LibcVersion(int(match.group("major")), int(match.group("minor")))
70+
71+
72+
def _get_glibc_version() -> LibcVersion:
73+
# CS_GNU_LIBC_VERSION is only for glibc and shall return e.g. "glibc 2.3.4"
74+
try:
75+
version_string: str | None = os.confstr("CS_GNU_LIBC_VERSION")
76+
assert version_string is not None
77+
_, version = version_string.rsplit()
78+
except (AssertionError, AttributeError, OSError, ValueError) as err:
79+
# os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
80+
msg = "failed to determine glibc version"
81+
raise InvalidLibc(msg) from err
82+
83+
m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version)
84+
if not m:
85+
msg = f"failed to parse glibc version from string {version!r}"
86+
raise InvalidLibc(msg)
87+
88+
return LibcVersion(int(m.group("major")), int(m.group("minor")))

0 commit comments

Comments
 (0)