From 3884fb546b080baa12d983dd87a6c0b0c52bf96a Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 19 Jul 2026 21:27:08 +0200 Subject: [PATCH 1/2] TST: add Rust extension module using PyO3 This requires nightly Rust and unreleased Meson, and it currently bumps into a Meson warning: ``WARNING: Library entry portable_atomic has unexpected keys "doc-scrape-examples"``. --- tests/conftest.py | 4 + tests/packages/rust-pyo3/Cargo.lock | 132 ++++++++++++ tests/packages/rust-pyo3/Cargo.toml | 15 ++ tests/packages/rust-pyo3/meson.build | 17 ++ tests/packages/rust-pyo3/pyproject.toml | 10 + tests/packages/rust-pyo3/src/lib.rs | 16 ++ .../packages/rust-pyo3/subprojects/.wraplock | 0 .../pyo3-0.29-rs/meson/meson.build | 1 + .../packagefiles/pyo3-ffi-0.29-rs/meson.build | 202 ++++++++++++++++++ .../pyo3-ffi-0.29-rs/meson.options | 15 ++ tests/test_wheel.py | 15 ++ 11 files changed, 427 insertions(+) create mode 100644 tests/packages/rust-pyo3/Cargo.lock create mode 100644 tests/packages/rust-pyo3/Cargo.toml create mode 100644 tests/packages/rust-pyo3/meson.build create mode 100644 tests/packages/rust-pyo3/pyproject.toml create mode 100644 tests/packages/rust-pyo3/src/lib.rs create mode 100644 tests/packages/rust-pyo3/subprojects/.wraplock create mode 100644 tests/packages/rust-pyo3/subprojects/packagefiles/pyo3-0.29-rs/meson/meson.build create mode 100644 tests/packages/rust-pyo3/subprojects/packagefiles/pyo3-ffi-0.29-rs/meson.build create mode 100644 tests/packages/rust-pyo3/subprojects/packagefiles/pyo3-ffi-0.29-rs/meson.options diff --git a/tests/conftest.py b/tests/conftest.py index be9b52b6a..f4267cbe3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -224,6 +224,10 @@ def __init__(self, source_dir, build_dir, meson_args=None, editable_verbose=None # keeps working. 'link-against-local-lib', + # The ``rust-pyo3`` package bumps into this Meson + # warning: ``WARNING: Library entry portable_atomic + # has unexpected keys "doc-scrape-examples"``. + 'rust-pyo3', }: if meson_args is None: meson_args = {} diff --git a/tests/packages/rust-pyo3/Cargo.lock b/tests/packages/rust-pyo3/Cargo.lock new file mode 100644 index 000000000..e154726f2 --- /dev/null +++ b/tests/packages/rust-pyo3/Cargo.lock @@ -0,0 +1,132 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b" +dependencies = [ + "libc", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", +] + +[[package]] +name = "pyo3-build-config" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rust-pyo3" +version = "1.0.0" +dependencies = [ + "pyo3", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/tests/packages/rust-pyo3/Cargo.toml b/tests/packages/rust-pyo3/Cargo.toml new file mode 100644 index 000000000..f1dbda72e --- /dev/null +++ b/tests/packages/rust-pyo3/Cargo.toml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[package] +name = "rust-pyo3" +version = "1.0.0" +edition = "2024" + +[lib] +name = "rust_pyo3" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = "0.29.0" diff --git a/tests/packages/rust-pyo3/meson.build b/tests/packages/rust-pyo3/meson.build new file mode 100644 index 000000000..ee2b45b73 --- /dev/null +++ b/tests/packages/rust-pyo3/meson.build @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('rust-pyo3', 'c', 'rust', version: '1.0.0', meson_version: '>= 1.12.0', default_options: ['rust_std=2024']) + +py = import('python').find_installation(pure: false) +pkg = import('rust').workspace().package() + +py.extension_module( + 'rust_pyo3', + 'src/lib.rs', + rust_dependency_map : pkg.rust_dependency_map(), + rust_args: pkg.rust_args(), + dependencies : pkg.dependencies(), + install: true, +) diff --git a/tests/packages/rust-pyo3/pyproject.toml b/tests/packages/rust-pyo3/pyproject.toml new file mode 100644 index 000000000..8f494d51c --- /dev/null +++ b/tests/packages/rust-pyo3/pyproject.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2021 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = "mesonpy" +requires = ["meson-python"] + +[tool.meson-python.args] +setup = ['--vsenv'] diff --git a/tests/packages/rust-pyo3/src/lib.rs b/tests/packages/rust-pyo3/src/lib.rs new file mode 100644 index 000000000..1b6bb9fc3 --- /dev/null +++ b/tests/packages/rust-pyo3/src/lib.rs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2026 The meson-python developers +// +// SPDX-License-Identifier: MIT + +use pyo3::prelude::*; + +#[pymodule] +mod rust_pyo3 { + use pyo3::prelude::*; + + /// Sum two numbers. + #[pyfunction] + fn sum(a: i64, b: i64) -> PyResult { + Ok(a + b) + } +} diff --git a/tests/packages/rust-pyo3/subprojects/.wraplock b/tests/packages/rust-pyo3/subprojects/.wraplock new file mode 100644 index 000000000..e69de29bb diff --git a/tests/packages/rust-pyo3/subprojects/packagefiles/pyo3-0.29-rs/meson/meson.build b/tests/packages/rust-pyo3/subprojects/packagefiles/pyo3-0.29-rs/meson/meson.build new file mode 100644 index 000000000..919683dfd --- /dev/null +++ b/tests/packages/rust-pyo3/subprojects/packagefiles/pyo3-0.29-rs/meson/meson.build @@ -0,0 +1 @@ +extra_args += cargo_ws.subproject('pyo3-ffi', pkg_obj.api(), native: false).dependency().get_variable('args').split('\0') diff --git a/tests/packages/rust-pyo3/subprojects/packagefiles/pyo3-ffi-0.29-rs/meson.build b/tests/packages/rust-pyo3/subprojects/packagefiles/pyo3-ffi-0.29-rs/meson.build new file mode 100644 index 000000000..ac20b57b8 --- /dev/null +++ b/tests/packages/rust-pyo3/subprojects/packagefiles/pyo3-ffi-0.29-rs/meson.build @@ -0,0 +1,202 @@ +project( + 'pyo3-ffi', + 'rust', + meson_version : '>= 1.12', + version : '0.29.2', + license : 'MIT OR Apache-2.0' +) + +MINIMUM_SUPPORTED_VERSION_MINOR = 8 +STABLE_ABI_MAX_MINOR = 15 +MINIMUM_SUPPORTED_VERSION_PYPY_MINOR = 11 +MAXIMUM_SUPPORTED_VERSION_PYPY_MINOR = 11 + +extra_args = [] +extra_deps = [] + +# Registers PyO3's config names as reachable cfg expressions. +extra_args += [ + '--check-cfg', 'cfg(Py_LIMITED_API)', + '--check-cfg', 'cfg(Py_GIL_DISABLED)', + '--check-cfg', 'cfg(PyPy)', + '--check-cfg', 'cfg(GraalPy)', + '--check-cfg', 'cfg(RustPython)', + '--check-cfg', 'cfg(py_sys_config, values("Py_DEBUG", "Py_REF_DEBUG", "Py_TRACE_REFS", "COUNT_ALLOCS"))', +] + +# Allow 'Py_3_{minor}' cfgs from the minimum supported version up to +# the maximum minor version (+1 for development for the next). +foreach i : range(MINIMUM_SUPPORTED_VERSION_MINOR, STABLE_ABI_MAX_MINOR + 2) + extra_args += ['--check-cfg', f'cfg(Py_3_@i@)'] +endforeach + +# Allow all possible 'pyo3_dll' cfg values for raw-dylib linking on Windows. +dll_names = ['python3', 'python3_d', 'python3t', 'python3t_d'] + +foreach i : range(MINIMUM_SUPPORTED_VERSION_MINOR, STABLE_ABI_MAX_MINOR + 2) + # CPython DLL names. + dll_names += [f'python3@i@', f'python3@i@_d'] + if i >= 13 + dll_names += [f'python3@i@t', f'python3@i@t_d'] + endif +endforeach + +foreach i : range(MINIMUM_SUPPORTED_VERSION_PYPY_MINOR, MAXIMUM_SUPPORTED_VERSION_PYPY_MINOR + 1) + # PyPy DLL names. + dll_names += [f'libpypy3.@i@-c'] +endforeach + +dll_names_quoted = [] +foreach name : dll_names + dll_names_quoted += f'"@name@"' +endforeach + +pyo3_dll_values = ', '.join(dll_names_quoted) +extra_args += ['--check-cfg', f'cfg(pyo3_dll, values(@pyo3_dll_values@))'] + + +fs = import('fs') +rust = import('rust') +cargo_ws = rust.workspace() +pkg_obj = cargo_ws.package('pyo3-ffi', native : false) + + +features = pkg_obj.features() +message('Enabled features:', features) + + +# Deprecated cargo features. +if features.contains('extension-module') + warning('The extension-module cargo feature is deprecated. Use the "extension_module" Meson option instead.') +endif +if features.contains('generate-import-lib') + warning('The generate-import-lib cargo feature is deprecated') +endif + + +py = import('python').find_installation() +py_minor_version = py.language_version().split('.')[1].to_int() + +is_windows = host_machine.system() == 'windows' +py_gil_disabled = py.get_variable('Py_GIL_DISABLED', 0) == 1 +py_debug = py.get_variable('Py_DEBUG', 0) == 1 + + +# Implementation detection. +soabi = py.get_variable('SOABI', 'cpython') +is_pypy = soabi.startswith('pypy') +if is_pypy + extra_args += ['--cfg', 'PyPy'] +endif + + +# Python ABI. +if features.contains('abi3t') + if not py_gil_disabled + error('abi3t required but Python is not free-threaded') + endif + abi = '3t' + limited_api = true +elif features.contains('abi3') + if py_gil_disabled + error('abi3 required but Python is free-threaded') + endif + abi = '3' + limited_api = true +else + abi = f'3@py_minor_version@' + (py_gil_disabled ? 't' : '') + limited_api = false +endif + +if py_gil_disabled + extra_args += ['--cfg', 'Py_GIL_DISABLED'] +endif + + +# Emit cumulative 'Py_3_{minor}' cfgs up to the interpreter version, +# or up to the abi3 floor when targeting the stable ABI. +py_minor_version_max = py_minor_version +if limited_api + if is_pypy + error('abi3 required but PyPy does not support it') + endif + if is_windows and py_debug + error('no stable-ABI python3.dll exists for debug CPython on Windows') + endif + + extra_args += ['--cfg', 'Py_LIMITED_API'] + # Parse feature in the form 'abi3-py3{minor}' or 'abi3t-py3{minor}' + # to extract the minor version. + foreach f : features + if f.startswith('abi3-py3') + floor = f.substring(8).to_int() + elif f.startswith('abi3t-py3') + floor = f.substring(9).to_int() + else + continue + endif + py_minor_version_max = floor < py_minor_version_max ? floor : py_minor_version_max + endforeach +endif + +foreach m : range(MINIMUM_SUPPORTED_VERSION_MINOR, py_minor_version_max + 1) + extra_args += ['--cfg', f'Py_3_@m@'] +endforeach + + +foreach flag : ['Py_DEBUG', 'Py_REF_DEBUG', 'Py_TRACE_REFS', 'COUNT_ALLOCS'] + if py.get_variable(flag, 0) == 1 + extra_args += ['--cfg', f'py_sys_config="@flag@"'] + endif +endforeach + + +if is_windows + # Windows: extension modules need to link to libpython. + if is_pypy + py_lib_name = f'libpypy3.@py_minor_version@-c' + else + py_lib_name = 'python' + abi + (py_debug ? '_d' : '') + endif + if get_option('use_raw_dylib') + extra_args += ['--cfg', 'pyo3_use_raw_dylib', '--cfg', f'pyo3_dll="@py_lib_name@"'] + else + error('configuration currently not supported') + # extra_args += ['-l', f'pythonXY:@py_lib_name@'] + # extra_deps += py.dependency() + endif +else + # Unix: extension modules never link libpython, only embedding links it. + if not get_option('extension_module') + extra_deps += py.dependency(embed : true) + endif +endif + + +# Compiler features used by PyO3. +rustc_features = { + 'const_is_null': 84, + 'fn_ptr_eq': 85, + 'from_bytes_with_nul_error': 86, + 'cfg_select': 95, +} +rustc_minor_version = meson.get_compiler('rust').version().split('.')[1].to_int() +foreach cfg, minor_version_required : rustc_features + extra_args += ['--check-cfg', f'cfg(@cfg@)'] + if rustc_minor_version >= minor_version_required + extra_args += ['--cfg', cfg] + endif +endforeach + + +lib = pkg_obj.library('pyo3_ffi', dependencies : extra_deps, rust_args : extra_args) +dep = declare_dependency( + link_with : lib, + variables : { + 'features' : ','.join(features), + 'args' : '\0'.join(extra_args), + 'extension_module' : get_option('extension_module').to_string(), + }, + version : pkg_obj.version() +) +pkg_obj.override_dependency(dep) diff --git a/tests/packages/rust-pyo3/subprojects/packagefiles/pyo3-ffi-0.29-rs/meson.options b/tests/packages/rust-pyo3/subprojects/packagefiles/pyo3-ffi-0.29-rs/meson.options new file mode 100644 index 000000000..922935ea2 --- /dev/null +++ b/tests/packages/rust-pyo3/subprojects/packagefiles/pyo3-ffi-0.29-rs/meson.options @@ -0,0 +1,15 @@ +option( + 'extension_module', + type : 'boolean', + value : true, # Opposite default as the PyO3 native build system. + yield : true, + description : 'Build for a Python extension module. Set to false for embedding. Analogue to PYO3_BUILD_EXTENSION_MODULE. ', +) + +option( + 'use_raw_dylib', + type : 'boolean', + value : true, + yield : true, + description : 'Rely on raw-dylib on Windows instead than linking to the Python library. Analogue to PYO3_USE_RAW_DYLIB', +) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 2fdf7a5a0..d2ebf5973 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -6,6 +6,7 @@ import re import shutil import stat +import subprocess import sys import textwrap @@ -17,6 +18,11 @@ from .conftest import EXT_SUFFIX, FREE_THREADED_BUILD, MESON_VERSION, adjust_packaging_platform_tag, metadata +if shutil.which('rustc'): + _rustc_ver_str = subprocess.run(['rustc', '--version'], check=True, stdout=subprocess.PIPE, text=True).stdout + RUSTC_VERSION = tuple(map(int, re.search(r'[0-9]+\.[0-9]+\.[0-9]', _rustc_ver_str).group(0).split('.'))) +else: + RUSTC_VERSION = (0, ) if sys.platform in {'win32', 'cygwin'}: EXT_IMP_SUFFIX = re.sub(r'.(pyd|dll)$', '.lib' if shutil.which('cl.exe') else '.dll.a', EXT_SUFFIX) @@ -413,3 +419,12 @@ def test_limited_api_free_threaded(wheel_limited_api_free_threaded): assert name.group('pyver') == INTERPRETER assert name.group('abi') == 'abi3.abi3t' if FREE_THREADED_BUILD else 'abi3' assert name.group('plat') == PLATFORM + + +@pytest.mark.skipif(MESON_VERSION < (1, 12, 0), reason='meson too old') +@pytest.mark.skipif(RUSTC_VERSION < (1, 99, 0), reason='rustc not found or too old') +@pytest.mark.skipif(sys.platform == 'win32' and not os.getenv('CI'), reason='requires MSVC') +def test_rust_pyo3(venv, wheel_rust_pyo3): + venv.pip('install', wheel_rust_pyo3) + output = venv.python('-c', 'import rust_pyo3; print(rust_pyo3.sum(1, 2))') + assert int(output) == 3 From e2385e136443281d06aeb31c08e03408276987d9 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 19 Jul 2026 21:38:11 +0200 Subject: [PATCH 2/2] CI: Set up Rust compiler --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4f837e5af..2dc514002 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -136,6 +136,9 @@ jobs: run: python -m pip install ${{ matrix.dependencies }} if: ${{ matrix.dependencies }} + - name: Set up Rust compiler + run: rustup override set nightly + - name: Install run: python -m pip install . --group test