diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index d4dd3bd0..b8c03b4d 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -15,6 +15,7 @@ import collections import contextlib import copy +import dataclasses import difflib import functools import importlib.machinery @@ -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(): @@ -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. ' @@ -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 @@ -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: @@ -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 @@ -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 @@ -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) @@ -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) @@ -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 @@ -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. diff --git a/mesonpy/_rpath.py b/mesonpy/_rpath.py index a7cbbb92..1902282e 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -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 @@ -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) @@ -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 diff --git a/tests/packages/sharedlib-in-package/meson.build b/tests/packages/sharedlib-in-package/meson.build index 71921cfe..d311a58a 100644 --- a/tests/packages/sharedlib-in-package/meson.build +++ b/tests/packages/sharedlib-in-package/meson.build @@ -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') diff --git a/tests/packages/sharedlib-in-package/mypkg/__init__.py b/tests/packages/sharedlib-in-package/mypkg/__init__.py index 857d2e90..e4fbc2a9 100644 --- a/tests/packages/sharedlib-in-package/mypkg/__init__.py +++ b/tests/packages/sharedlib-in-package/mypkg/__init__.py @@ -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'] diff --git a/tests/packages/sharedlib-in-package/mypkg/_examplemod.c b/tests/packages/sharedlib-in-package/mypkg/_examplemod.c index 080e03c1..b4cc3f0c 100644 --- a/tests/packages/sharedlib-in-package/mypkg/_examplemod.c +++ b/tests/packages/sharedlib-in-package/mypkg/_examplemod.c @@ -4,36 +4,23 @@ #include -#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}, }; diff --git a/tests/packages/sharedlib-in-package/mypkg/examplelib.c b/tests/packages/sharedlib-in-package/mypkg/examplelib.c deleted file mode 100644 index f486bd7f..00000000 --- a/tests/packages/sharedlib-in-package/mypkg/examplelib.c +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2022 The meson-python developers -// -// SPDX-License-Identifier: MIT - -#include "sub/mypkg_dll.h" - -MYPKG_DLL int sum(int a, int b) { - return a + b; -} diff --git a/tests/packages/sharedlib-in-package/mypkg/examplelib.h b/tests/packages/sharedlib-in-package/mypkg/examplelib.h deleted file mode 100644 index c09f4f78..00000000 --- a/tests/packages/sharedlib-in-package/mypkg/examplelib.h +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-FileCopyrightText: 2022 The meson-python developers -// -// SPDX-License-Identifier: MIT - -#include "sub/mypkg_dll.h" - -MYPKG_DLL int sum(int a, int b); diff --git a/tests/packages/sharedlib-in-package/mypkg/meson.build b/tests/packages/sharedlib-in-package/mypkg/meson.build index 75904bed..cebf2d52 100644 --- a/tests/packages/sharedlib-in-package/mypkg/meson.build +++ b/tests/packages/sharedlib-in-package/mypkg/meson.build @@ -2,37 +2,17 @@ # # SPDX-License-Identifier: MIT -if meson.get_compiler('c').get_id() in ['msvc', 'clang-cl', 'intel-cl'] - export_dll_args = ['-DMYPKG_DLL_EXPORTS'] - import_dll_args = ['-DMYPKG_DLL_IMPORTS'] -else - export_dll_args = [] - import_dll_args = [] -endif - -example_lib = shared_library( - 'examplelib', - 'examplelib.c', - c_args: export_dll_args, - install: true, - install_dir: py.get_install_dir() / 'mypkg', -) - -example_lib_dep = declare_dependency( - compile_args: import_dll_args, - link_with: example_lib, -) - -subdir('sub') - py.extension_module( '_example', '_examplemod.c', - dependencies: [example_lib_dep, example_lib2_dep], - include_directories: 'sub', + dependencies: lib_dep, install: true, subdir: 'mypkg', - install_rpath: '$ORIGIN', + # install_rpath is not exposed in the Meson introspection data in Meson + # versions prior to 1.6.0 and thus cannot be set by meson-python when + # building the Python wheel. Use link_args to set the RPATH. + # install_rpath: f'@origin@', + link_args: f'-Wl,-rpath,@origin@', ) py.install_sources( diff --git a/tests/packages/sharedlib-in-package/mypkg/sub/examplelib2.h b/tests/packages/sharedlib-in-package/mypkg/sub/examplelib2.h deleted file mode 100644 index 64b6a907..00000000 --- a/tests/packages/sharedlib-in-package/mypkg/sub/examplelib2.h +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-FileCopyrightText: 2022 The meson-python developers -// -// SPDX-License-Identifier: MIT - -#include "mypkg_dll.h" - -MYPKG_DLL int prod(int a, int b); diff --git a/tests/packages/sharedlib-in-package/src/lib.c b/tests/packages/sharedlib-in-package/src/lib.c new file mode 100644 index 00000000..e4fe1478 --- /dev/null +++ b/tests/packages/sharedlib-in-package/src/lib.c @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2022 The meson-python developers +// +// SPDX-License-Identifier: MIT + +#include "lib.h" +#include "sublib.h" + +int prodsum(int a, int b, int x) { + return prod(a, x) + b; +} diff --git a/tests/packages/sharedlib-in-package/src/lib.h b/tests/packages/sharedlib-in-package/src/lib.h new file mode 100644 index 00000000..fb6a02d8 --- /dev/null +++ b/tests/packages/sharedlib-in-package/src/lib.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2022 The meson-python developers +// +// SPDX-License-Identifier: MIT + +#if defined(MYPKG_DLL_EXPORTS) + #define EXPORT __declspec(dllexport) +#elif defined(MYPKG_DLL_IMPORTS) + #define EXPORT __declspec(dllimport) +#else + #define EXPORT +#endif + +EXPORT int prodsum(int a, int b, int x); diff --git a/tests/packages/sharedlib-in-package/src/meson.build b/tests/packages/sharedlib-in-package/src/meson.build new file mode 100644 index 00000000..b673ef29 --- /dev/null +++ b/tests/packages/sharedlib-in-package/src/meson.build @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2022 The meson-python developers +# +# SPDX-License-Identifier: MIT + +if meson.get_compiler('c').get_id() in ['msvc', 'clang-cl', 'intel-cl'] + export_dll_args = ['-DMYPKG_DLL_EXPORTS'] + import_dll_args = ['-DMYPKG_DLL_IMPORTS'] +else + export_dll_args = [] + import_dll_args = [] +endif + +sublib = shared_library( + 'sublib', + 'sublib.c', + c_args: export_dll_args, + install: true, + install_dir: py.get_install_dir() / 'mypkg/sub', +) + +sublib_dep = declare_dependency( + compile_args: import_dll_args, + link_with: sublib, +) + +lib = shared_library( + 'lib', + 'lib.c', + dependencies: sublib_dep, + c_args: export_dll_args, + install: true, + install_dir: py.get_install_dir() / 'mypkg', + # install_rpath is not exposed in the Meson introspection data in Meson + # versions prior to 1.6.0 and thus cannot be set by meson-python when + # building the Python wheel. Use link_args to set the RPATH. + # install_rpath: f'@origin@/sub', + link_args: f'-Wl,-rpath,@origin@/sub', +) + +lib_dep = declare_dependency( + compile_args: import_dll_args, + link_with: lib, + include_directories: include_directories('.'), +) diff --git a/tests/packages/sharedlib-in-package/mypkg/sub/examplelib2.c b/tests/packages/sharedlib-in-package/src/sublib.c similarity index 66% rename from tests/packages/sharedlib-in-package/mypkg/sub/examplelib2.c rename to tests/packages/sharedlib-in-package/src/sublib.c index 12f5b87a..facfdf2e 100644 --- a/tests/packages/sharedlib-in-package/mypkg/sub/examplelib2.c +++ b/tests/packages/sharedlib-in-package/src/sublib.c @@ -2,8 +2,8 @@ // // SPDX-License-Identifier: MIT -#include "mypkg_dll.h" +#include "sublib.h" -MYPKG_DLL int prod(int a, int b) { +int prod(int a, int b) { return a * b; } diff --git a/tests/packages/sharedlib-in-package/src/sublib.h b/tests/packages/sharedlib-in-package/src/sublib.h new file mode 100644 index 00000000..9fc7ae51 --- /dev/null +++ b/tests/packages/sharedlib-in-package/src/sublib.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2022 The meson-python developers +// +// SPDX-License-Identifier: MIT + +#if defined(MYPKG_DLL_EXPORTS) + #define EXPORT __declspec(dllexport) +#elif defined(MYPKG_DLL_IMPORTS) + #define EXPORT __declspec(dllimport) +#else + #define EXPORT +#endif + +EXPORT int prod(int a, int b); diff --git a/tests/test_rpath.py b/tests/test_rpath.py new file mode 100644 index 00000000..425decd8 --- /dev/null +++ b/tests/test_rpath.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: 2025 The meson-python developers +# +# SPDX-License-Identifier: MIT + +import sys + +import pytest +import wheel.wheelfile + +from mesonpy._rpath import get_rpath, set_rpath + + +@pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') +def test_rpath_get_set(wheel_sharedlib_in_package, tmp_path): + artifact = wheel.wheelfile.WheelFile(wheel_sharedlib_in_package) + artifact.extractall(tmp_path) + obj = list(tmp_path.joinpath('mypkg').glob('_example.*'))[0] + + rpath = get_rpath(obj) + assert rpath + + set_rpath(obj, rpath, []) + rpath = get_rpath(obj) + assert rpath == [] + + new_rpath = ['one', 'two'] + set_rpath(obj, rpath, new_rpath) + rpath = get_rpath(obj) + assert set(rpath) == set(new_rpath) + + new_rpath = ['one', 'three', 'two'] + set_rpath(obj, rpath, new_rpath) + rpath = get_rpath(obj) + assert set(rpath) == set(new_rpath) + + new_rpath = ['one'] + set_rpath(obj, rpath, new_rpath) + rpath = get_rpath(obj) + assert set(rpath) == set(new_rpath) diff --git a/tests/test_tags.py b/tests/test_tags.py index 62aa6193..60194348 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -101,7 +101,8 @@ def test_ios_platform_tag(monkeypatch): def wheel_builder_test_factory(content, pure=True, limited_api=False): manifest = defaultdict(list) - manifest.update({key: [(pathlib.Path(x), os.path.join('build', x)) for x in value] for key, value in content.items()}) + for key, value in content.items(): + manifest[key] = [mesonpy.Entry(pathlib.Path(x), os.path.join('build', x)) for x in value] return mesonpy._WheelBuilder(None, manifest, limited_api, False) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index b6f64cee..908b49c8 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -178,15 +178,50 @@ def test_local_lib(venv, wheel_link_against_local_lib): assert int(output) == 3 +@pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') +def test_sharedlib_in_package_rpath(wheel_sharedlib_in_package, tmp_path): + artifact = wheel.wheelfile.WheelFile(wheel_sharedlib_in_package) + artifact.extractall(tmp_path) + + origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN' + + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / f'_example{EXT_SUFFIX}')) + # This RPATH entry should be removed by meson-python but it is not. + build_rpath = {f'{origin}/../src'} + assert rpath == {origin, *build_rpath} + + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / f'liblib{LIB_SUFFIX}')) + # This RPATH entry should be removed by meson-python but it is not. + build_rpath = {f'{origin}/'} + assert rpath == {f'{origin}/sub', *build_rpath} + + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / 'sub' / f'libsublib{LIB_SUFFIX}')) + assert rpath == set() + + +@pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') +def test_sharedlib_in_package_rpath_ldflags(package_sharedlib_in_package, tmp_path, monkeypatch): + origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN' + extra_rpath = {f'{origin}/test-ldflags', '/usr/lib/test-ldflags'} + ldflags = ' '.join(f'-Wl,-rpath,{p}' for p in extra_rpath) + monkeypatch.setenv('LDFLAGS', ldflags) + + filename = mesonpy.build_wheel(tmp_path) + artifact = wheel.wheelfile.WheelFile(tmp_path / filename) + artifact.extractall(tmp_path) + + for path in f'_example{EXT_SUFFIX}', f'liblib{LIB_SUFFIX}', f'sub/libsublib{LIB_SUFFIX}': + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / path)) + assert extra_rpath <= rpath + + def test_sharedlib_in_package(venv, wheel_sharedlib_in_package): venv.pip('install', wheel_sharedlib_in_package) - output = venv.python('-c', 'import mypkg; print(mypkg.example_sum(2, 5))') - assert int(output) == 7 - output = venv.python('-c', 'import mypkg; print(mypkg.example_prod(6, 7))') - assert int(output) == 42 + output = venv.python('-c', 'import mypkg; print(mypkg.prodsum(2, 3, 4))') + assert int(output) == 11 -@pytest.mark.skipif(MESON_VERSION < (1, 3, 0), reason='Meson version too old') +@pytest.mark.skipif(MESON_VERSION < (1, 3, 0), reason='meson too old') def test_link_library_in_subproject(venv, wheel_link_library_in_subproject): venv.pip('install', wheel_link_library_in_subproject) output = venv.python('-c', 'import foo; print(foo.example_sum(3, 6))') @@ -194,17 +229,32 @@ def test_link_library_in_subproject(venv, wheel_link_library_in_subproject): @pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') -def test_rpath(wheel_link_against_local_lib, tmp_path): +def test_link_against_local_lib_rpath(wheel_link_against_local_lib, tmp_path): artifact = wheel.wheelfile.WheelFile(wheel_link_against_local_lib) artifact.extractall(tmp_path) origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN' expected = {f'{origin}/../.link_against_local_lib.mesonpy.libs', 'custom-rpath',} + # This RPATH entry should be removed by meson-python but it is not. + expected.add(f'{origin}/lib') + + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}')) + assert rpath == expected + + +@pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') +def test_link_against_local_lib_rpath_ldflags(package_link_against_local_lib, tmp_path, monkeypatch): + origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN' + extra_rpath = {f'{origin}/test-ldflags', '/usr/lib/test-ldflags'} + ldflags = ' '.join(f'-Wl,-rpath,{p}' for p in extra_rpath) + monkeypatch.setenv('LDFLAGS', ldflags) + + filename = mesonpy.build_wheel(tmp_path) + artifact = wheel.wheelfile.WheelFile(tmp_path / filename) + artifact.extractall(tmp_path) - rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}')) - # Verify that rpath is a superset of the expected one: linking to - # the Python runtime may require additional rpath entries. - assert rpath >= expected + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}')) + assert extra_rpath <= rpath @pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') @@ -213,7 +263,7 @@ def test_uneeded_rpath(wheel_purelib_and_platlib, tmp_path): artifact.extractall(tmp_path) origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN' - rpath = mesonpy._rpath._get_rpath(tmp_path / f'plat{EXT_SUFFIX}') + rpath = mesonpy._rpath.get_rpath(tmp_path / f'plat{EXT_SUFFIX}') for path in rpath: assert origin not in path