Skip to content

Commit

Permalink
ENH: correctly handle install_rpath when installing into wheels
Browse files Browse the repository at this point in the history
  • Loading branch information
dnicolodi committed Feb 15, 2025
1 parent 196bb57 commit 562e75b
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 50 deletions.
4 changes: 4 additions & 0 deletions docs/reference/meson-compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 17 additions & 9 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 ``.<project-name>.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())
Expand Down Expand Up @@ -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:
Expand All @@ -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

Expand Down
149 changes: 109 additions & 40 deletions mesonpy/_rpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ``.<package-name>.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
Expand All @@ -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)
Expand All @@ -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
3 changes: 2 additions & 1 deletion tests/packages/link-against-local-lib/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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',
)

0 comments on commit 562e75b

Please sign in to comment.