Skip to content

Fixes relative to RPATH handfling #788

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 31 additions & 36 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import collections
import contextlib
import copy
import dataclasses
import difflib
import functools
import importlib.machinery
Expand Down Expand Up @@ -111,9 +112,14 @@ class InvalidLicenseExpression(Exception): # type: ignore[no-redef]
}


def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]:
class Entry(typing.NamedTuple):
dst: pathlib.Path
src: str


def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[Entry]]:
"""Map files to the wheel, organized by wheel installation directory."""
wheel_files: DefaultDict[str, List[Tuple[pathlib.Path, str]]] = collections.defaultdict(list)
wheel_files: DefaultDict[str, List[Entry]] = collections.defaultdict(list)
packages: Dict[str, str] = {}

for key, group in sources.items():
Expand All @@ -131,7 +137,8 @@ def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[T
other = packages.setdefault(package, path)
if other != path:
this = os.fspath(pathlib.Path(path, *destination.parts[1:]))
that = os.fspath(other / next(d for d, s in wheel_files[other] if d.parts[0] == destination.parts[1]))
module = next(entry.dst for entry in wheel_files[other] if entry.dst.parts[0] == destination.parts[1])
that = os.fspath(other / module)
raise BuildError(
f'The {package} package is split between {path} and {other}: '
f'{this!r} and {that!r}, a "pure: false" argument may be missing in meson.build. '
Expand All @@ -154,9 +161,9 @@ def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[T
if relpath in exclude_files:
continue
filedst = dst / relpath
wheel_files[path].append((filedst, filesrc))
wheel_files[path].append(Entry(filedst, filesrc))
else:
wheel_files[path].append((dst, src))
wheel_files[path].append(Entry(dst, src))

return wheel_files

Expand Down Expand Up @@ -303,20 +310,14 @@ def _is_native(file: Path) -> bool:
return f.read(4) == b'\x7fELF' # ELF


@dataclasses.dataclass
class _WheelBuilder():
"""Helper class to build wheels from projects."""

def __init__(
self,
metadata: Metadata,
manifest: Dict[str, List[Tuple[pathlib.Path, str]]],
limited_api: bool,
allow_windows_shared_libs: bool,
) -> None:
self._metadata = metadata
self._manifest = manifest
self._limited_api = limited_api
self._allow_windows_shared_libs = allow_windows_shared_libs
_metadata: Metadata
_manifest: Dict[str, List[Entry]]
_limited_api: bool
_allow_windows_shared_libs: bool

@property
def _has_internal_libs(self) -> bool:
Expand All @@ -332,8 +333,8 @@ def _pure(self) -> bool:
"""Whether the wheel is architecture independent"""
if self._manifest['platlib'] or self._manifest['mesonpy-libs']:
return False
for _, file in self._manifest['scripts']:
if _is_native(file):
for entry in self._manifest['scripts']:
if _is_native(entry.src):
return False
return True

Expand Down Expand Up @@ -410,14 +411,14 @@ def _stable_abi(self) -> Optional[str]:
# in {platlib} that look like extension modules, and raise
# an exception if any of them has a Python version
# specific extension filename suffix ABI tag.
for path, _ in self._manifest['platlib']:
match = _EXTENSION_SUFFIX_REGEX.match(path.name)
for entry in self._manifest['platlib']:
match = _EXTENSION_SUFFIX_REGEX.match(entry.dst.name)
if match:
abi = match.group('abi')
if abi is not None and abi != 'abi3':
raise BuildError(
f'The package declares compatibility with Python limited API but extension '
f'module {os.fspath(path)!r} is tagged for a specific Python version.')
f'module {os.fspath(entry.dst)!r} is tagged for a specific Python version.')
return 'abi3'
return None

Expand All @@ -426,18 +427,6 @@ def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path,

if self._has_internal_libs:
if _is_native(origin):
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
# 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
# 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)

Expand Down Expand Up @@ -468,6 +457,12 @@ def _wheel_write_metadata(self, whl: mesonpy._wheelfile.WheelFile) -> None:
whl.write(f, f'{self._distinfo_dir}/licenses/{pathlib.Path(f).as_posix()}')

def build(self, directory: Path) -> pathlib.Path:
if sys.platform == 'win32' and self._has_internal_libs 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.')

wheel_file = pathlib.Path(directory, f'{self.name}.whl')
with mesonpy._wheelfile.WheelFile(wheel_file, 'w') as whl:
self._wheel_write_metadata(whl)
Expand Down Expand Up @@ -499,8 +494,8 @@ class _EditableWheelBuilder(_WheelBuilder):
def _top_level_modules(self) -> Collection[str]:
modules = set()
for type_ in self._manifest:
for path, _ in self._manifest[type_]:
name, dot, ext = path.parts[0].partition('.')
for entry in self._manifest[type_]:
name, dot, ext = entry.dst.parts[0].partition('.')
if dot:
# module
suffix = dot + ext
Expand Down Expand Up @@ -886,7 +881,7 @@ def _info(self, name: str) -> Any:
return json.loads(info.read_text(encoding='utf-8'))

@property
def _manifest(self) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]:
def _manifest(self) -> DefaultDict[str, List[Entry]]:
"""The files to be added to the wheel, organized by wheel path."""

# Obtain the list of files Meson would install.
Expand Down
130 changes: 92 additions & 38 deletions mesonpy/_rpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,69 @@


if typing.TYPE_CHECKING:
from typing import List
from typing import List, TypeVar

from mesonpy._compat import Iterable, Path
from mesonpy._compat import Path

T = TypeVar('T')

if sys.platform == 'win32' or sys.platform == 'cygwin':

def fix_rpath(filepath: Path, libs_relative_path: str) -> None:
def unique(values: List[T]) -> List[T]:
r = []
for value in values:
if value not in r:
r.append(value)
return r


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(cls, filepath: Path, libs_relative_path: str) -> None:
old_rpath = cls.get_rpath(filepath)
new_rpath = old_rpath[:]

# When an executable, libray, or Python extension module is
# dynamically linked to a library built as part of the project, Meson
# adds a build RPATH pointing to the build directory, in the form of a
# relative RPATH entry. We can use the presence of any RPATH entries
# relative to ``$ORIGIN`` as an indicator that the installed object
# depends on shared libraries internal to the project. In this case we
# need to add an RPATH entry pointing to the meson-python shared
# library install location. This heuristic is not perfect: RPATH
# entries relative to ``$ORIGIN`` can exist for other reasons.
# However, this only results in harmless additional RPATH entries.
if any(path.startswith(cls.origin) for path in old_rpath):
new_rpath.append(os.path.join(cls.origin, libs_relative_path))

new_rpath = unique(new_rpath)
if new_rpath != old_rpath:
cls.set_rpath(filepath, old_rpath, new_rpath)


class _Windows(RPATH):

@classmethod
def fix_rpath(cls, filepath: Path, libs_relative_path: str) -> None:
pass

elif sys.platform == 'darwin':

def _get_rpath(filepath: Path) -> List[str]:
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 +85,24 @@ 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)
@staticmethod
def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None:
# This implementation does not preserve the ordering of RPATH
# entries. Meson does the same, thus it should not be a problem.
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)

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':
class _SunOS5(RPATH):

def _get_rpath(filepath: Path) -> List[str]:
@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 +113,32 @@ 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: List[str], rpath: List[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(':')
return [x for x in r.stdout.strip().split(':') if x]

def _set_rpath(filepath: Path, rpath: Iterable[str]) -> None:
@staticmethod
def set_rpath(filepath: Path, old: List[str], rpath: List[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':
_cls = _Windows
elif sys.platform == 'darwin':
_cls = _MacOS
elif sys.platform == 'sunos5':
_cls = _SunOS5
else:
_cls = _ELF

get_rpath = _cls.get_rpath
set_rpath = _cls.set_rpath
fix_rpath = _cls.fix_rpath
3 changes: 3 additions & 0 deletions tests/packages/sharedlib-in-package/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ project('sharedlib-in-package', 'c', version: '1.0.0')

py = import('python').find_installation(pure: false)

origin = build_machine.system() == 'darwin' ? '@loader_path' : '$ORIGIN'

subdir('src')
subdir('mypkg')
4 changes: 2 additions & 2 deletions tests/packages/sharedlib-in-package/mypkg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def _append_to_sharedlib_load_path():
# end-literalinclude


from ._example import example_prod, example_sum #noqa: E402
from ._example import prodsum # noqa: E402


__all__ = ['example_prod', 'example_sum']
__all__ = ['prodsum']
25 changes: 6 additions & 19 deletions tests/packages/sharedlib-in-package/mypkg/_examplemod.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,23 @@

#include <Python.h>

#include "examplelib.h"
#include "examplelib2.h"
#include "lib.h"

static PyObject* example_sum(PyObject* self, PyObject *args)
static PyObject* example_prodsum(PyObject* self, PyObject *args)
{
int a, b;
if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
return NULL;
}
int a, b, x;

long result = sum(a, b);

return PyLong_FromLong(result);
}

static PyObject* example_prod(PyObject* self, PyObject *args)
{
int a, b;
if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
if (!PyArg_ParseTuple(args, "iii", &a, &b, &x)) {
return NULL;
}

long result = prod(a, b);
long result = prodsum(a, b, x);

return PyLong_FromLong(result);
}

static PyMethodDef methods[] = {
{"example_prod", (PyCFunction)example_prod, METH_VARARGS, NULL},
{"example_sum", (PyCFunction)example_sum, METH_VARARGS, NULL},
{"prodsum", (PyCFunction)example_prodsum, METH_VARARGS, NULL},
{NULL, NULL, 0, NULL},
};

Expand Down
9 changes: 0 additions & 9 deletions tests/packages/sharedlib-in-package/mypkg/examplelib.c

This file was deleted.

7 changes: 0 additions & 7 deletions tests/packages/sharedlib-in-package/mypkg/examplelib.h

This file was deleted.

Loading
Loading