diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index c56e5592..661d373c 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[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[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..7f1a94da 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -11,19 +11,54 @@ if typing.TYPE_CHECKING: - from typing import List + from typing import List, Optional, TypeVar from mesonpy._compat import Iterable, Path + T = TypeVar('T') -if sys.platform == 'win32' or sys.platform == 'cygwin': - def fix_rpath(filepath: Path, libs_relative_path: str) -> None: - pass +def unique(values: List[T]) -> List[T]: + r = [] + for value in values: + if value not in r: + r.append(value) + return r -elif sys.platform == 'darwin': - def _get_rpath(filepath: Path) -> List[str]: +class RPATH: + origin = '$ORIGIN' + + @staticmethod + def get_rpath(filepath: Path) -> List[str]: + raise NotImplementedError + + @staticmethod + def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None: + raise NotImplementedError + + @classmethod + def fix_rpath(self, filepath: Path, install_rpath: Optional[str], libs_rpath: Optional[str]) -> None: + old_rpath = self.get_rpath(filepath) + new_rpath = [] + if libs_rpath is not None: + if libs_rpath == '.': + libs_rpath = '' + for path in old_rpath: + if path.split('/', 1)[0] == self.origin: + new_rpath.append(f'{self.origin}/{libs_rpath}') + if install_rpath: + new_rpath.append(install_rpath) + new_rpath = unique(new_rpath) + if new_rpath != old_rpath: + self.set_rpath(filepath, old_rpath, new_rpath) + + +class _MacOS(RPATH): + origin = '@loader_path' + + @staticmethod + def get_rpath(filepath: Path) -> List[str]: rpath = [] r = subprocess.run(['otool', '-l', os.fspath(filepath)], capture_output=True, text=True) rpath_tag = False @@ -35,17 +70,31 @@ 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 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) - -elif sys.platform == 'sunos5': - - def _get_rpath(filepath: Path) -> List[str]: + @staticmethod + def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None: + args = [] + for path in old: + if path == rpath[0]: + rpath.pop(0) + continue + args.extend(('-delete_rpath', path)) + for path in rpath: + args.extend(('-add_rpath', path)) + subprocess.run(['install_name_tool', *args, os.fspath(filepath)], check=True) + + @classmethod + def fix_rpath(self, filepath: Path, install_rpath: Optional[str], libs_rpath: Optional[str]) -> None: + root, sep, stem = install_rpath.partition('/') + if root == '$ORIGIN': + install_rpath = f'{self.origin}{sep}{stem}' + #warnings.warn('something') + super().fix_rpath(filepath, install_rpath, libs_rpath) + + +class _SunOS(RPATH): + + @staticmethod + def get_rpath(filepath: Path) -> List[str]: rpath = [] r = subprocess.run(['/usr/bin/elfedit', '-r', '-e', 'dyn:rpath', os.fspath(filepath)], capture_output=True, check=True, text=True) @@ -56,35 +105,33 @@ def _get_rpath(filepath: Path) -> List[str]: rpath.append(path) return rpath - def _set_rpath(filepath: Path, rpath: Iterable[str]) -> None: + @staticmethod + def set_rpath(filepath: Path, old: Iterable[str], 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: - 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 new_rpath != old_rpath: - _set_rpath(filepath, new_rpath) -else: - # Assume that any other platform uses ELF binaries. +class _ELF(RPATH): - def _get_rpath(filepath: Path) -> List[str]: + @staticmethod + def get_rpath(filepath: Path) -> List[str]: r = subprocess.run(['patchelf', '--print-rpath', os.fspath(filepath)], capture_output=True, text=True) return r.stdout.strip().split(':') - def _set_rpath(filepath: Path, rpath: Iterable[str]) -> None: + @staticmethod + def set_rpath(filepath: Path, old: Iterable[str], 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: - 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 new_rpath != old_rpath: - _set_rpath(filepath, new_rpath) + +if sys.platform == 'win32' or sys.platform == 'cygwin': + + def fix_rpath(filepath: Path, install_rpath: Optional[str], libs_rpath: Optional[str]) -> None: + pass + +elif sys.platform == 'darwin': + fix_rpath = _MacOS.fix_rpath + +elif sys.platform == 'sunos5': + fix_rpath = _SunOS.fix_rpath + +else: + fix_rpath = _ELF.fix_rpath diff --git a/tests/packages/link-against-local-lib/meson.build b/tests/packages/link-against-local-lib/meson.build index e8cd2830..de4aa4a8 100644 --- a/tests/packages/link-against-local-lib/meson.build +++ b/tests/packages/link-against-local-lib/meson.build @@ -9,7 +9,7 @@ if meson.get_compiler('c').get_id() in ['msvc', 'clang-cl', 'intel-cl'] link_args = ['-DEXAMPLE_DLL_IMPORTS'] else lib_compile_args = [] - link_args = ['-Wl,-rpath,custom-rpath'] + link_args = ['-Wl,-rpath,custom-rpath-wrong-way'] endif subdir('lib') @@ -26,6 +26,7 @@ py.extension_module( 'examplemod.c', link_with: example_lib, link_args: link_args, + install_rpath: 'custom-rpath', install: true, subdir: 'example', )