Skip to content

Commit a9d8574

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 cc3c583 commit a9d8574

File tree

3 files changed

+138
-54
lines changed

3 files changed

+138
-54
lines changed

docs/reference/meson-compatibility.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ versions.
5252
populate the package license and license files from the ones
5353
declared via the ``project()`` call in ``meson.build``.
5454

55+
Meson 1.6.0 or later is also required for support for the
56+
``install_rpath`` argument to Meson functions declaring build rules
57+
for object files.
58+
5559
Build front-ends by default build packages in an isolated Python
5660
environment where build dependencies are installed. Most often, unless
5761
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
@@ -115,6 +115,7 @@ class InvalidLicenseExpression(Exception): # type: ignore[no-redef]
115115
class Entry(typing.NamedTuple):
116116
dst: pathlib.Path
117117
src: str
118+
rpath: Optional[str] = None
118119

119120

120121
def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[Entry]]:
@@ -163,7 +164,7 @@ def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[E
163164
filedst = dst / relpath
164165
wheel_files[path].append(Entry(filedst, filesrc))
165166
else:
166-
wheel_files[path].append(Entry(dst, src))
167+
wheel_files[path].append(Entry(dst, src, target.get('install_rpath')))
167168

168169
return wheel_files
169170

@@ -422,25 +423,25 @@ def _stable_abi(self) -> Optional[str]:
422423
return 'abi3'
423424
return None
424425

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

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

445446
try:
446447
wheel_file.write(origin, destination.as_posix())
@@ -469,6 +470,13 @@ def _wheel_write_metadata(self, whl: mesonpy._wheelfile.WheelFile) -> None:
469470
whl.write(f, f'{self._distinfo_dir}/licenses/{pathlib.Path(f).as_posix()}')
470471

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

480488
for path, entries in self._manifest.items():
481-
for dst, src in entries:
489+
for dst, src, rpath in entries:
482490
counter.update(src)
483491

484492
if path == root:
@@ -489,7 +497,7 @@ def build(self, directory: Path) -> pathlib.Path:
489497
else:
490498
dst = pathlib.Path(self._data_dir, path, dst)
491499

492-
self._install_path(whl, src, dst)
500+
self._install_path(whl, src, dst, rpath)
493501

494502
return wheel_file
495503

mesonpy/_rpath.py

Lines changed: 111 additions & 39 deletions
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:
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 _Windows:
30+
31+
@staticmethod
32+
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:
2237
pass
2338

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

26-
def _get_rpath(filepath: Path) -> List[str]:
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)