From f487199e2d5f451c0b95ba29c5519eae00b3dabe Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Thu, 13 Feb 2025 18:15:28 +0100 Subject: [PATCH] ENH: correctly handle install_rpath when installing into wheels Fixes #711. --- mesonpy/__init__.py | 21 ++++++++---- mesonpy/_rpath.py | 78 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 74 insertions(+), 25 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index c56e5592..6cc9676b 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -113,6 +113,7 @@ class InvalidLicenseExpression(Exception): # type: ignore[no-redef] class Entry(typing.NamedTuple): dst: pathlib.Path src: str + rpath: Optional[List[str]] = None 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 filedst = dst / relpath wheel_files[path].append(Entry(filedst, filesrc)) else: - wheel_files[path].append(Entry(dst, src)) + wheel_files[path].append(Entry(dst, src, target.get('install_rpath'))) return wheel_files @@ -420,11 +421,14 @@ def _stable_abi(self) -> Optional[str]: return 'abi3' return None - def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path, destination: pathlib.Path) -> None: + def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, + origin: Path, destination: pathlib.Path, rpath: Optional[List[str]]) -> None: """Add a file to the wheel.""" - if self._has_internal_libs: - if _is_native(origin): + if _is_native(origin): + libspath = None + + if self._has_internal_libs: if sys.platform == 'win32' and not self._allow_windows_shared_libs: raise NotImplementedError( 'Loading shared libraries bundled in the Python wheel on Windows requires ' @@ -438,7 +442,10 @@ def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path, # relocates the shared libraries to the $project.mesonpy.libs # folder. Rewrite the RPATH to point to that folder instead. libspath = os.path.relpath(self._libs_dir, destination.parent) - mesonpy._rpath.fix_rpath(origin, libspath) + + # Apply RPATH as per above and any ``install_rpath`` specified in meson.build + if rpath or libspath is not None: + mesonpy._rpath.fix_rpath(origin, rpath, libspath) try: wheel_file.write(origin, destination.as_posix()) @@ -476,7 +483,7 @@ def build(self, directory: Path) -> pathlib.Path: root = 'purelib' if self._pure else 'platlib' for path, entries in self._manifest.items(): - for dst, src in entries: + for dst, src, rpath in entries: counter.update(src) if path == root: @@ -487,7 +494,7 @@ def build(self, directory: Path) -> pathlib.Path: else: dst = pathlib.Path(self._data_dir, path, dst) - self._install_path(whl, src, dst) + self._install_path(whl, src, dst, rpath) return wheel_file diff --git a/mesonpy/_rpath.py b/mesonpy/_rpath.py index a7cbbb92..448882df 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -9,16 +9,28 @@ import sys import typing +from itertools import chain + if typing.TYPE_CHECKING: - from typing import List + from typing import List, Optional, TypeVar from mesonpy._compat import Iterable, Path + T = TypeVar('T') + + +def unique(values: List[T]) -> List[T]: + r = [] + for value in values: + if value not in r: + r.append(value) + return r + if sys.platform == 'win32' or sys.platform == 'cygwin': - def fix_rpath(filepath: Path, libs_relative_path: str) -> None: + def fix_rpath(filepath: Path, rpath: List[str]) -> None: pass elif sys.platform == 'darwin': @@ -35,13 +47,33 @@ def _get_rpath(filepath: Path) -> List[str]: rpath_tag = False return rpath - def _replace_rpath(filepath: Path, old: str, new: str) -> None: - subprocess.run(['install_name_tool', '-rpath', old, new, os.fspath(filepath)], check=True) + def _delete_rpath(filepath: Path, rpath: Iterable[str]) -> None: + args = list(chain(*(('-delete_rpath', path) for path in rpath))) + if not args: + return + subprocess.run(['install_name_tool', *args, os.fspath(filepath)], check=True) - def fix_rpath(filepath: Path, libs_relative_path: str) -> None: - for path in _get_rpath(filepath): - if path.startswith('@loader_path/'): - _replace_rpath(filepath, path, '@loader_path/' + libs_relative_path) + def _add_rpath(filepath: Path, rpath: Iterable[str]) -> None: + args = list(chain(*(('-add_rpath', path) for path in rpath))) + if not args: + return + subprocess.run(['install_name_tool', *args, os.fspath(filepath)], check=True) + + def fix_rpath(filepath: Path, install_rpath: Optional[List[str]], libs_rpath: Optional[str]) -> None: + old_rpath = _get_rpath(filepath) + new_rpath = [] + if libs_rpath is not None: + if libs_rpath == '.': + libs_rpath = '' + for path in old_rpath: + if path.startswith('@loader_path/'): + new_rpath.append('@loader_path/' + libs_rpath) + if install_rpath: + new_rpath.extend(install_rpath) + new_rpath = unique(new_rpath) + if new_rpath != old_rpath: + _delete_rpath(filepath, old_rpath) + _add_rpath(filepath, new_rpath) elif sys.platform == 'sunos5': @@ -59,13 +91,18 @@ def _get_rpath(filepath: Path) -> List[str]: def _set_rpath(filepath: Path, rpath: Iterable[str]) -> None: subprocess.run(['/usr/bin/elfedit', '-e', 'dyn:rpath ' + ':'.join(rpath), os.fspath(filepath)], check=True) - def fix_rpath(filepath: Path, libs_relative_path: str) -> None: + def fix_rpath(filepath: Path, rpath: List[str], libs_rpath: Optional[str]) -> None: old_rpath = _get_rpath(filepath) new_rpath = [] - for path in old_rpath: - if path.startswith('$ORIGIN/'): - path = '$ORIGIN/' + libs_relative_path - new_rpath.append(path) + if libs_rpath is not None: + if libs_rpath == '.': + libs_rpath = '' + for path in old_rpath: + if path.startswith('$ORIGIN/'): + new_rpath.append('$ORIGIN/' + libs_rpath) + if install_rpath: + new_rpath.extend(install_rpath) + new_rpath = unique(new_rpath) if new_rpath != old_rpath: _set_rpath(filepath, new_rpath) @@ -79,12 +116,17 @@ def _get_rpath(filepath: Path) -> List[str]: def _set_rpath(filepath: Path, rpath: Iterable[str]) -> None: subprocess.run(['patchelf','--set-rpath', ':'.join(rpath), os.fspath(filepath)], check=True) - def fix_rpath(filepath: Path, libs_relative_path: str) -> None: + def fix_rpath(filepath: Path, rpath: List[str], libs_rpath: Optional[str]) -> None: old_rpath = _get_rpath(filepath) new_rpath = [] - for path in old_rpath: - if path.startswith('$ORIGIN/'): - path = '$ORIGIN/' + libs_relative_path - new_rpath.append(path) + if libs_rpath is not None: + if libs_rpath == '.': + libs_rpath = '' + for path in old_rpath: + if path.startswith('$ORIGIN/'): + new_rpath.append('$ORIGIN/' + libs_rpath) + if install_rpath: + new_rpath.extend(install_rpath) + new_rpath = unique(new_rpath) if new_rpath != old_rpath: _set_rpath(filepath, new_rpath)