diff --git a/docs/reference/meson-compatibility.rst b/docs/reference/meson-compatibility.rst index 6b339d60..dfa96432 100644 --- a/docs/reference/meson-compatibility.rst +++ b/docs/reference/meson-compatibility.rst @@ -53,6 +53,10 @@ versions. declared via the ``project()`` call in ``meson.build``. This also requires ``pyproject-metadata`` version 0.9.0 or later. + Meson 1.6.0 or later is also required for support for the + ``install_rpath`` argument to Meson functions declaring build rules + for object files. + Build front-ends by default build packages in an isolated Python environment where build dependencies are installed. Most often, unless a package or its build dependencies declare explicitly a version diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index c56e5592..0a977e4a 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,25 +421,32 @@ 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 ' 'setting the DLL load path or preloading. See the documentation for ' 'the "tool.meson-python.allow-windows-internal-shared-libs" option.') - # When an executable, libray, or Python extension module is + # When an executable, library, or Python extension module is # dynamically linked to a library built as part of the project, # Meson adds a library load path to it pointing to the build # directory, in the form of a relative RPATH entry. meson-python - # relocates the shared libraries to the $project.mesonpy.libs + # relocates the shared libraries to the ``..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) + + # Adjust RPATH: remove build RPATH added by meson, add an RPATH + # entries as per above, and add any ``install_rpath`` specified in + # meson.build + mesonpy._rpath.fix_rpath(origin, rpath, libspath) try: wheel_file.write(origin, destination.as_posix()) @@ -476,7 +484,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 +495,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..67719cdf 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -11,19 +11,70 @@ 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: + # Any RPATH entry relative to ``$ORIGIN`` is interpreted as + # pointing to a location in the build directory added by + # Meson. These need to be removed. Their presence indicates + # that the executable, shared library, or Python module + # depends on libraries build as part of the package. These + # entries are thus replaced with entries pointing to the + # ``..mesonpy.libs`` folder where meson-python + # relocates shared libraries distributed with the package. + # The package may however explicitly install these in a + # different location, thus this is not a perfect heuristic + # and may add not required RPATH entries. These are however + # harmless. + path = f'{self.origin}/{libs_rpath}' + # Any other RPATH entry is preserved. + new_rpath.append(path) + if install_rpath: + # Add the RPATH entry spcified with the ``install_rpath`` argument. + new_rpath.append(install_rpath) + # Make the RPATH entries unique. + 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 +86,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: List[str] = [] + for path in rpath: + if path not in old: + args += ['-add_rpath', path] + for path in old: + if path not in rpath: + args += ['-delete_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: + if install_rpath is not None: + root, sep, stem = install_rpath.partition('/') + if root == '$ORIGIN': + install_rpath = f'{self.origin}{sep}{stem}' + # warnings.warn('...') + 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 +121,39 @@ 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 _get_rpath(filepath: Path) -> List[str]: + return [] + + def fix_rpath(filepath: Path, install_rpath: Optional[str], libs_rpath: Optional[str]) -> None: + pass + +elif sys.platform == 'darwin': + _get_rpath = _MacOS.get_rpath + fix_rpath = _MacOS.fix_rpath + +elif sys.platform == 'sunos5': + _get_rpath = _SunOS.get_rpath + fix_rpath = _SunOS.fix_rpath + +else: + _get_rpath = _ELF.get_rpath + 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', )