From 2706ec4577dd31018c7309591725c70a71885a80 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 17:58:38 +0200 Subject: [PATCH 01/11] 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 a7cbbb92a..13aedb4f1 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -74,7 +74,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 497c0183eee1a33267d9bb84bf4ccd865e3722b2 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sat, 15 Feb 2025 12:03:21 +0100 Subject: [PATCH 02/11] 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 the RPATH handling. --- .../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 | 8 ++-- 13 files changed, 91 insertions(+), 76 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 b6f64ceec..3ae6b3afa 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -180,13 +180,11 @@ 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 version too old') +@pytest.mark.skipif(MESON_VERSION < (1, 3, 0), reason='meson too old') def test_link_library_in_subproject(venv, wheel_link_library_in_subproject): venv.pip('install', wheel_link_library_in_subproject) output = venv.python('-c', 'import foo; print(foo.example_sum(3, 6))') From c2e7036ba7dc1b0dc692f01e7634f642ad6635fa Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 14:39:27 +0200 Subject: [PATCH 03/11] TST: use linker flags instead of install_rpath to set the RPATH meson-python does not support `install_rpath` yet and anyhow `install_rpath` is not exposed in the introspection data and thus cannot be set by meson-python when building the Python wheel. Use `link_args` to set the RPATH. --- tests/packages/sharedlib-in-package/mypkg/meson.build | 6 +++++- tests/packages/sharedlib-in-package/src/meson.build | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) 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 98c06167e9c351abf950f03aed618274a930eb58 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 14:45:25 +0200 Subject: [PATCH 04/11] 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 5bb37a7906b127b231f099dab0e1b7a3ea34e916 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 16:42:59 +0200 Subject: [PATCH 05/11] 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 3ae6b3afa..264d9decb 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -192,7 +192,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) @@ -200,9 +200,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 d3ddf79a66472b9cd99db1b2faba7363f180faee Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 16:43:24 +0200 Subject: [PATCH 06/11] TST: add another RPATH handling verification test This shows that build RPATHs are not correctly stripped. --- tests/test_wheel.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 264d9decb..7e82ab68f 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -178,6 +178,27 @@ def test_local_lib(venv, wheel_link_against_local_lib): assert int(output) == 3 +@pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') +def test_sharedlib_in_package_rpath(wheel_sharedlib_in_package, tmp_path): + artifact = wheel.wheelfile.WheelFile(wheel_sharedlib_in_package) + artifact.extractall(tmp_path) + + origin = '@loader_path' if sys.platform == 'darwin' else '$ORIGIN' + + rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / f'_example{EXT_SUFFIX}')) + # This RPATH entry should be removed by meson-python but it is not. + build_rpath = {f'{origin}/../src'} + assert rpath == {origin, *build_rpath} + + rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / f'liblib{LIB_SUFFIX}')) + # This RPATH entry should be removed by meson-python but it is not. + build_rpath = {f'{origin}/'} + assert rpath == {f'{origin}/sub', *build_rpath} + + rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / 'sub' / f'libsublib{LIB_SUFFIX}')) + assert rpath == set() + + 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 e99ea48589c3d2d80adb43c21d3b516b812c48cb Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Wed, 30 Jul 2025 17:25:38 +0200 Subject: [PATCH 07/11] TST: test RPATH entries added via flags in $LDFLAGS environment variable --- tests/test_wheel.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 7e82ab68f..d118a4e08 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -199,6 +199,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))') @@ -224,6 +240,25 @@ 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) + + # The RPATH entry relative to $ORIGIN added via $LDFLAGS is + # erroneusly stripped by meson-python. + extra_rpath = {'/usr/lib/test-ldflags',} + + 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 d7072d7dad4ca14ac1a5816fe7d9e8256c7786f4 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Thu, 13 Feb 2025 17:57:10 +0100 Subject: [PATCH 08/11] MAINT: refactoring --- mesonpy/__init__.py | 49 +++++++++++++++++++++++---------------------- tests/test_tags.py | 3 ++- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index d4dd3bd08..0128ddb4b 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -15,6 +15,7 @@ import collections import contextlib import copy +import dataclasses import difflib import functools import importlib.machinery @@ -111,9 +112,14 @@ class InvalidLicenseExpression(Exception): # type: ignore[no-redef] } -def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]: +class Entry(typing.NamedTuple): + dst: pathlib.Path + src: str + + +def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[Entry]]: """Map files to the wheel, organized by wheel installation directory.""" - wheel_files: DefaultDict[str, List[Tuple[pathlib.Path, str]]] = collections.defaultdict(list) + wheel_files: DefaultDict[str, List[Entry]] = collections.defaultdict(list) packages: Dict[str, str] = {} for key, group in sources.items(): @@ -131,7 +137,8 @@ def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[T other = packages.setdefault(package, path) if other != path: this = os.fspath(pathlib.Path(path, *destination.parts[1:])) - that = os.fspath(other / next(d for d, s in wheel_files[other] if d.parts[0] == destination.parts[1])) + module = next(entry.dst for entry in wheel_files[other] if entry.dst.parts[0] == destination.parts[1]) + that = os.fspath(other / module) raise BuildError( f'The {package} package is split between {path} and {other}: ' f'{this!r} and {that!r}, a "pure: false" argument may be missing in meson.build. ' @@ -154,9 +161,9 @@ def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[T if relpath in exclude_files: continue filedst = dst / relpath - wheel_files[path].append((filedst, filesrc)) + wheel_files[path].append(Entry(filedst, filesrc)) else: - wheel_files[path].append((dst, src)) + wheel_files[path].append(Entry(dst, src)) return wheel_files @@ -303,20 +310,14 @@ def _is_native(file: Path) -> bool: return f.read(4) == b'\x7fELF' # ELF +@dataclasses.dataclass class _WheelBuilder(): """Helper class to build wheels from projects.""" - def __init__( - self, - metadata: Metadata, - manifest: Dict[str, List[Tuple[pathlib.Path, str]]], - limited_api: bool, - allow_windows_shared_libs: bool, - ) -> None: - self._metadata = metadata - self._manifest = manifest - self._limited_api = limited_api - self._allow_windows_shared_libs = allow_windows_shared_libs + _metadata: Metadata + _manifest: Dict[str, List[Entry]] + _limited_api: bool + _allow_windows_shared_libs: bool @property def _has_internal_libs(self) -> bool: @@ -332,8 +333,8 @@ def _pure(self) -> bool: """Whether the wheel is architecture independent""" if self._manifest['platlib'] or self._manifest['mesonpy-libs']: return False - for _, file in self._manifest['scripts']: - if _is_native(file): + for entry in self._manifest['scripts']: + if _is_native(entry.src): return False return True @@ -410,14 +411,14 @@ def _stable_abi(self) -> Optional[str]: # in {platlib} that look like extension modules, and raise # an exception if any of them has a Python version # specific extension filename suffix ABI tag. - for path, _ in self._manifest['platlib']: - match = _EXTENSION_SUFFIX_REGEX.match(path.name) + for entry in self._manifest['platlib']: + match = _EXTENSION_SUFFIX_REGEX.match(entry.dst.name) if match: abi = match.group('abi') if abi is not None and abi != 'abi3': raise BuildError( f'The package declares compatibility with Python limited API but extension ' - f'module {os.fspath(path)!r} is tagged for a specific Python version.') + f'module {os.fspath(entry.dst)!r} is tagged for a specific Python version.') return 'abi3' return None @@ -499,8 +500,8 @@ class _EditableWheelBuilder(_WheelBuilder): def _top_level_modules(self) -> Collection[str]: modules = set() for type_ in self._manifest: - for path, _ in self._manifest[type_]: - name, dot, ext = path.parts[0].partition('.') + for entry in self._manifest[type_]: + name, dot, ext = entry.dst.parts[0].partition('.') if dot: # module suffix = dot + ext @@ -886,7 +887,7 @@ def _info(self, name: str) -> Any: return json.loads(info.read_text(encoding='utf-8')) @property - def _manifest(self) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]: + def _manifest(self) -> DefaultDict[str, List[Entry]]: """The files to be added to the wheel, organized by wheel path.""" # Obtain the list of files Meson would install. diff --git a/tests/test_tags.py b/tests/test_tags.py index 62aa61934..60194348e 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -101,7 +101,8 @@ def test_ios_platform_tag(monkeypatch): def wheel_builder_test_factory(content, pure=True, limited_api=False): manifest = defaultdict(list) - manifest.update({key: [(pathlib.Path(x), os.path.join('build', x)) for x in value] for key, value in content.items()}) + for key, value in content.items(): + manifest[key] = [mesonpy.Entry(pathlib.Path(x), os.path.join('build', x)) for x in value] return mesonpy._WheelBuilder(None, manifest, limited_api, False) From 8795d1268c8561718c175ce363d4659aed53bd67 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 10 Aug 2025 13:54:53 +0200 Subject: [PATCH 09/11] MAINT: refactor RPATH handling code and add tests This does not introduce any functional changes. --- mesonpy/_rpath.py | 118 ++++++++++++++++++++++++++++++-------------- tests/test_rpath.py | 39 +++++++++++++++ tests/test_wheel.py | 14 +++--- 3 files changed, 127 insertions(+), 44 deletions(-) create mode 100644 tests/test_rpath.py diff --git a/mesonpy/_rpath.py b/mesonpy/_rpath.py index 13aedb4f1..c3e2bc725 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -11,19 +11,59 @@ if typing.TYPE_CHECKING: - from typing import List + from typing import List, TypeVar - from mesonpy._compat import Iterable, Path + from mesonpy._compat import Path + T = TypeVar('T') -if sys.platform == 'win32' or sys.platform == 'cygwin': - def fix_rpath(filepath: Path, libs_relative_path: str) -> None: +def unique(values: List[T]) -> List[T]: + r = [] + for value in values: + if value not in r: + r.append(value) + return r + + +class RPATH: + + origin = '$ORIGIN' + + @staticmethod + def get_rpath(filepath: Path) -> List[str]: + raise NotImplementedError + + @staticmethod + def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None: + raise NotImplementedError + + @classmethod + def fix_rpath(cls, filepath: Path, libs_relative_path: str) -> None: + old_rpath = cls.get_rpath(filepath) + new_rpath = [] + 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) + + +class _Windows(RPATH): + + @classmethod + def fix_rpath(cls, filepath: Path, libs_relative_path: str) -> None: pass -elif sys.platform == 'darwin': - def _get_rpath(filepath: Path) -> List[str]: +class _MacOS(RPATH): + + origin = '@loader_path' + + @staticmethod + def get_rpath(filepath: Path) -> List[str]: rpath = [] r = subprocess.run(['otool', '-l', os.fspath(filepath)], capture_output=True, text=True) rpath_tag = False @@ -35,17 +75,24 @@ def _get_rpath(filepath: Path) -> List[str]: rpath_tag = False return rpath - def _replace_rpath(filepath: Path, old: str, new: str) -> None: - subprocess.run(['install_name_tool', '-rpath', old, new, os.fspath(filepath)], check=True) + @staticmethod + def set_rpath(filepath: Path, old: List[str], rpath: List[str]) -> None: + # This implementation does not preserve the ordering of RPATH + # entries. Meson does the same, thus it should not be a problem. + args: List[str] = [] + for path in rpath: + if path not in old: + args += ['-add_rpath', path] + for path in old: + if path not in rpath: + args += ['-delete_rpath', path] + subprocess.run(['install_name_tool', *args, os.fspath(filepath)], check=True) - def fix_rpath(filepath: Path, libs_relative_path: str) -> None: - for path in _get_rpath(filepath): - if path.startswith('@loader_path/'): - _replace_rpath(filepath, path, '@loader_path/' + libs_relative_path) -elif sys.platform == 'sunos5': +class _SunOS5(RPATH): - def _get_rpath(filepath: Path) -> List[str]: + @staticmethod + def get_rpath(filepath: Path) -> List[str]: rpath = [] r = subprocess.run(['/usr/bin/elfedit', '-r', '-e', 'dyn:rpath', os.fspath(filepath)], capture_output=True, check=True, text=True) @@ -56,35 +103,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 d118a4e08..63914153c 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -185,17 +185,17 @@ 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}')) # This RPATH entry should be removed by meson-python but it is not. build_rpath = {f'{origin}/../src'} assert rpath == {origin, *build_rpath} - rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / f'liblib{LIB_SUFFIX}')) + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / f'liblib{LIB_SUFFIX}')) # This RPATH entry should be removed by meson-python but it is not. build_rpath = {f'{origin}/'} assert rpath == {f'{origin}/sub', *build_rpath} - rpath = set(mesonpy._rpath._get_rpath(tmp_path / 'mypkg' / 'sub' / f'libsublib{LIB_SUFFIX}')) + rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'mypkg' / 'sub' / f'libsublib{LIB_SUFFIX}')) assert rpath == set() @@ -211,7 +211,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 @@ -236,7 +236,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 @@ -255,7 +255,7 @@ def test_link_against_local_lib_rpath_ldflags(package_link_against_local_lib, tm # erroneusly stripped by meson-python. extra_rpath = {'/usr/lib/test-ldflags',} - 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 @@ -265,7 +265,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 0659973a0e01c004d16d412e7f4471a4aea502e8 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 10 Aug 2025 16:14:03 +0200 Subject: [PATCH 10/11] 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 0128ddb4b..7326c5b1c 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -427,12 +427,6 @@ def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path, if self._has_internal_libs: if _is_native(origin): - if sys.platform == 'win32' and not self._allow_windows_shared_libs: - raise NotImplementedError( - 'Loading shared libraries bundled in the Python wheel on Windows requires ' - 'setting the DLL load path or preloading. See the documentation for ' - 'the "tool.meson-python.allow-windows-internal-shared-libs" option.') - # When an executable, libray, or Python extension module is # dynamically linked to a library built as part of the project, # Meson adds a library load path to it pointing to the build @@ -469,6 +463,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 f84c84fc0dfce0d0980c8819fa5722aeba78041d Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 10 Aug 2025 17:40:17 +0200 Subject: [PATCH 11/11] BUG: do not remove RPATH entries relative to $ORIGIN for packages using internal shared libraries relocated by meson-python. --- mesonpy/__init__.py | 6 ------ mesonpy/_rpath.py | 20 +++++++++++++++----- tests/test_wheel.py | 6 ++---- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 7326c5b1c..b8c03b4dd 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -427,12 +427,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 c3e2bc725..1902282ee 100644 --- a/mesonpy/_rpath.py +++ b/mesonpy/_rpath.py @@ -41,11 +41,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 63914153c..908b49c8e 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -235,6 +235,8 @@ 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',} + # This RPATH entry should be removed by meson-python but it is not. + expected.add(f'{origin}/lib') rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}')) assert rpath == expected @@ -251,10 +253,6 @@ 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) - # The RPATH entry relative to $ORIGIN added via $LDFLAGS is - # erroneusly stripped by meson-python. - extra_rpath = {'/usr/lib/test-ldflags',} - rpath = set(mesonpy._rpath.get_rpath(tmp_path / 'example' / f'_example{EXT_SUFFIX}')) assert extra_rpath <= rpath