Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/auditwheel/architecture.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Architecture(Enum):
value: str

aarch64 = "aarch64"
arm64_v8a = "arm64_v8a"
armv7l = "armv7l"
i686 = "i686"
loongarch64 = "loongarch64"
Expand Down
53 changes: 36 additions & 17 deletions src/auditwheel/lddtree.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
# Regex to match libpython shared library names
LIBPYTHON_RE = re.compile(r"^libpython\d+\.\d+m?.so(\.\d)*$")

ORIGIN_RE = re.compile(r"\$(ORIGIN|\{ORIGIN\})")


@dataclass(frozen=True)
class Platform:
Expand Down Expand Up @@ -150,6 +152,10 @@ def _get_platform(elf: ELFFile) -> Platform:
error_msg = "armv7l shall use hard-float"
if error_msg is not None:
base_arch = None
elif base_arch == Architecture.aarch64: # noqa: SIM102
# Android uses a different platform tag for this architecture.
if elf.get_section_by_name(".note.android.ident"):
base_arch = Architecture.arm64_v8a

return Platform(
elf_osabi,
Expand Down Expand Up @@ -240,8 +246,8 @@ def parse_ld_paths(str_ldpaths: str, path: str, root: str = "") -> list[str]:
if ldpath == "":
# The ldso treats "" paths as $PWD.
ldpath_ = os.getcwd()
elif "$ORIGIN" in ldpath:
ldpath_ = ldpath.replace("$ORIGIN", os.path.dirname(os.path.abspath(path)))
elif re.search(ORIGIN_RE, ldpath):
ldpath_ = re.sub(ORIGIN_RE, os.path.dirname(os.path.abspath(path)), ldpath)
else:
ldpath_ = root + ldpath
ldpaths.append(normpath(ldpath_))
Expand Down Expand Up @@ -521,19 +527,22 @@ def ldd(

if _first:
# get the libc based on dependencies
def set_libc(new_libc: Libc) -> None:
nonlocal libc
if libc is None:
libc = new_libc
if libc != new_libc:
msg = f"found a dependency on {new_libc} but the libc is already set to {libc}"
raise InvalidLibcError(msg)

for soname in needed:
if soname.startswith(("libc.musl-", "ld-musl-")):
if libc is None:
libc = Libc.MUSL
if libc != Libc.MUSL:
msg = f"found a dependency on MUSL but the libc is already set to {libc}"
raise InvalidLibcError(msg)
set_libc(Libc.MUSL)
elif soname == "libc.so.6" or soname.startswith(("ld-linux-", "ld64.so.")):
if libc is None:
libc = Libc.GLIBC
if libc != Libc.GLIBC:
msg = f"found a dependency on GLIBC but the libc is already set to {libc}"
raise InvalidLibcError(msg)
set_libc(Libc.GLIBC)
elif soname == "libc.so":
set_libc(Libc.ANDROID)

if libc is None:
# try the filename as a last resort
if path.name.endswith(("-arm-linux-musleabihf.so", "-linux-musl.so")):
Expand All @@ -544,6 +553,8 @@ def ldd(
valid_python = tuple(f"3{minor}" for minor in range(11, 100))
if soabi[0] == "cpython" and soabi[1].startswith(valid_python):
libc = Libc.GLIBC
elif path.name.endswith("-linux-android.so"):
libc = Libc.ANDROID

if ldpaths is None:
ldpaths = load_ld_paths(libc).copy()
Expand Down Expand Up @@ -577,12 +588,20 @@ def ldd(
continue

# special case for libpython, see https://github.com/pypa/auditwheel/issues/589
# we want to return the dependency to be able to remove it later on but
# we don't want to analyze it for symbol versions nor do we want to analyze its
# dependencies as it will be removed.
# On Linux we want to return the dependency to be able to remove it later on.
#
# On Android linking with libpython is normal, but we don't want to return it as
# this will make the wheel appear to have external references, requiring it to
# have an API level of at least 24 (see wheel_abi.analyze_wheel_abi).
#
# Either way, we don't want to analyze it for symbol versions, nor do we want to
# analyze its dependencies.
if LIBPYTHON_RE.match(soname):
log.info("Skip %s resolution", soname)
_all_libs[soname] = DynamicLibrary(soname, None, None)
if libc == Libc.ANDROID:
_excluded_libs.add(soname)
else:
log.info("Skip %s resolution", soname)
_all_libs[soname] = DynamicLibrary(soname, None, None)
continue

realpath, fullpath = find_lib(platform, soname, all_ldpaths, root)
Expand Down
14 changes: 13 additions & 1 deletion src/auditwheel/libc.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ class Libc(Enum):

GLIBC = "glibc"
MUSL = "musl"
ANDROID = "android"

def __str__(self) -> str:
return self.value

def get_current_version(self) -> LibcVersion:
if self == Libc.MUSL:
return _get_musl_version(_find_musl_libc())
return _get_glibc_version()
if self == Libc.GLIBC:
return _get_glibc_version()
msg = f"can't determine version of libc '{self}'"
raise InvalidLibcError(msg)

@staticmethod
def detect() -> Libc:
Expand All @@ -44,6 +48,14 @@ def detect() -> Libc:
logger.debug("Falling back to GNU libc")
return Libc.GLIBC

@property
def tag_prefix(self) -> str:
return {
Libc.GLIBC: "manylinux",
Libc.MUSL: "musllinux",
Libc.ANDROID: "android",
}[self]


def _find_musl_libc() -> Path:
try:
Expand Down
4 changes: 0 additions & 4 deletions src/auditwheel/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@


def main() -> int | None:
if sys.platform != "linux":
print("Error: This tool only supports Linux")
return 1

location = pathlib.Path(auditwheel.__file__).parent.resolve()
version = "auditwheel {} installed at {} (python {}.{})".format(
metadata.version("auditwheel"),
Expand Down
57 changes: 38 additions & 19 deletions src/auditwheel/main_repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ def configure_parser(sub_parsers: Any) -> None: # noqa: ANN401
action="append",
default=[],
)
parser.add_argument(
"--ldpaths",
dest="LDPATHS",
help="Colon-delimited list of directories to search for external libraries. "
"This replaces the default list; to add to the default, use LD_LIBRARY_PATH.",
)
parser.add_argument(
"--only-plat",
dest="ONLY_PLAT",
Expand Down Expand Up @@ -180,24 +186,16 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
logger.debug("The libc could not be deduced from the wheel filename")
libc = None

if plat_base.startswith("manylinux"):
if libc is None:
libc = Libc.GLIBC
if libc != Libc.GLIBC:
msg = (
f"can't repair wheel {wheel_filename} with {libc.name} libc to a wheel "
"targeting GLIBC"
)
parser.error(msg)
elif plat_base.startswith("musllinux"):
if libc is None:
libc = Libc.MUSL
if libc != Libc.MUSL:
msg = (
f"can't repair wheel {wheel_filename} with {libc.name} libc to a wheel "
"targeting MUSL"
)
parser.error(msg)
for lc in Libc:
if plat_base.startswith(lc.tag_prefix):
if libc is None:
libc = lc
if libc != lc:
msg = (
f"can't repair wheel {wheel_filename} with {libc.name} libc "
f"to a wheel targeting {lc.name}"
)
parser.error(msg)

logger.info("Repairing %s", wheel_filename)

Expand All @@ -213,6 +211,7 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
disable_isa_ext_check=args.DISABLE_ISA_EXT_CHECK,
allow_graft=True,
requested_policy_base_name=plat_base,
ldpaths=parse_ldpaths_arg(parser, args.LDPATHS),
)
except NonPlatformWheelError as e:
logger.info(e.message)
Expand Down Expand Up @@ -282,7 +281,7 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
*abis,
]

patcher = Patchelf()
patcher = Patchelf(libc)
out_wheel = repair_wheel(
wheel_abi,
wheel_file,
Expand All @@ -298,3 +297,23 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
if out_wheel is not None:
logger.info("\nFixed-up wheel written to %s", out_wheel)
return 0


# None of the special behavior of lddtree.parse_ld_paths is applicable to the --ldpaths
# option.
def parse_ldpaths_arg(
parser: argparse.ArgumentParser,
ldpaths: str | None,
) -> tuple[str, ...] | None:
if ldpaths is None:
return None

result: list[str] = []
for ldp_str in ldpaths.split(":"):
ldp_path = Path(ldp_str)
if (not ldp_str) or (not ldp_path.exists()):
msg = f"--ldpaths item {ldp_str!r} does not exist"
parser.error(msg)
result.append(str(ldp_path.absolute()))

return tuple(result)
10 changes: 8 additions & 2 deletions src/auditwheel/patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
if TYPE_CHECKING:
from pathlib import Path

from .libc import Libc


class ElfPatcher:
def replace_needed(self, file_name: Path, *old_new_pairs: tuple[str, str]) -> None:
Expand Down Expand Up @@ -49,7 +51,8 @@ def _verify_patchelf() -> None:


class Patchelf(ElfPatcher):
def __init__(self) -> None:
def __init__(self, libc: Libc | None = None) -> None:
self.libc = libc
_verify_patchelf()

def replace_needed(self, file_name: Path, *old_new_pairs: tuple[str, str]) -> None:
Expand All @@ -75,7 +78,10 @@ def set_soname(self, file_name: Path, new_so_name: str) -> None:

def set_rpath(self, file_name: Path, rpath: str) -> None:
check_call(["patchelf", "--remove-rpath", file_name])
check_call(["patchelf", "--force-rpath", "--set-rpath", rpath, file_name])

# Android supports only RUNPATH, not RPATH.
extra_args = [] if self.libc == Libc.ANDROID else ["--force-rpath"]
check_call(["patchelf", *extra_args, "--set-rpath", rpath, file_name])

def get_rpath(self, file_name: Path) -> str:
return check_output(["patchelf", "--print-rpath", file_name]).decode("utf-8").strip()
52 changes: 50 additions & 2 deletions src/auditwheel/policy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import logging
import re
from collections import defaultdict
from dataclasses import dataclass
from dataclasses import dataclass, replace
from pathlib import Path
from typing import TYPE_CHECKING, Any

from packaging.utils import parse_wheel_filename

from auditwheel.architecture import Architecture
from auditwheel.elfutils import filter_undefined_symbols
from auditwheel.error import InvalidLibcError
Expand All @@ -27,6 +29,9 @@
_POLICY_JSON_MAP = {
Libc.GLIBC: _HERE / "manylinux-policy.json",
Libc.MUSL: _HERE / "musllinux-policy.json",
# Android whitelists are based on
# https://developer.android.com/ndk/guides/stable_apis.
Libc.ANDROID: _HERE / "android-policy.json",
}


Expand Down Expand Up @@ -58,6 +63,7 @@ def __init__(
*,
libc: Libc,
arch: Architecture,
wheel_fn: Path | None = None,
musl_policy: str | None = None,
) -> None:
if libc != Libc.MUSL and musl_policy is not None:
Expand Down Expand Up @@ -118,9 +124,42 @@ def __init__(
self._policies = [self._policies[0], self._policies[1]]
assert len(self._policies) == 2, self._policies # noqa: S101

elif self._libc_variant == Libc.ANDROID:
# Pick the policy with the highest API level that's less than or equal to
# the wheel's existing tag.
assert wheel_fn is not None # noqa: S101
plats = list({t.platform for t in parse_wheel_filename(wheel_fn.name)[3]})
if len(plats) != 1:
msg = "Android wheels must have exactly one platform tag"
raise ValueError(msg)
api_level = tag_api_level(plats[0])

valid_policies = [
p
for p in self._policies
if p.name.startswith("android") and tag_api_level(p.name) <= api_level
]
if not valid_policies:
msg = f"minimum supported platform tag is {self.lowest.name}"
raise ValueError(msg)
best_policy = max(valid_policies, key=lambda p: tag_api_level(p.name))

# It's unsafe to reduce the API level of the existing tag, so rename the
# policy to match it.
self._policies = [self.linux, replace(best_policy, name=plats[0])]

def __iter__(self) -> Generator[Policy]:
yield from self._policies

def __len__(self) -> int:
return len(self._policies)

def __getitem__(self, index: int) -> Policy:
return self._policies[index]

def __setitem__(self, index: int, p: Policy) -> None:
self._policies[index] = p

@property
def libc(self) -> Libc:
return self._libc_variant
Expand Down Expand Up @@ -321,7 +360,13 @@ def _fixup_musl_libc_soname(
return frozenset(new_whitelist)


def get_replace_platforms(name: str) -> list[str]:
def tag_api_level(tag: str) -> int:
match = re.match(r"android_(\d+)", tag)
assert match is not None # noqa: S101
return int(match[1])


def get_replace_platforms(name: str) -> list[str | re.Pattern[str]]:
"""Extract platform tag replacement rules from policy

>>> get_replace_platforms('linux_x86_64')
Expand All @@ -340,6 +385,9 @@ def get_replace_platforms(name: str) -> list[str]:
return ["linux_" + "_".join(name.split("_")[3:])]
if name.startswith("musllinux_"):
return ["linux_" + "_".join(name.split("_")[3:])]
if name.startswith("android_"):
# On Android it only makes sense to have one platform tag at a time.
return [re.compile(r"android_.+")]
return ["linux_" + "_".join(name.split("_")[1:])]


Expand Down
Loading