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
75 changes: 50 additions & 25 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 @@ -152,6 +154,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 @@ -480,14 +486,17 @@ def ldd(
libc = Libc.MUSL if soname.startswith("ld-musl-") else Libc.GLIBC
if ldpaths is None:
ldpaths = load_ld_paths(libc).copy()
# XXX: Should read it and scan for /lib paths.
ldpaths["interp"] = [
normpath(root + os.path.dirname(interp)),
normpath(
root + prefix + "/usr" + os.path.dirname(interp).lstrip(prefix)
),
]
log.debug(" ldpaths[interp] = %s", ldpaths["interp"])
# XXX: Should read it and scan for /lib paths.
ldpaths["interp"] = [
normpath(root + os.path.dirname(interp)),
normpath(
root
+ prefix
+ "/usr"
+ os.path.dirname(interp).lstrip(prefix)
),
]
log.debug(" ldpaths[interp] = %s", ldpaths["interp"])
break

# Parse the ELF's dynamic tags.
Expand All @@ -513,19 +522,25 @@ 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 "
f"to {libc}"
)
raise InvalidLibc(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 InvalidLibc(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 InvalidLibc(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 @@ -536,6 +551,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 @@ -569,12 +586,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 InvalidLibc(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 @@ -13,10 +13,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"), location, *sys.version_info
Expand Down
57 changes: 43 additions & 14 deletions src/auditwheel/main_repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ def configure_parser(sub_parsers) -> None: # type: ignore[no-untyped-def]
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 @@ -167,18 +173,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 @@ -187,7 +191,13 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:

try:
wheel_abi = analyze_wheel_abi(
libc, arch, wheel_file, exclude, args.DISABLE_ISA_EXT_CHECK, True
libc,
arch,
wheel_file,
exclude,
args.DISABLE_ISA_EXT_CHECK,
True,
parse_ldpaths_arg(parser, args.LDPATHS),
)
except NonPlatformWheel as e:
logger.info(e.message)
Expand Down Expand Up @@ -251,7 +261,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 @@ -267,3 +277,22 @@ 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 @@ -6,6 +6,8 @@
from shutil import which
from subprocess import CalledProcessError, check_call, check_output

from .libc import Libc


class ElfPatcher:
def replace_needed(self, file_name: Path, *old_new_pairs: tuple[str, str]) -> None:
Expand Down Expand Up @@ -46,7 +48,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 Down Expand Up @@ -74,7 +77,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 (
Expand Down
Loading
Loading