Skip to content

Commit 37c79fc

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. This requires Meson 1.6.0 or later. Deduplicate RPATH entries. Fixes #711.
1 parent 196bb57 commit 37c79fc

File tree

3 files changed

+138
-54
lines changed

3 files changed

+138
-54
lines changed

docs/reference/meson-compatibility.rst

+4
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

+23-15
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 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

+111-39
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,81 @@
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

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

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]:
100+
@staticmethod
101+
def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None:
102+
args: List[str] = []
103+
for path in rpath:
104+
if path not in old:
105+
args += ['-add_rpath', path]
106+
for path in old:
107+
if path not in rpath:
108+
args += ['-delete_rpath', path]
109+
subprocess.run(['install_name_tool', *args, os.fspath(filepath)], check=True)
110+
111+
@classmethod
112+
def fix_rpath(cls, filepath: Path, install_rpath: Optional[str], libs_rpath: Optional[str]) -> None:
113+
if install_rpath is not None:
114+
root, sep, stem = install_rpath.partition('/')
115+
if root == '$ORIGIN':
116+
install_rpath = f'{cls.origin}{sep}{stem}'
117+
# warnings.warn('...')
118+
super().fix_rpath(filepath, install_rpath, libs_rpath)
119+
120+
121+
class _SunOS(RPATH):
122+
123+
@staticmethod
124+
def get_rpath(filepath: Path) -> List[str]:
49125
rpath = []
50126
r = subprocess.run(['/usr/bin/elfedit', '-r', '-e', 'dyn:rpath', os.fspath(filepath)],
51127
capture_output=True, check=True, text=True)
@@ -56,35 +132,31 @@ def _get_rpath(filepath: Path) -> List[str]:
56132
rpath.append(path)
57133
return rpath
58134

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

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

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

75-
def _get_rpath(filepath: Path) -> List[str]:
142+
@staticmethod
143+
def get_rpath(filepath: Path) -> List[str]:
76144
r = subprocess.run(['patchelf', '--print-rpath', os.fspath(filepath)], capture_output=True, text=True)
77145
return r.stdout.strip().split(':')
78146

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

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)
151+
152+
if sys.platform == 'win32' or sys.platform == 'cygwin':
153+
_cls = _Windows
154+
elif sys.platform == 'darwin':
155+
_cls = _MacOS
156+
elif sys.platform == 'sunos5':
157+
_cls = _SunOS
158+
else:
159+
_cls = _ELF
160+
161+
_get_rpath = _cls.get_rpath
162+
fix_rpath = _cls.fix_rpath

0 commit comments

Comments
 (0)