Skip to content

Commit e97d1fb

Browse files
committed
ENH: improve RPATH handling
Always strip RPATH pointing to the build directory automatically added by meson at build time to all artifacts linking to a shared library built as part of the project. Before this was done only when the project contained a shared library relocated to .<project-name>.mesonpy.libs. Add the RPATH entry specified in the meson.build definition via the install_rpath argument to all artifacts. This automatically remaps the $ORIGIN anchor to @loader_path on macOS. Deduplicate RPATH entries. Fixes #711.
1 parent 196bb57 commit e97d1fb

File tree

4 files changed

+138
-56
lines changed

4 files changed

+138
-56
lines changed

docs/reference/meson-compatibility.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ versions.
5353
declared via the ``project()`` call in ``meson.build``. This also
5454
requires ``pyproject-metadata`` version 0.9.0 or later.
5555

56+
Meson 1.6.0 or later is also required for support for the
57+
``install_rpath`` argument to Meson functions declaring build rules
58+
for object files.
59+
5660
Build front-ends by default build packages in an isolated Python
5761
environment where build dependencies are installed. Most often, unless
5862
a package or its build dependencies declare explicitly a version

mesonpy/__init__.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class InvalidLicenseExpression(Exception): # type: ignore[no-redef]
113113
class Entry(typing.NamedTuple):
114114
dst: pathlib.Path
115115
src: str
116+
rpath: Optional[str] = None
116117

117118

118119
def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[Entry]]:
@@ -161,7 +162,7 @@ def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[E
161162
filedst = dst / relpath
162163
wheel_files[path].append(Entry(filedst, filesrc))
163164
else:
164-
wheel_files[path].append(Entry(dst, src))
165+
wheel_files[path].append(Entry(dst, src, target.get('install_rpath')))
165166

166167
return wheel_files
167168

@@ -420,25 +421,25 @@ def _stable_abi(self) -> Optional[str]:
420421
return 'abi3'
421422
return None
422423

423-
def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path, destination: pathlib.Path) -> None:
424+
def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile,
425+
origin: Path, destination: pathlib.Path, rpath: Optional[str]) -> None:
424426
"""Add a file to the wheel."""
425427

426-
if self._has_internal_libs:
427-
if _is_native(origin):
428-
if sys.platform == 'win32' and not self._allow_windows_shared_libs:
429-
raise NotImplementedError(
430-
'Loading shared libraries bundled in the Python wheel on Windows requires '
431-
'setting the DLL load path or preloading. See the documentation for '
432-
'the "tool.meson-python.allow-windows-internal-shared-libs" option.')
433-
434-
# When an executable, libray, or Python extension module is
428+
if _is_native(origin):
429+
libspath = None
430+
if self._has_internal_libs:
431+
# When an executable, library, or Python extension module is
435432
# dynamically linked to a library built as part of the project,
436433
# Meson adds a library load path to it pointing to the build
437434
# directory, in the form of a relative RPATH entry. meson-python
438-
# relocates the shared libraries to the $project.mesonpy.libs
435+
# relocates the shared libraries to the ``.<project-name>.mesonpy.libs``
439436
# folder. Rewrite the RPATH to point to that folder instead.
440437
libspath = os.path.relpath(self._libs_dir, destination.parent)
441-
mesonpy._rpath.fix_rpath(origin, libspath)
438+
439+
# Adjust RPATH: remove build RPATH added by meson, add an RPATH
440+
# entries as per above, and add any ``install_rpath`` specified in
441+
# meson.build
442+
mesonpy._rpath.fix_rpath(origin, rpath, libspath)
442443

443444
try:
444445
wheel_file.write(origin, destination.as_posix())
@@ -467,6 +468,13 @@ def _wheel_write_metadata(self, whl: mesonpy._wheelfile.WheelFile) -> None:
467468
whl.write(f, f'{self._distinfo_dir}/licenses/{pathlib.Path(f).as_posix()}')
468469

469470
def build(self, directory: Path) -> pathlib.Path:
471+
472+
if sys.platform == 'win32' and if self._has_internal_libs and not self._allow_windows_shared_libs:
473+
raise ConfigError(
474+
'Loading shared libraries bundled in the Python wheel on Windows requires '
475+
'setting the DLL load path or preloading. See the documentation for '
476+
'the "tool.meson-python.allow-windows-internal-shared-libs" option.')
477+
470478
wheel_file = pathlib.Path(directory, f'{self.name}.whl')
471479
with mesonpy._wheelfile.WheelFile(wheel_file, 'w') as whl:
472480
self._wheel_write_metadata(whl)
@@ -476,7 +484,7 @@ def build(self, directory: Path) -> pathlib.Path:
476484
root = 'purelib' if self._pure else 'platlib'
477485

478486
for path, entries in self._manifest.items():
479-
for dst, src in entries:
487+
for dst, src, rpath in entries:
480488
counter.update(src)
481489

482490
if path == root:
@@ -487,7 +495,7 @@ def build(self, directory: Path) -> pathlib.Path:
487495
else:
488496
dst = pathlib.Path(self._data_dir, path, dst)
489497

490-
self._install_path(whl, src, dst)
498+
self._install_path(whl, src, dst, rpath)
491499

492500
return wheel_file
493501

mesonpy/_rpath.py

Lines changed: 109 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,70 @@
1111

1212

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

1616
from mesonpy._compat import Iterable, 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:
22-
pass
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
2327

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

26-
def _get_rpath(filepath: Path) -> List[str]:
29+
class RPATH:
30+
origin = '$ORIGIN'
31+
32+
@staticmethod
33+
def get_rpath(filepath: Path) -> List[str]:
34+
raise NotImplementedError
35+
36+
@staticmethod
37+
def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None:
38+
raise NotImplementedError
39+
40+
@classmethod
41+
def fix_rpath(self, filepath: Path, install_rpath: Optional[str], libs_rpath: Optional[str]) -> None:
42+
old_rpath = self.get_rpath(filepath)
43+
new_rpath = []
44+
if libs_rpath is not None:
45+
if libs_rpath == '.':
46+
libs_rpath = ''
47+
for path in old_rpath:
48+
if path.split('/', 1)[0] == self.origin:
49+
# Any RPATH entry relative to ``$ORIGIN`` is interpreted as
50+
# pointing to a location in the build directory added by
51+
# Meson. These need to be removed. Their presence indicates
52+
# that the executable, shared library, or Python module
53+
# depends on libraries build as part of the package. These
54+
# entries are thus replaced with entries pointing to the
55+
# ``.<package-name>.mesonpy.libs`` folder where meson-python
56+
# relocates shared libraries distributed with the package.
57+
# The package may however explicitly install these in a
58+
# different location, thus this is not a perfect heuristic
59+
# and may add not required RPATH entries. These are however
60+
# harmless.
61+
path = f'{self.origin}/{libs_rpath}'
62+
# Any other RPATH entry is preserved.
63+
new_rpath.append(path)
64+
if install_rpath:
65+
# Add the RPATH entry spcified with the ``install_rpath`` argument.
66+
new_rpath.append(install_rpath)
67+
# Make the RPATH entries unique.
68+
new_rpath = unique(new_rpath)
69+
if new_rpath != old_rpath:
70+
self.set_rpath(filepath, old_rpath, new_rpath)
71+
72+
73+
class _MacOS(RPATH):
74+
origin = '@loader_path'
75+
76+
@staticmethod
77+
def get_rpath(filepath: Path) -> List[str]:
2778
rpath = []
2879
r = subprocess.run(['otool', '-l', os.fspath(filepath)], capture_output=True, text=True)
2980
rpath_tag = False
@@ -35,17 +86,31 @@ def _get_rpath(filepath: Path) -> List[str]:
3586
rpath_tag = False
3687
return rpath
3788

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)
40-
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)
45-
46-
elif sys.platform == 'sunos5':
47-
48-
def _get_rpath(filepath: Path) -> List[str]:
89+
@staticmethod
90+
def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None:
91+
args: List[str] = []
92+
for path in rpath:
93+
if path not in old:
94+
args += ['-add_rpath', path]
95+
for path in old:
96+
if path not in rpath:
97+
args += ['-delete_rpath', path]
98+
subprocess.run(['install_name_tool', *args, os.fspath(filepath)], check=True)
99+
100+
@classmethod
101+
def fix_rpath(self, filepath: Path, install_rpath: Optional[str], libs_rpath: Optional[str]) -> None:
102+
if install_rpath is not None:
103+
root, sep, stem = install_rpath.partition('/')
104+
if root == '$ORIGIN':
105+
install_rpath = f'{self.origin}{sep}{stem}'
106+
# warnings.warn('...')
107+
super().fix_rpath(filepath, install_rpath, libs_rpath)
108+
109+
110+
class _SunOS(RPATH):
111+
112+
@staticmethod
113+
def get_rpath(filepath: Path) -> List[str]:
49114
rpath = []
50115
r = subprocess.run(['/usr/bin/elfedit', '-r', '-e', 'dyn:rpath', os.fspath(filepath)],
51116
capture_output=True, check=True, text=True)
@@ -56,35 +121,39 @@ def _get_rpath(filepath: Path) -> List[str]:
56121
rpath.append(path)
57122
return rpath
58123

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

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)
71128

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

75-
def _get_rpath(filepath: Path) -> List[str]:
131+
@staticmethod
132+
def get_rpath(filepath: Path) -> List[str]:
76133
r = subprocess.run(['patchelf', '--print-rpath', os.fspath(filepath)], capture_output=True, text=True)
77134
return r.stdout.strip().split(':')
78135

79-
def _set_rpath(filepath: Path, rpath: Iterable[str]) -> None:
136+
@staticmethod
137+
def set_rpath(filepath: Path, old: Iterable[str], rpath: Iterable[str]) -> None:
80138
subprocess.run(['patchelf','--set-rpath', ':'.join(rpath), os.fspath(filepath)], check=True)
81139

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)
140+
141+
if sys.platform == 'win32' or sys.platform == 'cygwin':
142+
143+
def _get_rpath(filepath: Path) -> List[str]:
144+
return []
145+
146+
def fix_rpath(filepath: Path, install_rpath: Optional[str], libs_rpath: Optional[str]) -> None:
147+
pass
148+
149+
elif sys.platform == 'darwin':
150+
_get_rpath = _MacOS.get_rpath
151+
fix_rpath = _MacOS.fix_rpath
152+
153+
elif sys.platform == 'sunos5':
154+
_get_rpath = _SunOS.get_rpath
155+
fix_rpath = _SunOS.fix_rpath
156+
157+
else:
158+
_get_rpath = _ELF.get_rpath
159+
fix_rpath = _ELF.fix_rpath

tests/packages/link-against-local-lib/meson.build

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ if meson.get_compiler('c').get_id() in ['msvc', 'clang-cl', 'intel-cl']
99
link_args = ['-DEXAMPLE_DLL_IMPORTS']
1010
else
1111
lib_compile_args = []
12-
link_args = ['-Wl,-rpath,custom-rpath']
12+
link_args = ['-Wl,-rpath,custom-rpath-wrong-way']
1313
endif
1414

1515
subdir('lib')
@@ -26,6 +26,7 @@ py.extension_module(
2626
'examplemod.c',
2727
link_with: example_lib,
2828
link_args: link_args,
29+
install_rpath: 'custom-rpath',
2930
install: true,
3031
subdir: 'example',
3132
)

0 commit comments

Comments
 (0)