Skip to content
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a95e8d0
Begin Android support
mhsmith Dec 13, 2025
a35924f
Improve API level auto-detection
mhsmith Dec 14, 2025
d2a0db9
Fix some tests
mhsmith Dec 15, 2025
237874e
Working with manual repair of numpy-1.26.2-0-cp313-cp313-android_24_a…
mhsmith Dec 15, 2025
588758c
Use LD_LIBRARY_PATHS variable even when --ldpaths is passed
mhsmith Dec 16, 2025
38a59c5
Use RUNPATH rather than RPATH
mhsmith Dec 16, 2025
e675469
Merge remote-tracking branch 'origin/main' into android
mhsmith Feb 4, 2026
2033e47
Rename and test android_api_level
mhsmith Mar 4, 2026
3a5e1b2
Revert unnecessary reformatting
mhsmith Mar 4, 2026
d4092fc
Add tests
mhsmith Mar 4, 2026
5e54a3e
Remove automatic increase to API level 24
mhsmith Mar 9, 2026
c2a76d4
Add tests for policies
mhsmith Mar 13, 2026
e088b03
Add --ldpaths to all subcommands, and make it required on Android
mhsmith Mar 13, 2026
79627e4
Move API level 24 check to patcher
mhsmith Mar 13, 2026
5fb678f
Rename main_options.py to options.py
mhsmith Mar 15, 2026
364ab91
Improve error messages
mhsmith Mar 15, 2026
4568843
Respond to Copilot and Codecov reports; update documentation
mhsmith Mar 15, 2026
c3911bc
Merge remote-tracking branch 'origin/main' into android
mhsmith Mar 15, 2026
5cf82d6
Add integration test
mhsmith Mar 15, 2026
bd58c59
Merge branch 'main' into android
mayeut Mar 16, 2026
91ccc3a
Always pass --ldpaths in integration test
mhsmith Mar 16, 2026
725c7ef
Add coverage of elf_read_soname
mhsmith Mar 16, 2026
9841ab1
Correct handling of libpython
mhsmith Mar 22, 2026
e0bf5ba
Make --ldpaths default to empty on Android
mhsmith Mar 22, 2026
16d4c24
Merge branch 'main' into android
auvipy Apr 14, 2026
8105698
Move elf_read_soname to test file
mhsmith Apr 15, 2026
1f82ffa
Move API level 24 check to base ElfPatcher class
mhsmith Apr 15, 2026
21c67b8
Restore previous RPATH behavior on Linux
mhsmith Apr 15, 2026
f370314
Add macOS CI job
mhsmith Apr 16, 2026
3e55a9a
Update help string of `--plat`
mhsmith Apr 24, 2026
7f7fb5c
Switch to AUDITWHEEL_LD_LIBRARY_PATH variable
mhsmith Apr 24, 2026
108c7f7
Update tests
mhsmith Apr 24, 2026
092bb4d
Update options.py
mayeut Apr 25, 2026
c12866f
Merge branch 'main' into pr/643
mayeut Apr 28, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ jobs:
- platform: [ 'riscv64', 'ubuntu-24.04' ]
python: '3.12'
qemu: true
- platform: [ 'aarch64', 'macos-26' ]
python: '3.12'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand All @@ -102,7 +104,7 @@ jobs:
AUDITWHEEL_QEMU: ${{ matrix.qemu }}
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: coverage-${{ matrix.python }}-${{ matrix.platform[0] }}
name: coverage-${{ matrix.python }}-${{ matrix.platform[0] }}-${{ matrix.platform[1] }}
path: coverage-*.xml
retention-days: 1

Expand Down
17 changes: 7 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,20 @@ auditwheel
.. image:: https://pepy.tech/badge/auditwheel/month
:target: https://pepy.tech/project/auditwheel/month

Auditing and relabeling of `PEP 600 manylinux_x_y
<https://www.python.org/dev/peps/pep-0600/>`_, `PEP 513 manylinux1
<https://www.python.org/dev/peps/pep-0513/>`_, `PEP 571 manylinux2010
<https://www.python.org/dev/peps/pep-0571/>`_ and `PEP 599 manylinux2014
<https://www.python.org/dev/peps/pep-0599/>`_ Linux wheels.
Auditing and relabeling of Linux and Android wheels.

Overview
--------

``auditwheel`` is a command line tool to facilitate the creation of Python
`wheel packages <http://pythonwheels.com/>`_ for Linux (containing pre-compiled
binary extensions) that are compatible with a wide variety of Linux distributions,
`wheel packages <http://pythonwheels.com/>`_ (containing pre-compiled
binary extensions) that are compatible with a wide variety of distributions,
consistent with the `PEP 600 manylinux_x_y
<https://www.python.org/dev/peps/pep-0600/>`_, `PEP 513 manylinux1
<https://www.python.org/dev/peps/pep-0513/>`_, `PEP 571 manylinux2010
<https://www.python.org/dev/peps/pep-0571/>`_ and `PEP 599 manylinux2014
<https://www.python.org/dev/peps/pep-0599/>`_ platform tags.
<https://www.python.org/dev/peps/pep-0571/>`_, `PEP 599 manylinux2014
<https://www.python.org/dev/peps/pep-0599/>`_ and `PEP 738 android
<https://www.python.org/dev/peps/pep-0738/>`_ platform tags.

``auditwheel show``: shows external shared libraries that the wheel depends on
(beyond the libraries included in the ``manylinux`` policies), and
Expand All @@ -39,7 +36,7 @@ advised that bundling, like static linking, may implicate copyright concerns.

Requirements
------------
- OS: Linux
- OS: Linux (for Android wheels, macOS may also be used)
- Python: 3.10+
- `patchelf <https://github.com/NixOS/patchelf>`_: 0.14+

Expand Down
9 changes: 5 additions & 4 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,11 @@ def tests(session: nox.Session) -> None:

if RUNNING_CI:
posargs.extend(["--cov", "auditwheel", "--cov-config", "pyproject.toml"])
# pull manylinux images that will be used.
# this helps passing tests which would otherwise timeout.
for image in _docker_images(session):
session.run("docker", "pull", image, external=True)
if sys.platform != "darwin":
# pull manylinux images that will be used.
# this helps passing tests which would otherwise timeout.
for image in _docker_images(session):
session.run("docker", "pull", image, external=True)

session.run("pytest", "-s", *posargs)
if RUNNING_CI:
Expand Down
3 changes: 3 additions & 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" # Android
armv7l = "armv7l"
i686 = "i686"
loongarch64 = "loongarch64"
Expand All @@ -28,6 +29,8 @@ def __str__(self) -> str:

@property
def baseline(self) -> Architecture:
if self.value.startswith("arm64"):
return Architecture.aarch64
if self.value.startswith("x86_64"):
return Architecture.x86_64
return self
Expand Down
30 changes: 26 additions & 4 deletions src/auditwheel/lddtree.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
# Regex to match libpython shared library names
LIBPYTHON_RE = re.compile(r"^libpython\d+\.\d+m?.so(\.\d)*$")

# Regex to match ORIGIN references in rpaths.
ORIGIN_RE = re.compile(r"\$(ORIGIN|\{ORIGIN\})")


@dataclass(frozen=True)
class Platform:
Expand Down Expand Up @@ -213,12 +216,12 @@ def dedupe(items: list[str]) -> list[str]:
return [seen.setdefault(x, x) for x in items if x not in seen]


def parse_ld_paths(str_ldpaths: str, path: str, root: str = "") -> list[str]:
def parse_ld_paths(str_ldpaths: str, path: str = "", root: str = "") -> list[str]:
"""Parse the colon-delimited list of paths and apply ldso rules to each

Note the special handling as dictated by the ldso:
- Empty paths are equivalent to $PWD
- $ORIGIN is expanded to the path of the given file
- $ORIGIN is expanded to the directory containing the given file
- (TODO) $LIB and friends

Parameters
Expand All @@ -235,13 +238,19 @@ def parse_ld_paths(str_ldpaths: str, path: str, root: str = "") -> list[str]:
list of processed paths

"""
if not str_ldpaths:
return []

ldpaths: list[str] = []
for ldpath in str_ldpaths.split(":"):
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):
if not path:
msg = "can't expand $ORIGIN without a path"
raise ValueError(msg)
ldpath_ = re.sub(ORIGIN_RE, os.path.dirname(os.path.abspath(path)), ldpath)
Comment thread
mhsmith marked this conversation as resolved.
else:
ldpath_ = root + ldpath
ldpaths.append(normpath(ldpath_))
Expand Down Expand Up @@ -363,6 +372,19 @@ def load_ld_paths(
return ldpaths


def ld_paths_from_arg(args_ldpaths: str | None) -> dict[str, list[str]] | None:
"""Load linker paths from the --ldpaths option and the environment."""
if args_ldpaths is None:
# The option was not provided, so fall back on load_ld_paths.
return None

return {
"conf": parse_ld_paths(args_ldpaths),
"env": parse_ld_paths(os.environ.get("AUDITWHEEL_LD_LIBRARY_PATH", "")),
"interp": [],
}


def find_lib(
platform: Platform,
lib: str,
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
7 changes: 5 additions & 2 deletions src/auditwheel/main_lddtree.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any

from auditwheel import options

if TYPE_CHECKING:
import argparse

Expand All @@ -14,12 +16,13 @@ def configure_subparser(sub_parsers: Any) -> None: # noqa: ANN401
help_ = "Analyze a single ELF file (similar to ``ldd``)."
p = sub_parsers.add_parser("lddtree", help=help_, description=help_)
p.add_argument("file", type=Path, help="Path to .so file")
options.ldpaths(p)
p.set_defaults(func=execute)


def execute(args: argparse.Namespace, p: argparse.ArgumentParser) -> int: # noqa: ARG001
from auditwheel import json
from auditwheel.lddtree import ldd
from auditwheel.lddtree import ld_paths_from_arg, ldd

logger.info(json.dumps(ldd(args.file)))
logger.info(json.dumps(ldd(args.file, ldpaths=ld_paths_from_arg(args.LDPATHS))))
return 0
52 changes: 18 additions & 34 deletions src/auditwheel/main_repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pathlib import Path
from typing import Any

from auditwheel import options
from auditwheel.architecture import Architecture
from auditwheel.error import NonPlatformWheelError, WheelToolsError
from auditwheel.libc import Libc
Expand All @@ -24,7 +25,7 @@ def configure_parser(sub_parsers: Any) -> None: # noqa: ANN401
policy_names += [alias for p in policies for alias in p.aliases]
policy_names += ["auto"]
epilog = """PLATFORMS:
These are the possible target platform tags, as specified by PEP 600.
These are the possible platform tags for this machine, as specified by PEP 600.
Note that old, pre-PEP 600 tags are still usable and are listed as aliases
below.
"""
Expand Down Expand Up @@ -118,20 +119,10 @@ def configure_parser(sub_parsers: Any) -> None: # noqa: ANN401
help="Do not check for higher policy compatibility",
default=False,
)
parser.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,
)
parser.add_argument(
"--allow-pure-python-wheel",
dest="ALLOW_PURE_PY_WHEEL",
action="store_true",
help="Allow processing of pure Python wheels (no platform-specific binaries) without error",
default=False,
)
options.disable_isa_check(parser)
options.allow_pure_python_wheel(parser)
options.ldpaths(parser)

parser.set_defaults(func=execute)


Expand Down Expand Up @@ -180,24 +171,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 to a wheel "
f"targeting {lc.name}"
)
parser.error(msg)
Comment thread
mayeut marked this conversation as resolved.

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

Expand All @@ -213,6 +196,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,
args_ldpaths=args.LDPATHS,
)
Comment thread
mayeut marked this conversation as resolved.
except NonPlatformWheelError as e:
logger.info(e.message)
Expand Down Expand Up @@ -282,7 +266,7 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
*abis,
]

patcher = Patchelf()
patcher = Patchelf(requested_policy.name)
out_wheel = repair_wheel(
wheel_abi,
wheel_file,
Expand Down
20 changes: 6 additions & 14 deletions src/auditwheel/main_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any

from auditwheel import options

if TYPE_CHECKING:
import argparse

Expand All @@ -14,20 +16,9 @@ def configure_parser(sub_parsers: Any) -> None: # noqa: ANN401
help_ = "Audit a wheel for external shared library dependencies."
p = sub_parsers.add_parser("show", help=help_, description=help_)
p.add_argument("WHEEL_FILE", type=Path, 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.add_argument(
"--allow-pure-python-wheel",
dest="ALLOW_PURE_PY_WHEEL",
action="store_true",
help="Allow processing of pure Python wheels (no platform-specific binaries) without error",
default=False,
)
options.disable_isa_check(p)
options.allow_pure_python_wheel(p)
options.ldpaths(p)
p.set_defaults(func=execute)


Expand Down Expand Up @@ -73,6 +64,7 @@ def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
frozenset(),
disable_isa_ext_check=args.DISABLE_ISA_EXT_CHECK,
allow_graft=False,
args_ldpaths=args.LDPATHS,
)
except NonPlatformWheelError as e:
logger.info("%s", e.message)
Expand Down
Loading
Loading