From f58cf99b8ad8ae0830c9c48248d26a12eeee8e4a Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 17:58:38 +0200 Subject: [PATCH 01/17] ENH: drop empty strings when listing RPATH entries for ELF binaries This entries are not meaningful. This has the additional benefit of not returning an empty string RPATH entry for binaries with RPATH unset. --- mesonpy/_rpath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesonpy/_rpath.py b/mesonpy/_rpath.py index 9953d7b5b..8bbc5bfe0 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -73,7 +73,7 @@ def fix_rpath(filepath: Path, libs_relative_path: str) -> None: 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: subprocess.run(['patchelf','--set-rpath', ':'.join(rpath), os.fspath(filepath)], check=True) From 4c62699524eecc0c6ac75944548cee058748d0ce Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Fri, 18 Sep 2026 15:15:54 +0200 Subject: [PATCH 02/17] TST: copy the shared-lib-in-package test package into -legacy version in preparation to modernize it. Keep the legacy version around to make sure that no regressions compared to the current behavior (which may be incorrect but relied upon by existing packages) are introduced while reworking the RPATH handling. --- .../sharedlib-in-package-legacy/meson.build | 9 ++++ .../mypkg/__init__.py | 51 +++++++++++++++++++ .../mypkg/_examplemod.c | 51 +++++++++++++++++++ .../mypkg/examplelib.c | 9 ++++ .../mypkg/examplelib.h | 7 +++ .../mypkg/meson.build | 41 +++++++++++++++ .../mypkg/sub/examplelib2.c | 9 ++++ .../mypkg/sub/examplelib2.h | 7 +++ .../mypkg/sub/meson.build | 16 ++++++ .../mypkg/sub/mypkg_dll.h | 23 +++++++++ .../pyproject.toml | 7 +++ tests/test_wheel.py | 8 +++ 12 files changed, 238 insertions(+) create mode 100644 tests/packages/sharedlib-in-package-legacy/meson.build create mode 100644 tests/packages/sharedlib-in-package-legacy/mypkg/__init__.py create mode 100644 tests/packages/sharedlib-in-package-legacy/mypkg/_examplemod.c create mode 100644 tests/packages/sharedlib-in-package-legacy/mypkg/examplelib.c create mode 100644 tests/packages/sharedlib-in-package-legacy/mypkg/examplelib.h create mode 100644 tests/packages/sharedlib-in-package-legacy/mypkg/meson.build create mode 100644 tests/packages/sharedlib-in-package-legacy/mypkg/sub/examplelib2.c create mode 100644 tests/packages/sharedlib-in-package-legacy/mypkg/sub/examplelib2.h create mode 100644 tests/packages/sharedlib-in-package-legacy/mypkg/sub/meson.build create mode 100644 tests/packages/sharedlib-in-package-legacy/mypkg/sub/mypkg_dll.h create mode 100644 tests/packages/sharedlib-in-package-legacy/pyproject.toml diff --git a/tests/packages/sharedlib-in-package-legacy/meson.build b/tests/packages/sharedlib-in-package-legacy/meson.build new file mode 100644 index 000000000..20554829b --- /dev/null +++ b/tests/packages/sharedlib-in-package-legacy/meson.build @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2022 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('sharedlib-in-package-legacy', 'c', version: '1.0.0') + +py = import('python').find_installation(pure: false) + +subdir('mypkg') diff --git a/tests/packages/sharedlib-in-package-legacy/mypkg/__init__.py b/tests/packages/sharedlib-in-package-legacy/mypkg/__init__.py new file mode 100644 index 000000000..857d2e906 --- /dev/null +++ b/tests/packages/sharedlib-in-package-legacy/mypkg/__init__.py @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: 2024 The meson-python developers +# +# SPDX-License-Identifier: MIT + +import os +import sys + + +# start-literalinclude +def _append_to_sharedlib_load_path(): + """Ensure the shared libraries in this package can be loaded on Windows. + + Windows lacks a concept equivalent to RPATH: Python extension modules + cannot find DLLs installed outside the DLL search path. This function + ensures that the location of the shared libraries distributed inside this + Python package is in the DLL search path of the process. + + The Windows DLL search path includes the path to the object attempting + to load the DLL: it needs to be augmented only when the Python extension + modules and the DLLs they require are installed in separate directories. + Cygwin does not have the same default library search path: all locations + where the shared libraries are installed need to be added to the search + path. + + This function is very similar to the snippet inserted into the main + ``__init__.py`` of a package by ``delvewheel`` when it vendors external + shared libraries. + + .. note:: + + `os.add_dll_directory` is only available for Python 3.8 and later, and + in the Conda ``python`` packages it works as advertised only for + version 3.10 and later. For older Python versions, pre-loading the DLLs + with `ctypes.WinDLL` may be preferred. + """ + basedir = os.path.dirname(__file__) + subdir = os.path.join(basedir, 'sub') + if os.name == 'nt': + os.add_dll_directory(subdir) + elif sys.platform == 'cygwin': + os.environ['PATH'] = os.pathsep.join((os.environ['PATH'], basedir, subdir)) + + +_append_to_sharedlib_load_path() +# end-literalinclude + + +from ._example import example_prod, example_sum #noqa: E402 + + +__all__ = ['example_prod', 'example_sum'] diff --git a/tests/packages/sharedlib-in-package-legacy/mypkg/_examplemod.c b/tests/packages/sharedlib-in-package-legacy/mypkg/_examplemod.c new file mode 100644 index 000000000..080e03c18 --- /dev/null +++ b/tests/packages/sharedlib-in-package-legacy/mypkg/_examplemod.c @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2022 The meson-python developers +// +// SPDX-License-Identifier: MIT + +#include + +#include "examplelib.h" +#include "examplelib2.h" + +static PyObject* example_sum(PyObject* self, PyObject *args) +{ + int a, b; + if (!PyArg_ParseTuple(args, "ii", &a, &b)) { + return NULL; + } + + 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)) { + return NULL; + } + + long result = prod(a, b); + + return PyLong_FromLong(result); +} + +static PyMethodDef methods[] = { + {"example_prod", (PyCFunction)example_prod, METH_VARARGS, NULL}, + {"example_sum", (PyCFunction)example_sum, METH_VARARGS, NULL}, + {NULL, NULL, 0, NULL}, +}; + +static struct PyModuleDef module = { + PyModuleDef_HEAD_INIT, + "_example", + NULL, + -1, + methods, +}; + +PyMODINIT_FUNC PyInit__example(void) +{ + return PyModule_Create(&module); +} diff --git a/tests/packages/sharedlib-in-package-legacy/mypkg/examplelib.c b/tests/packages/sharedlib-in-package-legacy/mypkg/examplelib.c new file mode 100644 index 000000000..f486bd7fb --- /dev/null +++ b/tests/packages/sharedlib-in-package-legacy/mypkg/examplelib.c @@ -0,0 +1,9 @@ +// 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-legacy/mypkg/examplelib.h b/tests/packages/sharedlib-in-package-legacy/mypkg/examplelib.h new file mode 100644 index 000000000..c09f4f785 --- /dev/null +++ b/tests/packages/sharedlib-in-package-legacy/mypkg/examplelib.h @@ -0,0 +1,7 @@ +// 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-legacy/mypkg/meson.build b/tests/packages/sharedlib-in-package-legacy/mypkg/meson.build new file mode 100644 index 000000000..75904bed6 --- /dev/null +++ b/tests/packages/sharedlib-in-package-legacy/mypkg/meson.build @@ -0,0 +1,41 @@ +# 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 + +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', + install: true, + subdir: 'mypkg', + install_rpath: '$ORIGIN', +) + +py.install_sources( + ['__init__.py'], + subdir: 'mypkg', +) diff --git a/tests/packages/sharedlib-in-package-legacy/mypkg/sub/examplelib2.c b/tests/packages/sharedlib-in-package-legacy/mypkg/sub/examplelib2.c new file mode 100644 index 000000000..12f5b87a7 --- /dev/null +++ b/tests/packages/sharedlib-in-package-legacy/mypkg/sub/examplelib2.c @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2022 The meson-python developers +// +// SPDX-License-Identifier: MIT + +#include "mypkg_dll.h" + +MYPKG_DLL int prod(int a, int b) { + return a * b; +} diff --git a/tests/packages/sharedlib-in-package-legacy/mypkg/sub/examplelib2.h b/tests/packages/sharedlib-in-package-legacy/mypkg/sub/examplelib2.h new file mode 100644 index 000000000..64b6a907e --- /dev/null +++ b/tests/packages/sharedlib-in-package-legacy/mypkg/sub/examplelib2.h @@ -0,0 +1,7 @@ +// 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-legacy/mypkg/sub/meson.build b/tests/packages/sharedlib-in-package-legacy/mypkg/sub/meson.build new file mode 100644 index 000000000..7a1978d4a --- /dev/null +++ b/tests/packages/sharedlib-in-package-legacy/mypkg/sub/meson.build @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2022 The meson-python developers +# +# SPDX-License-Identifier: MIT + +example_lib2 = shared_library( + 'examplelib2', + 'examplelib2.c', + c_args: export_dll_args, + install: true, + install_dir: py.get_install_dir() / 'mypkg/sub', +) + +example_lib2_dep = declare_dependency( + compile_args: import_dll_args, + link_with: example_lib2, +) diff --git a/tests/packages/sharedlib-in-package-legacy/mypkg/sub/mypkg_dll.h b/tests/packages/sharedlib-in-package-legacy/mypkg/sub/mypkg_dll.h new file mode 100644 index 000000000..8460e6c7d --- /dev/null +++ b/tests/packages/sharedlib-in-package-legacy/mypkg/sub/mypkg_dll.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2022 The meson-python developers +// +// SPDX-License-Identifier: MIT + +#pragma once + +// MYPKG_DLL +// inspired by https://github.com/abseil/abseil-cpp/blob/20240116.2/absl/base/config.h#L736-L753 +// and https://github.com/scipy/scipy/blob/9ded83b51099eee745418ccbb30196db96c81f3f/scipy/_build_utils/src/scipy_dll.h +// +// When building the `examplelib` DLL, this macro expands to `__declspec(dllexport)` +// so we can annotate symbols appropriately as being exported. When used in +// headers consuming a DLL, this macro expands to `__declspec(dllimport)` so +// that consumers know the symbol is defined inside the DLL. In all other cases, +// the macro expands to nothing. +// Note: MYPKG_DLL_{EX,IM}PORTS are set in mypkg/meson.build +#if defined(MYPKG_DLL_EXPORTS) + #define MYPKG_DLL __declspec(dllexport) +#elif defined(MYPKG_DLL_IMPORTS) + #define MYPKG_DLL __declspec(dllimport) +#else + #define MYPKG_DLL +#endif diff --git a/tests/packages/sharedlib-in-package-legacy/pyproject.toml b/tests/packages/sharedlib-in-package-legacy/pyproject.toml new file mode 100644 index 000000000..2542e4395 --- /dev/null +++ b/tests/packages/sharedlib-in-package-legacy/pyproject.toml @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2022 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 3fb3b4295..9447dc521 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -168,6 +168,14 @@ def test_local_lib(venv, wheel_link_against_local_lib): assert int(output) == 3 +def test_sharedlib_in_package_legacy(venv, wheel_sharedlib_in_package_legacy): + venv.pip('install', wheel_sharedlib_in_package_legacy) + 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 + + 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))') From 2ec7957080cd579a0ff4912551ee0f854a0bd351 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sat, 15 Feb 2025 12:03:21 +0100 Subject: [PATCH 03/17] TST: rework the sharedlib-in-package test package This reorganizes the test package to a flatter layout that helps visualizing all the parts involved in the test and introduces an asymmetry between the source layout and the installation layout that demonstrates the bugs in RPATH handling as currently implemented. --- .../packages/sharedlib-in-package/meson.build | 1 + .../sharedlib-in-package/mypkg/__init__.py | 4 +- .../sharedlib-in-package/mypkg/_examplemod.c | 25 +++--------- .../sharedlib-in-package/mypkg/examplelib.h | 7 ---- .../sharedlib-in-package/mypkg/meson.build | 26 +----------- .../mypkg/sub/examplelib2.h | 7 ---- .../{mypkg/examplelib.c => src/lib.c} | 7 ++-- tests/packages/sharedlib-in-package/src/lib.h | 13 ++++++ .../sharedlib-in-package/src/meson.build | 40 +++++++++++++++++++ .../{mypkg/sub/examplelib2.c => src/sublib.c} | 4 +- .../sharedlib-in-package/src/sublib.h | 13 ++++++ tests/test_wheel.py | 6 +-- 12 files changed, 84 insertions(+), 69 deletions(-) delete mode 100644 tests/packages/sharedlib-in-package/mypkg/examplelib.h delete mode 100644 tests/packages/sharedlib-in-package/mypkg/sub/examplelib2.h rename tests/packages/sharedlib-in-package/{mypkg/examplelib.c => src/lib.c} (50%) create mode 100644 tests/packages/sharedlib-in-package/src/lib.h create mode 100644 tests/packages/sharedlib-in-package/src/meson.build rename tests/packages/sharedlib-in-package/{mypkg/sub/examplelib2.c => src/sublib.c} (66%) create mode 100644 tests/packages/sharedlib-in-package/src/sublib.h diff --git a/tests/packages/sharedlib-in-package/meson.build b/tests/packages/sharedlib-in-package/meson.build index 71921cfea..e8f80243c 100644 --- a/tests/packages/sharedlib-in-package/meson.build +++ b/tests/packages/sharedlib-in-package/meson.build @@ -6,4 +6,5 @@ project('sharedlib-in-package', 'c', version: '1.0.0') py = import('python').find_installation(pure: false) +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 857d2e906..e4fbc2a9b 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 080e03c18..b4cc3f0c2 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.h b/tests/packages/sharedlib-in-package/mypkg/examplelib.h deleted file mode 100644 index c09f4f785..000000000 --- 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 75904bed6..5cad96363 100644 --- a/tests/packages/sharedlib-in-package/mypkg/meson.build +++ b/tests/packages/sharedlib-in-package/mypkg/meson.build @@ -2,34 +2,10 @@ # # 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', 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 64b6a907e..000000000 --- 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/mypkg/examplelib.c b/tests/packages/sharedlib-in-package/src/lib.c similarity index 50% rename from tests/packages/sharedlib-in-package/mypkg/examplelib.c rename to tests/packages/sharedlib-in-package/src/lib.c index f486bd7fb..0ec14522f 100644 --- a/tests/packages/sharedlib-in-package/mypkg/examplelib.c +++ b/tests/packages/sharedlib-in-package/src/lib.c @@ -2,8 +2,9 @@ // // SPDX-License-Identifier: MIT -#include "sub/mypkg_dll.h" +#include "lib.h" +#include "sublib.h" -MYPKG_DLL int sum(int a, int b) { - return a + b; +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 000000000..fb6a02d8a --- /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 000000000..5097a76e9 --- /dev/null +++ b/tests/packages/sharedlib-in-package/src/meson.build @@ -0,0 +1,40 @@ +# 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: '$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 12f5b87a7..facfdf2e1 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 000000000..9fc7ae51d --- /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_wheel.py b/tests/test_wheel.py index 9447dc521..04247d1ef 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -178,10 +178,8 @@ def test_sharedlib_in_package_legacy(venv, wheel_sharedlib_in_package_legacy): 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 too old') From f005ed6957525020b62d702946268343f3715a27 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 14:39:27 +0200 Subject: [PATCH 04/17] TST: use linker flags instead of install_rpath to set the RPATH meson-python does not support `install_rpath` yet and Meson does not expose it in introspection data prior to version 1.6.0. Use `link_args` to set the RPATH. --- tests/conftest.py | 8 ++++---- tests/packages/sharedlib-in-package/mypkg/meson.build | 6 +++++- tests/packages/sharedlib-in-package/src/meson.build | 6 +++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index be9b52b6a..f70708b47 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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: diff --git a/tests/packages/sharedlib-in-package/mypkg/meson.build b/tests/packages/sharedlib-in-package/mypkg/meson.build index 5cad96363..ff1d85a05 100644 --- a/tests/packages/sharedlib-in-package/mypkg/meson.build +++ b/tests/packages/sharedlib-in-package/mypkg/meson.build @@ -8,7 +8,11 @@ py.extension_module( 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: '$ORIGIN', + link_args: '-Wl,-rpath,$ORIGIN', ) py.install_sources( diff --git a/tests/packages/sharedlib-in-package/src/meson.build b/tests/packages/sharedlib-in-package/src/meson.build index 5097a76e9..d27a2d396 100644 --- a/tests/packages/sharedlib-in-package/src/meson.build +++ b/tests/packages/sharedlib-in-package/src/meson.build @@ -30,7 +30,11 @@ lib = shared_library( c_args: export_dll_args, install: true, install_dir: py.get_install_dir() / 'mypkg', - install_rpath: '$ORIGIN/sub', + # 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: '$ORIGIN/sub', + link_args: '-Wl,-rpath,$ORIGIN/sub', ) lib_dep = declare_dependency( From 59ff4c2f81b0e3bfb3e78896f392f0052c0a90dd Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 14:45:25 +0200 Subject: [PATCH 05/17] TST: use platform specific anchors in RPATH entries macOS requires using `@loader_path` in place of `$ORIGIN`. --- tests/packages/sharedlib-in-package/meson.build | 2 ++ tests/packages/sharedlib-in-package/mypkg/meson.build | 4 ++-- tests/packages/sharedlib-in-package/src/meson.build | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/packages/sharedlib-in-package/meson.build b/tests/packages/sharedlib-in-package/meson.build index e8f80243c..d311a58a7 100644 --- a/tests/packages/sharedlib-in-package/meson.build +++ b/tests/packages/sharedlib-in-package/meson.build @@ -6,5 +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/meson.build b/tests/packages/sharedlib-in-package/mypkg/meson.build index ff1d85a05..cebf2d523 100644 --- a/tests/packages/sharedlib-in-package/mypkg/meson.build +++ b/tests/packages/sharedlib-in-package/mypkg/meson.build @@ -11,8 +11,8 @@ py.extension_module( # 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: '$ORIGIN', - link_args: '-Wl,-rpath,$ORIGIN', + # install_rpath: f'@origin@', + link_args: f'-Wl,-rpath,@origin@', ) py.install_sources( diff --git a/tests/packages/sharedlib-in-package/src/meson.build b/tests/packages/sharedlib-in-package/src/meson.build index d27a2d396..b673ef29e 100644 --- a/tests/packages/sharedlib-in-package/src/meson.build +++ b/tests/packages/sharedlib-in-package/src/meson.build @@ -33,8 +33,8 @@ lib = shared_library( # 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: '$ORIGIN/sub', - link_args: '-Wl,-rpath,$ORIGIN/sub', + # install_rpath: f'@origin@/sub', + link_args: f'-Wl,-rpath,@origin@/sub', ) lib_dep = declare_dependency( From 1e1d1bc0ff04c6fb9a9094356d832501cffc64db Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 16:42:59 +0200 Subject: [PATCH 06/17] TST: add another RPATH handling verification test This shows that build RPATHs are not correctly stripped. --- tests/test_wheel.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 04247d1ef..41d78f9f6 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -176,6 +176,25 @@ def test_sharedlib_in_package_legacy(venv, wheel_sharedlib_in_package_legacy): assert int(output) == 42 +@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}')) + # FIXME: RPATH entries added by Meson to point to the build directory are not removed. + assert rpath >= {origin} + + rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / f'liblib{LIB_SUFFIX}')) + # FIXME: RPATH entries added by Meson to point to the build directory are not removed. + assert rpath >= {f'{origin}/sub'} + + rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / 'sub' / f'libsublib{LIB_SUFFIX}')) + assert rpath == set() + + 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.prodsum(2, 3, 4))') @@ -190,7 +209,7 @@ 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) From 58aa44a86187c0e12b6d99c432a87f3a7f8f345a Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 17:25:38 +0200 Subject: [PATCH 07/17] TST: test RPATH entries added via flags in $LDFLAGS environment variable --- tests/test_wheel.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 41d78f9f6..4890eeed8 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -195,6 +195,22 @@ def test_sharedlib_in_package_rpath(wheel_sharedlib_in_package, tmp_path): 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.prodsum(2, 3, 4))') @@ -222,6 +238,21 @@ def test_link_against_local_lib_rpath(wheel_link_against_local_lib, tmp_path): 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}')) + assert extra_rpath <= rpath + + @pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') def test_uneeded_rpath(wheel_purelib_and_platlib, tmp_path): artifact = wheel.wheelfile.WheelFile(wheel_purelib_and_platlib) From 5098cd44477530c4223799a0f0639fee2fc169f3 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 10 Aug 2025 13:54:53 +0200 Subject: [PATCH 08/17] MAINT: refactor RPATH handling code and add tests Update typing annotation while there. This does not introduce any functional changes, except removing duplicates entries from RPATH. Fixes #813. --- mesonpy/_rpath.py | 123 +++++++++++++++++++++++++++++++------------- tests/test_rpath.py | 39 ++++++++++++++ tests/test_wheel.py | 14 ++--- 3 files changed, 132 insertions(+), 44 deletions(-) create mode 100644 tests/test_rpath.py diff --git a/mesonpy/_rpath.py b/mesonpy/_rpath.py index 8bbc5bfe0..7bd043bbd 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -11,18 +11,52 @@ if typing.TYPE_CHECKING: - from typing import Iterable, List, Union - Path = Union[str, os.PathLike[str]] + Path = str | os.PathLike[str] + T = typing.TypeVar('T') -if sys.platform == 'win32' or sys.platform == 'cygwin': +def unique(values: list[T]) -> list[T]: + return list(dict.fromkeys(values)) + + +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 = [] + for path in old_rpath: + if path.startswith(cls.origin): + path = os.path.join(cls.origin, libs_relative_path) + new_rpath.append(path) + new_rpath = unique(new_rpath) + if new_rpath != old_rpath: + cls.set_rpath(filepath, old_rpath, new_rpath) + - def fix_rpath(filepath: Path, libs_relative_path: str) -> None: +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 @@ -34,17 +68,35 @@ 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: + # ``install_name_tool`` allows to delete, add, or rewrite specific + # LC_RPATH entries, however, this operations cannot be combined in + # arbitrary order in a single call. The only robust way to get + # entries in a specific order with at max two tool invocations is to + # delete the entries that are out of place and re-add them in the + # right order. - 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) + keep = 0 + while keep < len(old) and keep < len(rpath) and old[keep] == rpath[keep]: + keep += 1 -elif sys.platform == 'sunos5': + delete = old[keep:] + add = rpath[keep:] + + if delete: + args = [a for p in delete for a in ('-delete_rpath', p)] + subprocess.run(['install_name_tool', *args, os.fspath(filepath)], check=True) - def _get_rpath(filepath: Path) -> List[str]: + if add: + args = [a for p in add for a in ('-add_rpath', p)] + subprocess.run(['install_name_tool', *args, os.fspath(filepath)], check=True) + + +class _SunOS5(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) @@ -55,35 +107,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 [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/test_rpath.py b/tests/test_rpath.py new file mode 100644 index 000000000..982934686 --- /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 rpath == new_rpath + + new_rpath = ['one', 'three', 'two'] + set_rpath(obj, rpath, new_rpath) + rpath = get_rpath(obj) + assert rpath == new_rpath + + new_rpath = ['one'] + set_rpath(obj, rpath, new_rpath) + rpath = get_rpath(obj) + assert rpath == new_rpath diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 4890eeed8..da8368aed 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -183,15 +183,15 @@ def test_sharedlib_in_package_rpath(wheel_sharedlib_in_package, tmp_path): origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN' - rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / f'_example{EXT_SUFFIX}')) + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / f'_example{EXT_SUFFIX}')) # FIXME: RPATH entries added by Meson to point to the build directory are not removed. assert rpath >= {origin} - rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / f'liblib{LIB_SUFFIX}')) + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / f'liblib{LIB_SUFFIX}')) # FIXME: RPATH entries added by Meson to point to the build directory are not removed. assert rpath >= {f'{origin}/sub'} - rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / 'sub' / f'libsublib{LIB_SUFFIX}')) + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / 'sub' / f'libsublib{LIB_SUFFIX}')) assert rpath == set() @@ -207,7 +207,7 @@ def test_sharedlib_in_package_rpath_ldflags(package_sharedlib_in_package, tmp_pa 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)) + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / path)) assert extra_rpath <= rpath @@ -232,7 +232,7 @@ def test_link_against_local_lib_rpath(wheel_link_against_local_lib, tmp_path): origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN' expected = {f'{origin}/../.link_against_local_lib.mesonpy.libs', 'custom-rpath',} - rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}')) + 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 @@ -249,7 +249,7 @@ def test_link_against_local_lib_rpath_ldflags(package_link_against_local_lib, tm artifact = wheel.wheelfile.WheelFile(tmp_path / filename) artifact.extractall(tmp_path) - rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}')) + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}')) assert extra_rpath <= rpath @@ -259,7 +259,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 From 2e1893f7321059890cae1ff7df32d5196b7f748e Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 20 Sep 2026 21:43:11 +0200 Subject: [PATCH 09/17] BUG: correctly read RPATH entries containing spaces on macOS --- mesonpy/_rpath.py | 15 ++++++++------- tests/test_rpath.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/mesonpy/_rpath.py b/mesonpy/_rpath.py index 7bd043bbd..7cabe71eb 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -5,6 +5,7 @@ from __future__ import annotations import os +import re import subprocess import sys import typing @@ -59,13 +60,13 @@ class _MacOS(RPATH): def get_rpath(filepath: Path) -> list[str]: rpath = [] r = subprocess.run(['otool', '-l', os.fspath(filepath)], capture_output=True, text=True) - rpath_tag = False - for line in [x.split() for x in r.stdout.split('\n')]: - if line == ['cmd', 'LC_RPATH']: - rpath_tag = True - elif len(line) >= 2 and line[0] == 'path' and rpath_tag: - rpath.append(line[1]) - rpath_tag = False + lines = iter(r.stdout.splitlines()) + for line in lines: + if line.strip().startswith('cmd LC_RPATH'): + for line in lines: + if m := re.match(r'^\s*path (.+) \(offset \d+\)$', line): + rpath.append(m.group(1)) + break return rpath @staticmethod diff --git a/tests/test_rpath.py b/tests/test_rpath.py index 982934686..d450ea401 100644 --- a/tests/test_rpath.py +++ b/tests/test_rpath.py @@ -37,3 +37,19 @@ def test_rpath_get_set(wheel_sharedlib_in_package, tmp_path): set_rpath(obj, rpath, new_rpath) rpath = get_rpath(obj) assert rpath == new_rpath + + new_rpath = [ + 'with two or three spaces', + 'trailing space ', + '/some funny absolute path/(library)', + '@loader_path/Mach-O relative', + '$ORIGIN/ELF relative', + ] + set_rpath(obj, rpath, new_rpath) + rpath = get_rpath(obj) + assert rpath == new_rpath + + new_rpath = ['alpha', 'beta'] + set_rpath(obj, rpath, new_rpath) + rpath = get_rpath(obj) + assert rpath == new_rpath From d48d0a07ec9acfed34fd1ce7355e31248b533f20 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 10 Aug 2025 16:14:03 +0200 Subject: [PATCH 10/17] MAINT: move checking shared libs on Windows There is no need to perform the check for every native file installed. --- mesonpy/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 51ba001ba..874744bed 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -446,12 +446,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, 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 @@ -488,6 +482,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) From 25394589e2b8d3ec0780d39908873ed1b3e8ecb9 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 10 Aug 2025 17:40:17 +0200 Subject: [PATCH 11/17] BUG: do not remove RPATH entries relative to $ORIGIN for packages using internal shared libraries relocated by meson-python. Fixes #711. --- mesonpy/__init__.py | 6 ------ mesonpy/_rpath.py | 20 +++++++++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 874744bed..7f1b71818 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -446,12 +446,6 @@ def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path, if self._has_internal_libs: if _is_native(origin): - # 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 - # 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) diff --git a/mesonpy/_rpath.py b/mesonpy/_rpath.py index 7cabe71eb..7a51318d3 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -35,11 +35,21 @@ def set_rpath(filepath: Path, old: list[str], rpath: list[str]) -> None: @classmethod def fix_rpath(cls, filepath: Path, libs_relative_path: str) -> None: old_rpath = cls.get_rpath(filepath) - new_rpath = [] - for path in old_rpath: - if path.startswith(cls.origin): - path = os.path.join(cls.origin, libs_relative_path) - new_rpath.append(path) + new_rpath = old_rpath[:] + + # When an executable, library, 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) From edffe3038a85c3241e8211378cfb1e68ab93c68b Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sat, 18 Oct 2025 19:05:17 +0200 Subject: [PATCH 12/17] ENH: implement support for install_rpath Revise tests to exercise support when executed with Meson > 1.6 --- docs/reference/meson-compatibility.rst | 5 +++++ mesonpy/__init__.py | 21 ++++++++++++------- mesonpy/_rpath.py | 10 +++++---- .../sharedlib-in-package/mypkg/meson.build | 12 ++++++----- .../sharedlib-in-package/src/meson.build | 12 ++++++----- 5 files changed, 38 insertions(+), 22 deletions(-) diff --git a/docs/reference/meson-compatibility.rst b/docs/reference/meson-compatibility.rst index 73258e73a..d3edc8db4 100644 --- a/docs/reference/meson-compatibility.rst +++ b/docs/reference/meson-compatibility.rst @@ -52,6 +52,11 @@ 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()``. 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. diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 7f1b71818..769459c7b 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -127,6 +127,7 @@ def _compile_patterns(patterns: List[str]) -> Callable[[str], bool]: class _Entry(typing.NamedTuple): dst: pathlib.Path src: str + install_rpath: List[str] = [] def _map_to_wheel(sources: Dict[str, Dict[str, Any]], @@ -183,7 +184,9 @@ def _map_to_wheel(sources: Dict[str, Dict[str, Any]], filedst = dst / relpath wheel_files[path].append(_Entry(filedst, filesrc)) else: - wheel_files[path].append(_Entry(dst, src)) + rpath = target.get('install_rpath') + install_rpath = rpath.split(':') if rpath else [] + wheel_files[path].append(_Entry(dst, src, install_rpath)) return wheel_files @@ -441,13 +444,15 @@ 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]) -> None: """Add a file to the wheel.""" - if self._has_internal_libs: - if _is_native(origin): - 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, libspath) + elif install_rpath: + mesonpy._rpath.fix_rpath(origin, install_rpath, None) try: wheel_file.write(origin, destination.as_posix()) @@ -491,7 +496,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 in entries: counter.update(src) if path == root: @@ -502,7 +507,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) return wheel_file diff --git a/mesonpy/_rpath.py b/mesonpy/_rpath.py index 7a51318d3..0f3253a01 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -33,9 +33,11 @@ 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: + def fix_rpath(cls, filepath: Path, install_rpath: list[str], libs_relative_path: str | None) -> None: old_rpath = cls.get_rpath(filepath) - new_rpath = old_rpath[:] + + # Prepend install_rpath entries. + new_rpath = install_rpath + old_rpath # When an executable, library, or Python extension module is # dynamically linked to a library built as part of the project, Meson @@ -47,7 +49,7 @@ def fix_rpath(cls, filepath: Path, libs_relative_path: str) -> None: # 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): + 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)) new_rpath = unique(new_rpath) @@ -58,7 +60,7 @@ def fix_rpath(cls, filepath: Path, libs_relative_path: str) -> None: class _Windows(RPATH): @classmethod - def fix_rpath(cls, filepath: Path, libs_relative_path: str) -> None: + def fix_rpath(cls, filepath: Path, install_rpath: list[str], libs_relative_path: str) -> None: pass diff --git a/tests/packages/sharedlib-in-package/mypkg/meson.build b/tests/packages/sharedlib-in-package/mypkg/meson.build index cebf2d523..915d7c9cd 100644 --- a/tests/packages/sharedlib-in-package/mypkg/meson.build +++ b/tests/packages/sharedlib-in-package/mypkg/meson.build @@ -2,17 +2,19 @@ # # SPDX-License-Identifier: MIT +# 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 when +# using older Meson. +kwargs = meson.version().version_compare('< 1.6') ? {'link_args': f'-Wl,-rpath,@origin@'} : {'install_rpath': f'@origin@'} + py.extension_module( '_example', '_examplemod.c', dependencies: lib_dep, install: true, subdir: '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@', - link_args: f'-Wl,-rpath,@origin@', + kwargs: kwargs, ) py.install_sources( diff --git a/tests/packages/sharedlib-in-package/src/meson.build b/tests/packages/sharedlib-in-package/src/meson.build index b673ef29e..78ae3c9a2 100644 --- a/tests/packages/sharedlib-in-package/src/meson.build +++ b/tests/packages/sharedlib-in-package/src/meson.build @@ -23,6 +23,12 @@ sublib_dep = declare_dependency( link_with: sublib, ) +# 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 when +# using older Meson. +kwargs = meson.version().version_compare('< 1.6') ? {'link_args': f'-Wl,-rpath,@origin@/sub'} : {'install_rpath': f'@origin@/sub'} + lib = shared_library( 'lib', 'lib.c', @@ -30,11 +36,7 @@ lib = shared_library( 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', + kwargs: kwargs, ) lib_dep = declare_dependency( From 60a713bb9387d09aa9a3dbaeee8f7a67c73182f7 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sat, 27 Jun 2026 22:57:02 +0200 Subject: [PATCH 13/17] ENH: add support for removing build time RPATH entries Requires Meson 1.9.0. Extend some tests to strictly check that only the expected RPATH entries remain. This is complicated by the need to account for additional RPATH entries required by the Python runtime. --- docs/reference/meson-compatibility.rst | 5 +++ mesonpy/__init__.py | 19 ++++---- mesonpy/_rpath.py | 16 ++++--- tests/test_wheel.py | 62 +++++++++++++++++++------- 4 files changed, 72 insertions(+), 30 deletions(-) diff --git a/docs/reference/meson-compatibility.rst b/docs/reference/meson-compatibility.rst index d3edc8db4..a342f4ec3 100644 --- a/docs/reference/meson-compatibility.rst +++ b/docs/reference/meson-compatibility.rst @@ -61,6 +61,11 @@ versions. 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 executing 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. + 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 769459c7b..70cc0f412 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -128,6 +128,7 @@ class _Entry(typing.NamedTuple): dst: pathlib.Path src: str install_rpath: List[str] = [] + build_rpath: List[str] = [] def _map_to_wheel(sources: Dict[str, Dict[str, Any]], @@ -186,7 +187,8 @@ def _map_to_wheel(sources: Dict[str, Dict[str, Any]], else: rpath = target.get('install_rpath') install_rpath = rpath.split(':') if rpath else [] - wheel_files[path].append(_Entry(dst, src, install_rpath)) + build_rpath = target.get('build_rpaths') or [] + wheel_files[path].append(_Entry(dst, src, install_rpath, build_rpath)) return wheel_files @@ -444,15 +446,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, - install_rpath: List[str]) -> 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 and _is_native(origin): libspath = os.path.relpath(self._libs_dir, destination.parent) - mesonpy._rpath.fix_rpath(origin, install_rpath, libspath) - elif install_rpath: - mesonpy._rpath.fix_rpath(origin, install_rpath, None) + 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()) @@ -496,7 +499,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, install_rpath in entries: + for dst, src, install_rpath, build_rpath in entries: counter.update(src) if path == root: @@ -507,7 +510,7 @@ def build(self, directory: Path) -> pathlib.Path: else: dst = pathlib.Path(self._data_dir, path, dst) - self._install_path(whl, src, dst, install_rpath) + self._install_path(whl, src, dst, install_rpath, build_rpath) return wheel_file diff --git a/mesonpy/_rpath.py b/mesonpy/_rpath.py index 0f3253a01..662b99b8a 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -33,11 +33,17 @@ def set_rpath(filepath: Path, old: list[str], rpath: list[str]) -> None: raise NotImplementedError @classmethod - def fix_rpath(cls, filepath: Path, install_rpath: list[str], libs_relative_path: str | None) -> None: + def fix_rpath(cls, filepath: Path, install_rpath: list[str], build_rpath: list[str], libs_path: str | None) -> None: old_rpath = cls.get_rpath(filepath) # Prepend install_rpath entries. - new_rpath = install_rpath + old_rpath + new_rpath = install_rpath + + # Merge with existing entries, excluding build_rpath entries. Meson + # adds a padding entry to RPATH composed of enough ``X`` characters + # to reserve enough space in the ELF header to hold the the final + # installation RPATH. Remove this entry and empty entries too. + new_rpath += [path for path in old_rpath if path.strip('X') and path not in build_rpath] # When an executable, library, or Python extension module is # dynamically linked to a library built as part of the project, Meson @@ -49,8 +55,8 @@ def fix_rpath(cls, filepath: Path, install_rpath: list[str], libs_relative_path: # 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)) + if libs_path and any(path.startswith(cls.origin) for path in old_rpath): + new_rpath.append(os.path.join(cls.origin, libs_path)) new_rpath = unique(new_rpath) if new_rpath != old_rpath: @@ -60,7 +66,7 @@ def fix_rpath(cls, filepath: Path, install_rpath: list[str], libs_relative_path: class _Windows(RPATH): @classmethod - def fix_rpath(cls, filepath: Path, install_rpath: list[str], libs_relative_path: str) -> None: + def fix_rpath(cls, filepath: Path, install_rpath: list[str], build_rpath: list[str], libs_path: str | None) -> None: pass diff --git a/tests/test_wheel.py b/tests/test_wheel.py index da8368aed..8192d01cb 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -33,6 +33,9 @@ INTERPRETER = tag.interpreter PLATFORM = adjust_packaging_platform_tag(tag.platform) +# Support for removing build RPATH entries requires Meson 1.9 or later. +BUILD_RPATH_SUPPORT = MESON_VERSION >= (1, 9) + def wheel_contents(artifact): # Sometimes directories have entries, sometimes not, so we filter them out. @@ -177,22 +180,35 @@ def test_sharedlib_in_package_legacy(venv, wheel_sharedlib_in_package_legacy): @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) +def test_sharedlib_in_package_rpath(package_sharedlib_in_package, tmp_path): + filename = mesonpy.build_wheel(tmp_path) + artifact = wheel.wheelfile.WheelFile(tmp_path / filename) 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}')) - # FIXME: RPATH entries added by Meson to point to the build directory are not removed. - assert rpath >= {origin} + def check(rpath, *expected): + entries = [] + for path in rpath: + if path.startswith(origin): + entries.append(path) + continue + # There may be RPATH entries added by the build environment. + # However, these should be absolute paths and not point into + # the source or build directory. + assert not os.path.commonpath((path, str(package_sharedlib_in_package))) == str(package_sharedlib_in_package) + assert not os.path.commonpath((path, str(tmp_path))) == str(tmp_path) + + assert entries == list(expected) or not BUILD_RPATH_SUPPORT and set(entries) >= set(expected) - rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / f'liblib{LIB_SUFFIX}')) - # FIXME: RPATH entries added by Meson to point to the build directory are not removed. - assert rpath >= {f'{origin}/sub'} + rpath = mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / f'_example{EXT_SUFFIX}') + check(rpath, origin) - rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / 'sub' / f'libsublib{LIB_SUFFIX}')) - assert rpath == set() + rpath = mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / f'liblib{LIB_SUFFIX}') + check(rpath, f'{origin}/sub') + + rpath = mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / 'sub' / f'libsublib{LIB_SUFFIX}') + check(rpath) @pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') @@ -225,17 +241,29 @@ 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_link_against_local_lib_rpath(wheel_link_against_local_lib, tmp_path): - artifact = wheel.wheelfile.WheelFile(wheel_link_against_local_lib) +def test_link_against_local_lib_rpath(package_link_against_local_lib, tmp_path): + filename = mesonpy.build_wheel(tmp_path) + artifact = wheel.wheelfile.WheelFile(tmp_path / filename) artifact.extractall(tmp_path) origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN' - expected = {f'{origin}/../.link_against_local_lib.mesonpy.libs', 'custom-rpath',} - 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 + def check(rpath, *expected): + entries = [] + for path in rpath: + if path.startswith(origin) or path in expected: + entries.append(path) + continue + # There may be RPATH entries added by the build environment. + # However, these should be absolute paths and not point into + # the source or build directory. + assert not os.path.commonpath((path, str(package_link_against_local_lib))) == str(package_link_against_local_lib) + assert not os.path.commonpath((path, str(tmp_path))) == str(tmp_path) + + assert entries == list(expected) or not BUILD_RPATH_SUPPORT and set(entries) >= set(expected) + + rpath = mesonpy._rpath.get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}') + check(rpath, 'custom-rpath', f'{origin}/../.link_against_local_lib.mesonpy.libs') @pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') From 9c5891aead9556ae61031e260741e49d764d6529 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Fri, 18 Sep 2026 15:23:49 +0200 Subject: [PATCH 14/17] ENH: on macOS translate $ORIGIN to @loader_path in install_rpath Emit a warning when this is done. This is required to keep some backward compatibility with packages that relied on the incomplete RPATH handling behavior before #788 to work. --- mesonpy/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 70cc0f412..a397db8a1 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -124,6 +124,17 @@ def _compile_patterns(patterns: List[str]) -> Callable[[str], bool]: return typing.cast('Callable[[str], bool]', func) +def _translate_rpath(path: str, dst: pathlib.Path) -> str: + root, sep, stem = path.partition('/') + if root == '$ORIGIN': + translated = f'@loader_path{sep}{stem}' + warnings.warn( + f'translated "install_rpath" argument for {str(dst)!r} ' + f'from {path!r} to {translated!r}', stacklevel=2) + path = translated + return path + + class _Entry(typing.NamedTuple): dst: pathlib.Path src: str @@ -188,6 +199,13 @@ def _map_to_wheel(sources: Dict[str, Dict[str, Any]], rpath = target.get('install_rpath') install_rpath = rpath.split(':') if rpath else [] build_rpath = target.get('build_rpaths') or [] + + # Translate ``$ORIGIN`` to ``@loader_path`` in ``install_rpath`` + # arguments on macOS. This is better done here to be able to + # emit a meaningful warning. + if sys.platform == 'darwin': + install_rpath = [_translate_rpath(path, dst) for path in install_rpath] + wheel_files[path].append(_Entry(dst, src, install_rpath, build_rpath)) return wheel_files From 07eca0db75b28d9648db8e5e7f9208c139b45f28 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Fri, 18 Sep 2026 15:29:50 +0200 Subject: [PATCH 15/17] TST: fix sharedlib-in-package-legacy The tests package builds an extension module that links with two libraries, one installed alongside the extension module, and another installed in a sub-directory. The location of both libraries needs to be added to the RPATH. The test requires install_rpath support and thus Meson version 1.6 or later for install_rpath to be recorded in the metadata. Ignore the warning emitted building the package on macOS due to the '$ORIGIN' to '@loader_path' translation. --- tests/packages/sharedlib-in-package-legacy/mypkg/meson.build | 2 +- tests/test_wheel.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/packages/sharedlib-in-package-legacy/mypkg/meson.build b/tests/packages/sharedlib-in-package-legacy/mypkg/meson.build index 75904bed6..8d66edfbf 100644 --- a/tests/packages/sharedlib-in-package-legacy/mypkg/meson.build +++ b/tests/packages/sharedlib-in-package-legacy/mypkg/meson.build @@ -32,7 +32,7 @@ py.extension_module( include_directories: 'sub', install: true, subdir: 'mypkg', - install_rpath: '$ORIGIN', + install_rpath: '$ORIGIN:$ORIGIN/sub', ) py.install_sources( diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 8192d01cb..2709f4df9 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -171,6 +171,8 @@ def test_local_lib(venv, wheel_link_against_local_lib): assert int(output) == 3 +@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old') +@pytest.mark.filterwarnings('ignore:translated "install_rpath" argument for ') def test_sharedlib_in_package_legacy(venv, wheel_sharedlib_in_package_legacy): venv.pip('install', wheel_sharedlib_in_package_legacy) output = venv.python('-c', 'import mypkg; print(mypkg.example_sum(2, 5))') From 985d88df622eec28c54231a5967492d2820cf8ef Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Fri, 18 Sep 2026 23:19:02 +0200 Subject: [PATCH 16/17] DOC: mention that macOS uses @loader_path instead of $ORIGIN in RPATH Warn that the translation of $ORIGIN into @loader_path on macOS is provided only for backward compatibility and it is discouraged to rely on it. --- docs/how-to-guides/shared-libraries.rst | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/how-to-guides/shared-libraries.rst b/docs/how-to-guides/shared-libraries.rst index 16ab3ac69..d17572dab 100644 --- a/docs/how-to-guides/shared-libraries.rst +++ b/docs/how-to-guides/shared-libraries.rst @@ -76,8 +76,9 @@ the Python package itself: install_dir: py.get_install_dir() / 'mypkg/subdir', ) -Then an extension module in the same install directory can link against the -shared library in a portable manner by using ``install_rpath``: +On Linux, macOS, and in general on all systems except Windows, an extension +module can dynamically link against a shared library installed in the same +package by using ``install_rpath`` with a relative path: .. code-block:: meson @@ -86,12 +87,17 @@ shared library in a portable manner by using ``install_rpath``: link_with: example_lib, install: true, subdir: 'mypkg/subdir', - install_rpath: '$ORIGIN' + install_rpath: host_machine.system() == 'darwin' ? '@loader_path' : '$ORIGIN' ) -The above method will work as advertised on macOS and Linux; ``meson-python`` does -nothing special for this case. Windows needs some special handling though, due to -the lack of RPATH support: +Note that Mach-O binaries on macOS use the ``@loader_path`` token to indicate +the location of the current executable or shared library while ELF binaries use +the ``$ORIGIN`` token. For backward compatibility, ``meson-python`` translates +the more common ``$ORIGIN`` token to the macOS-specific ``@loader_path`` token +when building on macOS. However, Meson itself does not, thus relying on this +translation is discouraged. + +Windows needs some special handling though, due to the lack of RPATH support: .. literalinclude:: ../../tests/packages/sharedlib-in-package/mypkg/__init__.py :start-after: start-literalinclude From 4c22ef13f10799e11f4d121098837d1533383645 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Sat, 19 Sep 2026 16:51:21 +0000 Subject: [PATCH 17/17] TST: check installation RPATH precedence with competing shared libraries --- tests/conftest.py | 1 + .../packages/same-name-sharedlibs/meson.build | 9 +++++++ .../same-name-sharedlibs/pyproject.toml | 7 ++++++ .../same_name/__init__.py | 3 +++ .../same_name/competing/choice.c | 7 ++++++ .../same_name/competing/meson.build | 9 +++++++ .../same_name/meson.build | 23 ++++++++++++++++++ .../same-name-sharedlibs/same_name/module.c | 24 +++++++++++++++++++ .../same_name/private/choice.c | 7 ++++++ .../same_name/private/meson.build | 9 +++++++ tests/test_wheel.py | 15 ++++++++++++ 11 files changed, 114 insertions(+) create mode 100644 tests/packages/same-name-sharedlibs/meson.build create mode 100644 tests/packages/same-name-sharedlibs/pyproject.toml create mode 100644 tests/packages/same-name-sharedlibs/same_name/__init__.py create mode 100644 tests/packages/same-name-sharedlibs/same_name/competing/choice.c create mode 100644 tests/packages/same-name-sharedlibs/same_name/competing/meson.build create mode 100644 tests/packages/same-name-sharedlibs/same_name/meson.build create mode 100644 tests/packages/same-name-sharedlibs/same_name/module.c create mode 100644 tests/packages/same-name-sharedlibs/same_name/private/choice.c create mode 100644 tests/packages/same-name-sharedlibs/same_name/private/meson.build diff --git a/tests/conftest.py b/tests/conftest.py index f70708b47..1b5939de6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -223,6 +223,7 @@ def __init__(self, source_dir, build_dir, meson_args=None, editable_verbose=None # in the wild thus we should make sure it keeps working. 'link-against-local-lib', 'sharedlib-in-package', + 'same-name-sharedlibs', }: if meson_args is None: diff --git a/tests/packages/same-name-sharedlibs/meson.build b/tests/packages/same-name-sharedlibs/meson.build new file mode 100644 index 000000000..690cc5594 --- /dev/null +++ b/tests/packages/same-name-sharedlibs/meson.build @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('same-name-sharedlibs', 'c', version: '1.0.0') + +py = import('python').find_installation(pure: false) + +subdir('same_name') diff --git a/tests/packages/same-name-sharedlibs/pyproject.toml b/tests/packages/same-name-sharedlibs/pyproject.toml new file mode 100644 index 000000000..435419311 --- /dev/null +++ b/tests/packages/same-name-sharedlibs/pyproject.toml @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] diff --git a/tests/packages/same-name-sharedlibs/same_name/__init__.py b/tests/packages/same-name-sharedlibs/same_name/__init__.py new file mode 100644 index 000000000..775d000c4 --- /dev/null +++ b/tests/packages/same-name-sharedlibs/same_name/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/packages/same-name-sharedlibs/same_name/competing/choice.c b/tests/packages/same-name-sharedlibs/same_name/competing/choice.c new file mode 100644 index 000000000..0e2acdf20 --- /dev/null +++ b/tests/packages/same-name-sharedlibs/same_name/competing/choice.c @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2026 The meson-python developers +// +// SPDX-License-Identifier: MIT + +int choice(void) { + return 7; +} diff --git a/tests/packages/same-name-sharedlibs/same_name/competing/meson.build b/tests/packages/same-name-sharedlibs/same_name/competing/meson.build new file mode 100644 index 000000000..efc69ac7f --- /dev/null +++ b/tests/packages/same-name-sharedlibs/same_name/competing/meson.build @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +shared_library( + 'choice', 'choice.c', + install: true, + install_dir: py.get_install_dir() / 'same_name/competing', +) diff --git a/tests/packages/same-name-sharedlibs/same_name/meson.build b/tests/packages/same-name-sharedlibs/same_name/meson.build new file mode 100644 index 000000000..ee4f3d1df --- /dev/null +++ b/tests/packages/same-name-sharedlibs/same_name/meson.build @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +subdir('competing') +subdir('private') + +py.install_sources('__init__.py', subdir: 'same_name') + +origin = build_machine.system() == 'darwin' ? '@loader_path' : '$ORIGIN' + +# Simulate an external library whose runtime path is supplied through linker +# flags (for example, -Wl,-rpath,... in LDFLAGS). Keep that library inside this +# fixture to make the test self-contained. The requested private installation +# path must take precedence over this preserved competing path. +py.extension_module( + 'choice', 'module.c', + link_with: private, + link_args: f'-Wl,-rpath,@origin@/competing', + install: true, + subdir: 'same_name', + install_rpath: '$ORIGIN/private', +) diff --git a/tests/packages/same-name-sharedlibs/same_name/module.c b/tests/packages/same-name-sharedlibs/same_name/module.c new file mode 100644 index 000000000..0b72ecc9a --- /dev/null +++ b/tests/packages/same-name-sharedlibs/same_name/module.c @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 The meson-python developers +// +// SPDX-License-Identifier: MIT + +#include + +extern int choice(void); + +static PyObject *value(PyObject *self, PyObject *args) { + return PyLong_FromLong(choice()); +} + +static PyMethodDef methods[] = { + {"value", value, METH_NOARGS, NULL}, + {NULL, NULL, 0, NULL}, +}; + +static struct PyModuleDef module = { + PyModuleDef_HEAD_INIT, "choice", NULL, -1, methods, +}; + +PyMODINIT_FUNC PyInit_choice(void) { + return PyModule_Create(&module); +} diff --git a/tests/packages/same-name-sharedlibs/same_name/private/choice.c b/tests/packages/same-name-sharedlibs/same_name/private/choice.c new file mode 100644 index 000000000..40925a806 --- /dev/null +++ b/tests/packages/same-name-sharedlibs/same_name/private/choice.c @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2026 The meson-python developers +// +// SPDX-License-Identifier: MIT + +int choice(void) { + return 42; +} diff --git a/tests/packages/same-name-sharedlibs/same_name/private/meson.build b/tests/packages/same-name-sharedlibs/same_name/private/meson.build new file mode 100644 index 000000000..1b69252e1 --- /dev/null +++ b/tests/packages/same-name-sharedlibs/same_name/private/meson.build @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +private = shared_library( + 'choice', 'choice.c', + install: true, + install_dir: py.get_install_dir() / 'same_name/private', +) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 2709f4df9..a7cb150dc 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -294,6 +294,21 @@ def test_uneeded_rpath(wheel_purelib_and_platlib, tmp_path): assert origin not in path +@pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') +@pytest.mark.filterwarnings('ignore:translated "install_rpath" argument for ') +def test_rpath_install_precedence(venv, wheel_same_name_sharedlibs, tmp_path): + artifact = wheel.wheelfile.WheelFile(wheel_same_name_sharedlibs) + artifact.extractall(tmp_path) + + origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN' + rpath = mesonpy._rpath.get_rpath(tmp_path / 'same_name' / f'choice{EXT_SUFFIX}') + assert rpath.index(f'{origin}/private') < rpath.index(f'{origin}/competing') + + venv.pip('install', wheel_same_name_sharedlibs) + output = venv.python('-c', 'from same_name import choice; print(choice.value())') + assert int(output) == 42 + + @pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires executable bit support') def test_executable_bit(wheel_executable_bit): artifact = wheel.wheelfile.WheelFile(wheel_executable_bit)