From 59d6f012c3ec62907e5048b1b226efa7a7f85149 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 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 ce9124aefab16af17892bb4c4914e62f60d5bfa5 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 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.c b/tests/packages/sharedlib-in-package/mypkg/examplelib.c deleted file mode 100644 index f486bd7fb..000000000 --- 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 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/src/lib.c b/tests/packages/sharedlib-in-package/src/lib.c new file mode 100644 index 000000000..e4fe14784 --- /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 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 2fdf7a5a0..406c146c5 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 6ea1add0597709e73dfcdd2d0c09f9cf3015864b 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 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 0e59d3dd93358afa04d05ce1470c185b92d74666 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 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 9e666223e49c97112f6c11be857197cdf4cc23be 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 406c146c5..ee6e7d0dc 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 65be9cdec64414e1935393876d4aae6598298a00 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 ee6e7d0dc..9b6d470d9 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 6deda687d63dffa1b4b47349108e2b968b927c4c 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 9b6d470d9..8e75897d9 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 5de51ad87186859f8dfac0ee37a0aaac39b362e0 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 8bbc5bfe0..8988b0257 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 000000000..425decd8d --- /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 8e75897d9..c29cc7667 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 1511df16d7832ddf51c332e010a6cfc4ef2ed2e6 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 3281bb1d5..f55f9e515 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -456,12 +456,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 @@ -498,6 +492,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 8ddb3ef6070a422f70303d0aaede7007ee7d8714 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 f55f9e515..107712a08 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -456,12 +456,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 8988b0257..84ffbb572 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 c29cc7667..c1cac4376 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 bf592ec46be48a2c3a825b3b4fa9f96d238dac31 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 73258e73a..3ce8fa3d1 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 107712a08..627229ab2 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -127,6 +127,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( @@ -185,7 +189,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 @@ -451,13 +456,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()) @@ -501,7 +508,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: @@ -512,7 +519,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 84ffbb572..8ddca0182 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 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 60044af129be1b9be6d120fce003a890f5bc9837 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 3ce8fa3d1..0d6496207 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 627229ab2..05d2f3bc8 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -131,6 +131,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( @@ -190,7 +192,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 @@ -456,15 +459,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()) @@ -508,7 +512,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: @@ -519,7 +523,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 8ddca0182..e4800dd8b 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 c1cac4376..3ac4fc40d 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')