From c9aa0976cbbe2e4a92dc3adfda63228a6180533e Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 17:58:38 +0200 Subject: [PATCH 01/12] 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 9953d7b5..8bbc5bfe 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 de798e9528c24f21f88ecdc45330bbaf68b0bb57 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sat, 15 Feb 2025 12:03:21 +0100 Subject: [PATCH 02/12] 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.c | 9 ----- .../sharedlib-in-package/mypkg/examplelib.h | 7 ---- .../sharedlib-in-package/mypkg/meson.build | 26 +----------- .../mypkg/sub/examplelib2.h | 7 ---- tests/packages/sharedlib-in-package/src/lib.c | 10 +++++ 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 +-- 13 files changed, 90 insertions(+), 75 deletions(-) delete mode 100644 tests/packages/sharedlib-in-package/mypkg/examplelib.c delete mode 100644 tests/packages/sharedlib-in-package/mypkg/examplelib.h delete mode 100644 tests/packages/sharedlib-in-package/mypkg/sub/examplelib2.h create mode 100644 tests/packages/sharedlib-in-package/src/lib.c 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 71921cfe..e8f80243 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 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..5cad9636 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 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..5097a76e --- /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 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_wheel.py b/tests/test_wheel.py index 2fdf7a5a..406c146c 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -170,10 +170,8 @@ def test_local_lib(venv, wheel_link_against_local_lib): 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 7651c8231f9c931595c2c8ad2b4135624a5bb575 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 14:39:27 +0200 Subject: [PATCH 03/12] 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 be9b52b6..f70708b4 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 5cad9636..ff1d85a0 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 5097a76e..d27a2d39 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 50337e76e37fb92db2c8fb37d0c29a27bc7e00d5 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 14:45:25 +0200 Subject: [PATCH 04/12] 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 e8f80243..d311a58a 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 ff1d85a0..cebf2d52 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 d27a2d39..b673ef29 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 57de93ce2ca58080746827e105c3344cc11d75f9 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 16:42:59 +0200 Subject: [PATCH 05/12] TST: make RPATH test stricter --- tests/test_wheel.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 406c146c..ee6e7d0d 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -182,7 +182,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) @@ -190,9 +190,7 @@ def test_rpath(wheel_link_against_local_lib, tmp_path): 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 + assert rpath == expected @pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') From 76ba8c3a5b2311b25fd48317099c1689640e7fc4 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 16:43:24 +0200 Subject: [PATCH 06/12] TST: add another RPATH handling verification test This shows that build RPATHs are not correctly stripped. --- tests/test_wheel.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index ee6e7d0d..9b6d470d 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -168,6 +168,25 @@ 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}')) + # 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))') From 05356f5d172bbeecb6b6e0633a0f5001beb1338d Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 17:25:38 +0200 Subject: [PATCH 07/12] 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 9b6d470d..8e75897d 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -187,6 +187,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))') @@ -212,6 +228,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 b9d9651692252bebb22430d98706b9eb48d9c213 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 10 Aug 2025 13:54:53 +0200 Subject: [PATCH 08/12] MAINT: refactor RPATH handling code and add tests This does not introduce any functional changes, except removing duplicates entries from RPATH. Fixes #813. --- mesonpy/_rpath.py | 115 ++++++++++++++++++++++++++++++-------------- tests/test_rpath.py | 39 +++++++++++++++ tests/test_wheel.py | 14 +++--- 3 files changed, 125 insertions(+), 43 deletions(-) create mode 100644 tests/test_rpath.py diff --git a/mesonpy/_rpath.py b/mesonpy/_rpath.py index 8bbc5bfe..8988b025 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -11,18 +11,57 @@ if typing.TYPE_CHECKING: - from typing import Iterable, List, Union + from typing import List, TypeVar, Union Path = Union[str, os.PathLike[str]] + T = TypeVar('T') -if sys.platform == 'win32' or sys.platform == 'cygwin': +def unique(values: List[T]) -> List[T]: + r = [] + for value in values: + if value not in r: + r.append(value) + return r + + +class RPATH: + + origin = '$ORIGIN' + + @staticmethod + def get_rpath(filepath: Path) -> List[str]: + raise NotImplementedError + + @staticmethod + def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None: + raise NotImplementedError + + @classmethod + def fix_rpath(cls, filepath: Path, 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 +73,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) @@ -55,35 +101,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 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_wheel.py b/tests/test_wheel.py index 8e75897d..c29cc766 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -175,15 +175,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() @@ -199,7 +199,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 @@ -224,7 +224,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}')) assert rpath == expected @@ -239,7 +239,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 @@ -249,7 +249,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 de265ddb017d67bdeb64043857f750c696a0d4cf Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 10 Aug 2025 16:14:03 +0200 Subject: [PATCH 09/12] 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 94bdc6bb..ec180971 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -454,12 +454,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 @@ -496,6 +490,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 5873ad30ac35bc9cdf5aa5d9dac3ad9685bf489a Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 10 Aug 2025 17:40:17 +0200 Subject: [PATCH 10/12] 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 +++++++++++++++----- tests/test_wheel.py | 3 ++- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index ec180971..0d60eef3 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -454,12 +454,6 @@ def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path, if self._has_internal_libs: if _is_native(origin): - # 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) diff --git a/mesonpy/_rpath.py b/mesonpy/_rpath.py index 8988b025..84ffbb57 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -39,11 +39,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, 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) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index c29cc766..c1cac437 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -225,7 +225,8 @@ def test_link_against_local_lib_rpath(wheel_link_against_local_lib, tmp_path): expected = {f'{origin}/../.link_against_local_lib.mesonpy.libs', 'custom-rpath',} rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}')) - assert rpath == expected + # FIXME: RPATH entries added by Meson to point to the build directory are not removed. + assert rpath >= expected @pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') From 17c457d2d9b270a71d70af093e7d5c79090fb829 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sat, 18 Oct 2025 19:05:17 +0200 Subject: [PATCH 11/12] 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 | 23 ++++++++++++------- mesonpy/_rpath.py | 11 +++++---- .../sharedlib-in-package/mypkg/meson.build | 12 ++++++---- .../sharedlib-in-package/src/meson.build | 12 ++++++---- 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/docs/reference/meson-compatibility.rst b/docs/reference/meson-compatibility.rst index 73258e73..3ce8fa3d 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()`` from the ``python`` + Meson module. On older Meson versions, this argument has no effect. + .. option:: 1.9.0 Meson 1.9.0 or later is required to support building for iOS. diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 0d60eef3..a9ffd7b3 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -125,6 +125,10 @@ def _compile_patterns(patterns: List[str]) -> Callable[[str], bool]: class _Entry(typing.NamedTuple): dst: pathlib.Path src: str + # Meson support only one install_rpath entry per target. Use a + # list to store install RPATH to be able to add append more + # entries when needed. + install_rpath: List[str] = [] def _map_to_wheel( @@ -183,7 +187,8 @@ def _map_to_wheel( filedst = dst / relpath wheel_files[path].append(_Entry(filedst, filesrc)) else: - wheel_files[path].append(_Entry(dst, src)) + install_rpath = target.get('install_rpath') + wheel_files[path].append(_Entry(dst, src, [install_rpath] if install_rpath else [])) return wheel_files @@ -449,13 +454,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()) @@ -499,7 +506,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: @@ -510,7 +517,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 84ffbb57..8ddca018 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -11,7 +11,7 @@ if typing.TYPE_CHECKING: - from typing import List, TypeVar, Union + from typing import List, Optional, TypeVar, Union Path = Union[str, os.PathLike[str]] T = TypeVar('T') @@ -37,7 +37,7 @@ 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: Optional[str]) -> None: old_rpath = cls.get_rpath(filepath) new_rpath = old_rpath[:] @@ -51,9 +51,12 @@ 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)) + # Add install_rpath. + new_rpath += install_rpath + new_rpath = unique(new_rpath) if new_rpath != old_rpath: cls.set_rpath(filepath, old_rpath, new_rpath) @@ -62,7 +65,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 cebf2d52..915d7c9c 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 b673ef29..78ae3c9a 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 1fa120b472711192e947a86b814ff0e5db71fcdd Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sat, 27 Jun 2026 22:57:02 +0200 Subject: [PATCH 12/12] ENH: add support for removing build time RPATH entries Requires Meson 1.9.0. --- docs/reference/meson-compatibility.rst | 6 ++++++ mesonpy/__init__.py | 20 ++++++++++++-------- mesonpy/_rpath.py | 13 +++++++++---- tests/test_wheel.py | 12 +++++++++--- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/docs/reference/meson-compatibility.rst b/docs/reference/meson-compatibility.rst index 3ce8fa3d..0d649620 100644 --- a/docs/reference/meson-compatibility.rst +++ b/docs/reference/meson-compatibility.rst @@ -61,6 +61,12 @@ 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 to execute parts of the project from + the build directory and that are normally removed during ``meson + install``. On older Meson versions, these entries are not removed, + but this should not have any adverse effect. + Build front-ends by default build packages in an isolated Python environment where build dependencies are installed. Most often, unless a package or its build dependencies declare explicitly a version diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index a9ffd7b3..06ae1f63 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -129,6 +129,8 @@ class _Entry(typing.NamedTuple): # list to store install RPATH to be able to add append more # entries when needed. install_rpath: List[str] = [] + # RPATH entries to remove at install time. + build_rpath: List[str] = [] def _map_to_wheel( @@ -188,7 +190,8 @@ def _map_to_wheel( wheel_files[path].append(_Entry(filedst, filesrc)) else: install_rpath = target.get('install_rpath') - wheel_files[path].append(_Entry(dst, src, [install_rpath] if install_rpath else [])) + build_rpath = target.get('build_rpaths') + wheel_files[path].append(_Entry(dst, src, [install_rpath] if install_rpath else [], build_rpath or [])) return wheel_files @@ -454,15 +457,16 @@ def _stable_abi(self) -> Optional[str]: return 'abi3.abi3t' if abi3t else 'abi3' return None - def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path, destination: pathlib.Path, - 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()) @@ -506,7 +510,7 @@ def build(self, directory: Path) -> pathlib.Path: root = 'purelib' if self._pure else 'platlib' for path, entries in self._manifest.items(): - for dst, src, install_rpath in entries: + for dst, src, install_rpath, build_rpath in entries: counter.update(src) if path == root: @@ -517,7 +521,7 @@ def build(self, directory: Path) -> pathlib.Path: else: dst = pathlib.Path(self._data_dir, path, dst) - self._install_path(whl, src, dst, 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 8ddca018..e4800dd8 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -37,9 +37,14 @@ 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: Optional[str]) -> None: + def fix_rpath(cls, filepath: Path, add: List[str], remove: List[str], libs_relative_path: Optional[str]) -> None: old_rpath = cls.get_rpath(filepath) - new_rpath = old_rpath[:] + + # Meson adds a padding entry to RPATH composed of enough `X` + # characters to reserve enough space in the ELF header to hold + # the final installation RPATH. Remove this entry and other + # entries to be removed. + new_rpath = [path for path in old_rpath if path.strip('X') and path not in remove] # When an executable, libray, or Python extension module is # dynamically linked to a library built as part of the project, Meson @@ -55,7 +60,7 @@ def fix_rpath(cls, filepath: Path, install_rpath: List[str], libs_relative_path: new_rpath.append(os.path.join(cls.origin, libs_relative_path)) # Add install_rpath. - new_rpath += install_rpath + new_rpath += add new_rpath = unique(new_rpath) if new_rpath != old_rpath: @@ -65,7 +70,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, add: List[str], remove: List[str], libs_relative_path: Optional[str]) -> None: pass diff --git a/tests/test_wheel.py b/tests/test_wheel.py index c1cac437..3ac4fc40 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. @@ -176,12 +179,14 @@ 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}')) - # FIXME: RPATH entries added by Meson to point to the build directory are not removed. assert rpath >= {origin} + if BUILD_RPATH_SUPPORT: + 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'} + if BUILD_RPATH_SUPPORT: + assert rpath == {f'{origin}/sub'} rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / 'sub' / f'libsublib{LIB_SUFFIX}')) assert rpath == set() @@ -225,8 +230,9 @@ def test_link_against_local_lib_rpath(wheel_link_against_local_lib, tmp_path): expected = {f'{origin}/../.link_against_local_lib.mesonpy.libs', 'custom-rpath',} rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}')) - # FIXME: RPATH entries added by Meson to point to the build directory are not removed. assert rpath >= expected + if BUILD_RPATH_SUPPORT: + assert rpath == expected @pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support')