Skip to content

Commit 87bcd8f

Browse files
committed
MAINT: refactor rpath handling code and add tests
This does not introduce any functional changes.
1 parent d7072d7 commit 87bcd8f

File tree

3 files changed

+127
-44
lines changed

3 files changed

+127
-44
lines changed

mesonpy/_rpath.py

Lines changed: 81 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,59 @@
1111

1212

1313
if typing.TYPE_CHECKING:
14-
from typing import List
14+
from typing import List, TypeVar
1515

16-
from mesonpy._compat import Iterable, Path
16+
from mesonpy._compat import Path
1717

18+
T = TypeVar('T')
1819

19-
if sys.platform == 'win32' or sys.platform == 'cygwin':
2020

21-
def fix_rpath(filepath: Path, libs_relative_path: str) -> None:
21+
def unique(values: List[T]) -> List[T]:
22+
r = []
23+
for value in values:
24+
if value not in r:
25+
r.append(value)
26+
return r
27+
28+
29+
class RPATH:
30+
31+
origin = '$ORIGIN'
32+
33+
@staticmethod
34+
def get_rpath(filepath: Path) -> List[str]:
35+
raise NotImplementedError
36+
37+
@staticmethod
38+
def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None:
39+
raise NotImplementedError
40+
41+
@classmethod
42+
def fix_rpath(cls, filepath: Path, libs_relative_path: str) -> None:
43+
old_rpath = cls.get_rpath(filepath)
44+
new_rpath = []
45+
for path in old_rpath:
46+
if path.startswith(cls.origin):
47+
path = os.path.join(cls.origin, libs_relative_path)
48+
new_rpath.append(path)
49+
new_rpath = unique(new_rpath)
50+
if new_rpath != old_rpath:
51+
cls.set_rpath(filepath, old_rpath, new_rpath)
52+
53+
54+
class _Windows(RPATH):
55+
56+
@classmethod
57+
def fix_rpath(cls, filepath: Path, libs_relative_path: str) -> None:
2258
pass
2359

24-
elif sys.platform == 'darwin':
2560

26-
def _get_rpath(filepath: Path) -> List[str]:
61+
class _MacOS(RPATH):
62+
63+
origin = '@loader_path'
64+
65+
@staticmethod
66+
def get_rpath(filepath: Path) -> List[str]:
2767
rpath = []
2868
r = subprocess.run(['otool', '-l', os.fspath(filepath)], capture_output=True, text=True)
2969
rpath_tag = False
@@ -35,17 +75,24 @@ def _get_rpath(filepath: Path) -> List[str]:
3575
rpath_tag = False
3676
return rpath
3777

38-
def _replace_rpath(filepath: Path, old: str, new: str) -> None:
39-
subprocess.run(['install_name_tool', '-rpath', old, new, os.fspath(filepath)], check=True)
78+
@staticmethod
79+
def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None:
80+
# This implementation does not preserve the ordering of RPATH
81+
# entries. Meson does the same, thus it should not be a problem.
82+
args: List[str] = []
83+
for path in rpath:
84+
if path not in old:
85+
args += ['-add_rpath', path]
86+
for path in old:
87+
if path not in rpath:
88+
args += ['-delete_rpath', path]
89+
subprocess.run(['install_name_tool', *args, os.fspath(filepath)], check=True)
4090

41-
def fix_rpath(filepath: Path, libs_relative_path: str) -> None:
42-
for path in _get_rpath(filepath):
43-
if path.startswith('@loader_path/'):
44-
_replace_rpath(filepath, path, '@loader_path/' + libs_relative_path)
4591

46-
elif sys.platform == 'sunos5':
92+
class _SunOS5(RPATH):
4793

48-
def _get_rpath(filepath: Path) -> List[str]:
94+
@staticmethod
95+
def get_rpath(filepath: Path) -> List[str]:
4996
rpath = []
5097
r = subprocess.run(['/usr/bin/elfedit', '-r', '-e', 'dyn:rpath', os.fspath(filepath)],
5198
capture_output=True, check=True, text=True)
@@ -56,35 +103,32 @@ def _get_rpath(filepath: Path) -> List[str]:
56103
rpath.append(path)
57104
return rpath
58105

59-
def _set_rpath(filepath: Path, rpath: Iterable[str]) -> None:
106+
@staticmethod
107+
def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None:
60108
subprocess.run(['/usr/bin/elfedit', '-e', 'dyn:rpath ' + ':'.join(rpath), os.fspath(filepath)], check=True)
61109

62-
def fix_rpath(filepath: Path, libs_relative_path: str) -> None:
63-
old_rpath = _get_rpath(filepath)
64-
new_rpath = []
65-
for path in old_rpath:
66-
if path.startswith('$ORIGIN/'):
67-
path = '$ORIGIN/' + libs_relative_path
68-
new_rpath.append(path)
69-
if new_rpath != old_rpath:
70-
_set_rpath(filepath, new_rpath)
71110

72-
else:
73-
# Assume that any other platform uses ELF binaries.
111+
class _ELF(RPATH):
74112

75-
def _get_rpath(filepath: Path) -> List[str]:
113+
@staticmethod
114+
def get_rpath(filepath: Path) -> List[str]:
76115
r = subprocess.run(['patchelf', '--print-rpath', os.fspath(filepath)], capture_output=True, text=True)
77116
return [x for x in r.stdout.strip().split(':') if x]
78117

79-
def _set_rpath(filepath: Path, rpath: Iterable[str]) -> None:
118+
@staticmethod
119+
def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None:
80120
subprocess.run(['patchelf','--set-rpath', ':'.join(rpath), os.fspath(filepath)], check=True)
81121

82-
def fix_rpath(filepath: Path, libs_relative_path: str) -> None:
83-
old_rpath = _get_rpath(filepath)
84-
new_rpath = []
85-
for path in old_rpath:
86-
if path.startswith('$ORIGIN/'):
87-
path = '$ORIGIN/' + libs_relative_path
88-
new_rpath.append(path)
89-
if new_rpath != old_rpath:
90-
_set_rpath(filepath, new_rpath)
122+
123+
if sys.platform == 'win32' or sys.platform == 'cygwin':
124+
_cls = _Windows
125+
elif sys.platform == 'darwin':
126+
_cls = _MacOS
127+
elif sys.platform == 'sunos5':
128+
_cls = _SunOS5
129+
else:
130+
_cls = _ELF
131+
132+
get_rpath = _cls.get_rpath
133+
set_rpath = _cls.set_rpath
134+
fix_rpath = _cls.fix_rpath

tests/test_rpath.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# SPDX-FileCopyrightText: 2025 The meson-python developers
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
import sys
6+
7+
import pytest
8+
import wheel.wheelfile
9+
10+
from mesonpy._rpath import get_rpath, set_rpath
11+
12+
13+
@pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support')
14+
def test_rpath_get_set(wheel_sharedlib_in_package, tmp_path):
15+
artifact = wheel.wheelfile.WheelFile(wheel_sharedlib_in_package)
16+
artifact.extractall(tmp_path)
17+
obj = list(tmp_path.joinpath('mypkg').glob('_example.*'))[0]
18+
19+
rpath = get_rpath(obj)
20+
assert rpath
21+
22+
set_rpath(obj, rpath, [])
23+
rpath = get_rpath(obj)
24+
assert rpath == []
25+
26+
new_rpath = ['one', 'two']
27+
set_rpath(obj, rpath, new_rpath)
28+
rpath = get_rpath(obj)
29+
assert set(rpath) == set(new_rpath)
30+
31+
new_rpath = ['one', 'three', 'two']
32+
set_rpath(obj, rpath, new_rpath)
33+
rpath = get_rpath(obj)
34+
assert set(rpath) == set(new_rpath)
35+
36+
new_rpath = ['one']
37+
set_rpath(obj, rpath, new_rpath)
38+
rpath = get_rpath(obj)
39+
assert set(rpath) == set(new_rpath)

tests/test_wheel.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,17 +185,17 @@ def test_sharedlib_in_package_rpath(wheel_sharedlib_in_package, tmp_path):
185185

186186
origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN'
187187

188-
rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / f'_example{EXT_SUFFIX}'))
188+
rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / f'_example{EXT_SUFFIX}'))
189189
# This RPATH entry should be removed by meson-python but it is not.
190190
build_rpath = {f'{origin}/../src'}
191191
assert rpath == {origin, *build_rpath}
192192

193-
rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / f'liblib{LIB_SUFFIX}'))
193+
rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / f'liblib{LIB_SUFFIX}'))
194194
# This RPATH entry should be removed by meson-python but it is not.
195195
build_rpath = {f'{origin}/'}
196196
assert rpath == {f'{origin}/sub', *build_rpath}
197197

198-
rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / 'sub' / f'libsublib{LIB_SUFFIX}'))
198+
rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / 'sub' / f'libsublib{LIB_SUFFIX}'))
199199
assert rpath == set()
200200

201201

@@ -211,7 +211,7 @@ def test_sharedlib_in_package_rpath_ldflags(package_sharedlib_in_package, tmp_pa
211211
artifact.extractall(tmp_path)
212212

213213
for path in f'_example{EXT_SUFFIX}', f'liblib{LIB_SUFFIX}', f'sub/libsublib{LIB_SUFFIX}':
214-
rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / path))
214+
rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / path))
215215
assert extra_rpath <= rpath
216216

217217

@@ -236,7 +236,7 @@ def test_link_against_local_lib_rpath(wheel_link_against_local_lib, tmp_path):
236236
origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN'
237237
expected = {f'{origin}/../.link_against_local_lib.mesonpy.libs', 'custom-rpath',}
238238

239-
rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}'))
239+
rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}'))
240240
assert rpath == expected
241241

242242

@@ -255,7 +255,7 @@ def test_link_against_local_lib_rpath_ldflags(package_link_against_local_lib, tm
255255
# erroneusly stripped by meson-python.
256256
extra_rpath = {'/usr/lib/test-ldflags',}
257257

258-
rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}'))
258+
rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}'))
259259
assert extra_rpath <= rpath
260260

261261

@@ -265,7 +265,7 @@ def test_uneeded_rpath(wheel_purelib_and_platlib, tmp_path):
265265
artifact.extractall(tmp_path)
266266

267267
origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN'
268-
rpath = mesonpy._rpath._get_rpath(tmp_path / f'plat{EXT_SUFFIX}')
268+
rpath = mesonpy._rpath.get_rpath(tmp_path / f'plat{EXT_SUFFIX}')
269269
for path in rpath:
270270
assert origin not in path
271271

0 commit comments

Comments
 (0)