Skip to content

Commit 18a5487

Browse files
authored
fix: remove libpython linkage instead of ignoring it. (#590)
`libpython` should be blacklisted per specs. Rather than just adding it to a blacklist just yet, it's now automatically removed from `DT_NEEDED` entries & a warning is added in that case.
1 parent f9bbbce commit 18a5487

File tree

11 files changed

+181
-23
lines changed

11 files changed

+181
-23
lines changed

conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import os
2+
3+
import pytest
4+
5+
6+
@pytest.fixture(autouse=True, scope="session")
7+
def clean_env():
8+
variables = ("AUDITWHEEL_PLAT", "AUDITWHEEL_ZIP_COMPRESSION_LEVEL")
9+
for var in variables:
10+
os.environ.pop(var, None)

src/auditwheel/lddtree.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import glob
1919
import logging
2020
import os
21+
import re
2122
from dataclasses import dataclass
2223
from fnmatch import fnmatch
2324
from pathlib import Path
@@ -31,7 +32,10 @@
3132
from .libc import Libc
3233

3334
log = logging.getLogger(__name__)
34-
__all__ = ["DynamicExecutable", "DynamicLibrary", "ldd"]
35+
__all__ = ["LIBPYTHON_RE", "DynamicExecutable", "DynamicLibrary", "ldd"]
36+
37+
# Regex to match libpython shared library names
38+
LIBPYTHON_RE = re.compile(r"^libpython\d+\.\d+m?.so(\.\d)*$")
3539

3640

3741
@dataclass(frozen=True)
@@ -563,6 +567,16 @@ def ldd(
563567
log.info("Excluding %s", soname)
564568
_excluded_libs.add(soname)
565569
continue
570+
571+
# special case for libpython, see https://github.com/pypa/auditwheel/issues/589
572+
# we want to return the dependency to be able to remove it later on but
573+
# we don't want to analyze it for symbol versions nor do we want to analyze its
574+
# dependencies as it will be removed.
575+
if LIBPYTHON_RE.match(soname):
576+
log.info("Skip %s resolution", soname)
577+
_all_libs[soname] = DynamicLibrary(soname, None, None)
578+
continue
579+
566580
realpath, fullpath = find_lib(platform, soname, all_ldpaths, root)
567581
if realpath is not None and any(fnmatch(str(realpath), e) for e in exclude):
568582
log.info("Excluding %s", realpath)

src/auditwheel/patcher.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ class ElfPatcher:
1111
def replace_needed(self, file_name: Path, *old_new_pairs: tuple[str, str]) -> None:
1212
raise NotImplementedError()
1313

14+
def remove_needed(self, file_name: Path, *sonames: str) -> None:
15+
raise NotImplementedError()
16+
1417
def set_soname(self, file_name: Path, new_so_name: str) -> None:
1518
raise NotImplementedError()
1619

@@ -57,6 +60,15 @@ def replace_needed(self, file_name: Path, *old_new_pairs: tuple[str, str]) -> No
5760
]
5861
)
5962

63+
def remove_needed(self, file_name: Path, *sonames: str) -> None:
64+
check_call(
65+
[
66+
"patchelf",
67+
*chain.from_iterable(("--remove-needed", soname) for soname in sonames),
68+
file_name,
69+
]
70+
)
71+
6072
def set_soname(self, file_name: Path, new_so_name: str) -> None:
6173
check_call(["patchelf", "--set-soname", new_so_name, file_name])
6274

src/auditwheel/policy/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from ..tools import is_subdir
1818

1919
_HERE = Path(__file__).parent
20-
LIBPYTHON_RE = re.compile(r"^libpython\d+\.\d+m?.so(.\d)*$")
2120
_MUSL_POLICY_RE = re.compile(r"^musllinux_\d+_\d+$")
2221

2322
logger = logging.getLogger(__name__)
@@ -201,9 +200,6 @@ def filter_libs(
201200
# 'ld64.so.1' on ppc64le
202201
# 'ld-linux*' on other platforms
203202
continue
204-
if LIBPYTHON_RE.match(lib):
205-
# always exclude libpythonXY
206-
continue
207203
if lib in whitelist:
208204
# exclude any libs in the whitelist
209205
continue

src/auditwheel/repair.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616

1717
from .elfutils import elf_read_dt_needed, elf_read_rpaths
1818
from .hashfile import hashfile
19+
from .lddtree import LIBPYTHON_RE
1920
from .policy import get_replace_platforms
2021
from .tools import is_subdir, unique_by_index
2122
from .wheel_abi import WheelAbIInfo
2223
from .wheeltools import InWheelCtx, add_platforms
2324

2425
logger = logging.getLogger(__name__)
2526

26-
2727
# Copied from wheel 0.31.1
2828
WHEEL_INFO_RE = re.compile(
2929
r"""^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?))(-(?P<build>\d.*?))?
@@ -70,6 +70,16 @@ def repair_wheel(
7070
ext_libs = v[abis[0]].libs
7171
replacements: list[tuple[str, str]] = []
7272
for soname, src_path in ext_libs.items():
73+
# Handle libpython dependencies by removing them
74+
if LIBPYTHON_RE.match(soname):
75+
logger.warning(
76+
"Removing %s dependency from %s. Linking with libpython is forbidden for manylinux/musllinux wheels.",
77+
soname,
78+
str(fn),
79+
)
80+
patcher.remove_needed(fn, soname)
81+
continue
82+
7383
if src_path is None:
7484
msg = (
7585
"Cannot repair wheel, because required "

tests/integration/__init__.py

Whitespace-only changes.
3.85 MB
Binary file not shown.

tests/integration/test_bundled_wheels.py

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from datetime import datetime, timezone
1010
from os.path import isabs
1111
from pathlib import Path
12-
from unittest import mock
1312
from unittest.mock import Mock
1413

1514
import pytest
@@ -26,29 +25,41 @@
2625
@pytest.mark.parametrize(
2726
("file", "external_libs", "exclude"),
2827
[
29-
("cffi-1.5.0-cp27-none-linux_x86_64.whl", {"libffi.so.5"}, frozenset()),
30-
("cffi-1.5.0-cp27-none-linux_x86_64.whl", set(), frozenset(["libffi.so.5"])),
3128
(
3229
"cffi-1.5.0-cp27-none-linux_x86_64.whl",
33-
{"libffi.so.5"},
34-
frozenset(["libffi.so.noexist", "libnoexist.so.*"]),
30+
{"libffi.so.5", "libpython2.7.so.1.0"},
31+
frozenset(),
3532
),
3633
(
3734
"cffi-1.5.0-cp27-none-linux_x86_64.whl",
3835
set(),
36+
frozenset(["libffi.so.5", "libpython2.7.so.1.0"]),
37+
),
38+
(
39+
"cffi-1.5.0-cp27-none-linux_x86_64.whl",
40+
{"libffi.so.5", "libpython2.7.so.1.0"},
41+
frozenset(["libffi.so.noexist", "libnoexist.so.*"]),
42+
),
43+
(
44+
"cffi-1.5.0-cp27-none-linux_x86_64.whl",
45+
{"libpython2.7.so.1.0"},
3946
frozenset(["libffi.so.[4,5]"]),
4047
),
4148
(
4249
"cffi-1.5.0-cp27-none-linux_x86_64.whl",
43-
{"libffi.so.5"},
50+
{"libffi.so.5", "libpython2.7.so.1.0"},
4451
frozenset(["libffi.so.[6,7]"]),
4552
),
4653
(
4754
"cffi-1.5.0-cp27-none-linux_x86_64.whl",
48-
set(),
55+
{"libpython2.7.so.1.0"},
4956
frozenset([f"{HERE}/*"]),
5057
),
51-
("cffi-1.5.0-cp27-none-linux_x86_64.whl", set(), frozenset(["libffi.so.*"])),
58+
(
59+
"cffi-1.5.0-cp27-none-linux_x86_64.whl",
60+
{"libpython2.7.so.1.0"},
61+
frozenset(["libffi.so.*"]),
62+
),
5263
("cffi-1.5.0-cp27-none-linux_x86_64.whl", set(), frozenset(["*"])),
5364
(
5465
"python_snappy-0.5.2-pp260-pypy_41-linux_x86_64.whl",
@@ -170,13 +181,37 @@ def test_wheel_source_date_epoch(timestamp, tmp_path, monkeypatch):
170181
)
171182

172183
monkeypatch.setenv("SOURCE_DATE_EPOCH", str(timestamp[0]))
173-
# patchelf might not be available as we aren't running in a manylinux container
174-
# here. We don't need need it in this test, so just patch it.
175-
with mock.patch("auditwheel.patcher._verify_patchelf"):
176-
main_repair.execute(args, Mock())
177-
184+
main_repair.execute(args, Mock())
178185
output_wheel, *_ = list(wheel_output_path.glob("*.whl"))
179186
with zipfile.ZipFile(output_wheel) as wheel_file:
180187
for file in wheel_file.infolist():
181188
file_date_time = datetime(*file.date_time, tzinfo=timezone.utc)
182189
assert file_date_time.timestamp() == timestamp[1]
190+
191+
192+
def test_libpython(tmp_path, caplog):
193+
wheel = HERE / "python_mscl-67.0.1.0-cp313-cp313-manylinux2014_aarch64.whl"
194+
args = Namespace(
195+
LIB_SDIR=".libs",
196+
ONLY_PLAT=False,
197+
PLAT="auto",
198+
STRIP=False,
199+
UPDATE_TAGS=True,
200+
WHEEL_DIR=tmp_path,
201+
WHEEL_FILE=[wheel],
202+
EXCLUDE=[],
203+
DISABLE_ISA_EXT_CHECK=False,
204+
ZIP_COMPRESSION_LEVEL=6,
205+
cmd="repair",
206+
func=Mock(),
207+
prog="auditwheel",
208+
verbose=0,
209+
)
210+
main_repair.execute(args, Mock())
211+
assert (
212+
"Removing libpython3.13.so.1.0 dependency from python_mscl/_mscl.so"
213+
in caplog.text
214+
)
215+
assert tuple(path.name for path in tmp_path.glob("*.whl")) == (
216+
"python_mscl-67.0.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_31_aarch64.whl",
217+
)

tests/unit/test_elfpatcher.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,20 @@ def test_get_rpath(self, _0, check_output, _1): # noqa: PT019
110110

111111
assert result == check_output.return_value.decode()
112112
assert check_output.call_args_list == check_output_expected_args
113+
114+
def test_remove_needed(self, check_call, _0, _1): # noqa: PT019
115+
patcher = Patchelf()
116+
filename = Path("test.so")
117+
soname_1 = "TEST_REM_1"
118+
soname_2 = "TEST_REM_2"
119+
patcher.remove_needed(filename, soname_1, soname_2)
120+
check_call.assert_called_once_with(
121+
[
122+
"patchelf",
123+
"--remove-needed",
124+
soname_1,
125+
"--remove-needed",
126+
soname_2,
127+
filename,
128+
]
129+
)

tests/unit/test_lddtree.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from auditwheel.architecture import Architecture
6+
from auditwheel.lddtree import LIBPYTHON_RE, ldd
7+
from auditwheel.libc import Libc
8+
from auditwheel.tools import zip2dir
9+
10+
HERE = Path(__file__).parent.resolve(strict=True)
11+
12+
13+
@pytest.mark.parametrize(
14+
"soname",
15+
[
16+
"libpython3.7m.so.1.0",
17+
"libpython3.9.so.1.0",
18+
"libpython3.10.so.1.0",
19+
"libpython999.999.so.1.0",
20+
],
21+
)
22+
def test_libpython_re_match(soname: str) -> None:
23+
assert LIBPYTHON_RE.match(soname)
24+
25+
26+
@pytest.mark.parametrize(
27+
"soname",
28+
[
29+
"libpython3.7m.soa1.0",
30+
"libpython3.9.so.1a0",
31+
],
32+
)
33+
def test_libpython_re_nomatch(soname: str) -> None:
34+
assert LIBPYTHON_RE.match(soname) is None
35+
36+
37+
def test_libpython(tmp_path: Path, caplog: pytest.CaptureFixture) -> None:
38+
wheel = (
39+
HERE
40+
/ ".."
41+
/ "integration"
42+
/ "python_mscl-67.0.1.0-cp313-cp313-manylinux2014_aarch64.whl"
43+
)
44+
so = tmp_path / "python_mscl" / "_mscl.so"
45+
zip2dir(wheel, tmp_path)
46+
result = ldd(so)
47+
assert "Skip libpython3.13.so.1.0 resolution" in caplog.text
48+
assert result.interpreter is None
49+
assert result.libc == Libc.GLIBC
50+
assert result.platform.baseline_architecture == Architecture.aarch64
51+
assert result.platform.extended_architecture is None
52+
assert result.path is not None
53+
assert result.realpath.samefile(so)
54+
assert result.needed == (
55+
"libpython3.13.so.1.0",
56+
"libstdc++.so.6",
57+
"libm.so.6",
58+
"libgcc_s.so.1",
59+
"libc.so.6",
60+
"ld-linux-aarch64.so.1",
61+
)
62+
# libpython must be present in dependencies without path
63+
libpython = result.libraries["libpython3.13.so.1.0"]
64+
assert libpython.soname == "libpython3.13.so.1.0"
65+
assert libpython.path is None
66+
assert libpython.platform is None
67+
assert libpython.realpath is None
68+
assert libpython.needed == ()

0 commit comments

Comments
 (0)