Skip to content
Open
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
11 changes: 11 additions & 0 deletions docs/reference/meson-compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,21 @@ versions.
populate the package license and license files from the ones
declared via the ``project()`` call in ``meson.build``.

Meson 1.6.0 or later is also required to support the
``install_rpath`` argument to Meson functions that accept it, such
as ``library()`` and ``extension_module()`` from the ``python``
Meson module. On older Meson versions, this argument has no effect.

.. option:: 1.9.0

Meson 1.9.0 or later is required to support building for iOS.

Meson 1.9.0 or later is also required to remove RPATH entries that
are added by Meson to allow to execute parts of the project from
the build directory and that are normally removed during ``meson
install``. On older Meson versions, these entries are not removed,
but this should not have any adverse effect.

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
45 changes: 25 additions & 20 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ def _compile_patterns(patterns: List[str]) -> Callable[[str], bool]:
class _Entry(typing.NamedTuple):
dst: pathlib.Path
src: str
# Meson support only one install_rpath entry per target. Use a
# list to store install RPATH to be able to add append more
# entries when needed.
install_rpath: List[str] = []
# RPATH entries to remove at install time.
build_rpath: List[str] = []


def _map_to_wheel(
Expand Down Expand Up @@ -183,7 +189,9 @@ def _map_to_wheel(
filedst = dst / relpath
wheel_files[path].append(_Entry(filedst, filesrc))
else:
wheel_files[path].append(_Entry(dst, src))
install_rpath = target.get('install_rpath')
build_rpath = target.get('build_rpaths')
wheel_files[path].append(_Entry(dst, src, [install_rpath] if install_rpath else [], build_rpath or []))

return wheel_files

Expand Down Expand Up @@ -449,25 +457,16 @@ def _stable_abi(self) -> Optional[str]:
return 'abi3.abi3t' if abi3t else '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,
install_rpath: List[str], build_rpath: List[str]) -> None:
"""Add a file to the wheel."""

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)
if self._has_internal_libs and _is_native(origin):
libspath = os.path.relpath(self._libs_dir, destination.parent)
mesonpy._rpath.fix_rpath(origin, install_rpath, build_rpath, libspath)
elif install_rpath or build_rpath:
mesonpy._rpath.fix_rpath(origin, install_rpath, build_rpath, None)

try:
wheel_file.write(origin, destination.as_posix())
Expand Down Expand Up @@ -496,6 +495,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 All @@ -505,7 +510,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, install_rpath, build_rpath in entries:
counter.update(src)

if path == root:
Expand All @@ -516,7 +521,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, install_rpath, build_rpath)

return wheel_file

Expand Down
135 changes: 98 additions & 37 deletions mesonpy/_rpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,75 @@


if typing.TYPE_CHECKING:
from typing import Iterable, List, Union
from typing import List, Optional, TypeVar, Union
Path = Union[str, os.PathLike[str]]
T = TypeVar('T')


if sys.platform == 'win32' or sys.platform == 'cygwin':
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, add: List[str], remove: List[str], libs_relative_path: Optional[str]) -> None:
old_rpath = cls.get_rpath(filepath)

# Meson adds a padding entry to RPATH composed of enough `X`
# characters to reserve enough space in the ELF header to hold
# the final installation RPATH. Remove this entry and other
# entries to be removed.
new_rpath = [path for path in old_rpath if path.strip('X') and path not in remove]

# 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 libs_relative_path and any(path.startswith(cls.origin) for path in old_rpath):
new_rpath.append(os.path.join(cls.origin, libs_relative_path))

# Add install_rpath.
new_rpath += add

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


class _Windows(RPATH):

def fix_rpath(filepath: Path, libs_relative_path: str) -> None:
@classmethod
def fix_rpath(cls, filepath: Path, add: List[str], remove: List[str], libs_relative_path: Optional[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 @@ -34,17 +91,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 @@ -55,35 +119,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
8 changes: 4 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,11 @@ def __init__(self, source_dir, build_dir, meson_args=None, editable_verbose=None
# toolchains.
'cmake-subproject',

# The ``link-against-local-lib`` package uses linker arguments
# to add RPATH entries. This functionality is deprecated in
# Meson but it is used in the wild thus we should make sure it
# keeps working.
# These packages use linker arguments to add RPATH entries.
# This functionality is deprecated in Meson but it is used
# in the wild thus we should make sure it keeps working.
'link-against-local-lib',
'sharedlib-in-package',

}:
if meson_args is None:
Expand Down
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