From a2f965f62fbbb4432c21102a34343fd995e07bf8 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Sun, 8 Feb 2026 10:36:47 +0000 Subject: [PATCH] Add an integration test and debug issues with the meson backend (#2718) * Add an integration test with the meson backend * Modify script to allow testing of the GHA action on a PR * Try adding cython to languages in meson config * Revert "Try adding cython to languages in meson config" This reverts commit 50378a1c7e38665492ad0c683b178e4d96928e1e. * Pass --vsenv to meson on windows As seen here https://github.com/matplotlib/matplotlib/blob/9957c394bd01deb7a9bd9cb27804f447a52dc522/.github/workflows/cibuildwheel.yml#L114 * Disable win32 builds for the meson test * Move the windows-specific config into the test project definition This is so it can be tested with bin/run_example_ci_configs.py * Add some docs to the FAQ about meson on windows * Update bin/run_example_ci_configs.py Co-authored-by: Henry Schreiner --------- Co-authored-by: Henry Schreiner --- bin/run_example_ci_configs.py | 19 ++++++--- docs/faq.md | 15 +++++++ test/test_build_frontend_args.py | 5 +-- test/test_meson_python.py | 29 +++++++++++++ test/test_projects/__init__.py | 5 ++- test/test_projects/c.py | 71 -------------------------------- test/test_projects/meson.py | 60 +++++++++++++++++++++++++++ test/test_projects/setuptools.py | 71 ++++++++++++++++++++++++++++++++ 8 files changed, 194 insertions(+), 81 deletions(-) create mode 100644 test/test_meson_python.py create mode 100644 test/test_projects/meson.py create mode 100644 test/test_projects/setuptools.py diff --git a/bin/run_example_ci_configs.py b/bin/run_example_ci_configs.py index c01e2fad..22fb94e5 100755 --- a/bin/run_example_ci_configs.py +++ b/bin/run_example_ci_configs.py @@ -16,6 +16,8 @@ import click DIR = Path(__file__).parent.parent.resolve() +BuildBackend = typing.Literal["setuptools", "meson"] + def shell(cmd: str, *, check: bool, **kwargs: object) -> subprocess.CompletedProcess[str]: return subprocess.run([cmd], shell=True, check=check, **kwargs) # type: ignore[call-overload, no-any-return] @@ -27,11 +29,17 @@ def git_repo_has_changes() -> bool: return unstaged_changes or staged_changes -def generate_basic_project(path: Path) -> None: +def generate_project(path: Path, build_backend: BuildBackend) -> None: sys.path.insert(0, "") - from test.test_projects.c import new_c_project # noqa: PLC0415 + match build_backend: + case "meson": + from test.test_projects.meson import new_meson_project as new_project # noqa: PLC0415 + case "setuptools": + from test.test_projects.setuptools import new_c_project as new_project # noqa: PLC0415 + case _: + typing.assert_never(build_backend) - project = new_c_project() + project = new_project() project.generate(path) @@ -126,8 +134,9 @@ def ci_service_for_config_file(config_file: Path) -> CIService: @click.command() @click.argument("config_files", nargs=-1, type=click.Path()) +@click.option("--build-backend", type=click.Choice(["setuptools", "meson"]), default="setuptools") def run_example_ci_configs( - config_files: list[str], + config_files: list[str], build_backend: BuildBackend = "setuptools" ) -> None: """ Test the example configs. If no files are specified, will test @@ -166,7 +175,7 @@ def run_example_ci_configs( shell(f"git checkout --orphan {branch_name}", check=True) example_project = Path("example_root") - generate_basic_project(example_project) + generate_project(example_project, build_backend=build_backend) for config_file in config_file_paths: service = ci_service_for_config_file(config_file) diff --git a/docs/faq.md b/docs/faq.md index 81171fe6..949ee717 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -99,6 +99,21 @@ Second, there might be platforms you want to ship for that NumPy (or some other (Note the `*_ONLY_BINARY` variable also supports `":all:"`, and you don't need both that and `*_PREFER_BINARY`, you can use either one, depending on if you want a missing wheel to be a failure or an attempt to build in CI.) +### Building with Meson-Python on Windows + +Meson generally works well with cibuildwheel, but there are a few things to be aware of: + +- On GitHub Actions, the compiler that's chosen by default on Windows is often the MinGW compiler, rather than the MSVC toolchain that Python was compiled with. + + The simplest fix for this is to configure cibuildwheel to pass the `--vsenv` flag to meson, like this: + + ```toml + [tool.cibuildwheel.windows] + config-settings = { "setup-args" = "--vsenv" } + ``` + +- If you need to build 32-bit Windows wheels, you need to activate a 32-bit compiler toolchain before starting cibuildwheel. Many users use [ilammy/msvc-dev-cmd](https://github.com/ilammy/msvc-dev-cmd) for this purpose. + ### Automatic updates using Dependabot {: #automatic-updates} Selecting a moving target (like the latest release) is generally a bad idea in CI. If something breaks, you can't tell whether it was your code or an upstream update that caused the breakage, and in a worst-case scenario, it could occur during a release. diff --git a/test/test_build_frontend_args.py b/test/test_build_frontend_args.py index 83c0a177..8f5732a1 100644 --- a/test/test_build_frontend_args.py +++ b/test/test_build_frontend_args.py @@ -2,8 +2,7 @@ import subprocess import pytest -from . import utils -from .test_projects.c import new_c_project +from . import test_projects, utils @pytest.mark.parametrize( @@ -14,7 +13,7 @@ from .test_projects.c import new_c_project ], ) def test_build_frontend_args(tmp_path, capfd, frontend_name): - project = new_c_project() + project = test_projects.new_c_project() project_dir = tmp_path / "project" project.generate(project_dir) diff --git a/test/test_meson_python.py b/test/test_meson_python.py new file mode 100644 index 00000000..dcfeb2c5 --- /dev/null +++ b/test/test_meson_python.py @@ -0,0 +1,29 @@ +import packaging.utils + +from . import test_projects, utils + +meson_project = test_projects.new_meson_project() + + +def test_meson_python_basic(tmp_path, build_frontend_env): + """Test that cibuildwheel can build a project with meson-python backend.""" + project_dir = tmp_path / "project" + + meson_project.generate(project_dir) + + # build the wheels + actual_wheels = utils.cibuildwheel_run( + project_dir, + add_env=build_frontend_env, + single_python=True, + ) + + # check that the expected wheels are produced + # note that the meson test project doesn't support win32. + is_windows = utils.get_platform() == "windows" + expected_wheels = utils.expected_wheels( + "spam", "0.1.0", single_python=True, single_arch=is_windows + ) + actual_wheels_normalized = {packaging.utils.parse_wheel_filename(w) for w in actual_wheels} + expected_wheels_normalized = {packaging.utils.parse_wheel_filename(w) for w in expected_wheels} + assert actual_wheels_normalized == expected_wheels_normalized diff --git a/test/test_projects/__init__.py b/test/test_projects/__init__.py index d6bed818..b0107b72 100644 --- a/test/test_projects/__init__.py +++ b/test/test_projects/__init__.py @@ -1,4 +1,5 @@ from .base import TestProject -from .c import new_c_project +from .meson import new_meson_project +from .setuptools import new_c_project -__all__ = ("TestProject", "new_c_project") +__all__ = ("TestProject", "new_c_project", "new_meson_project") diff --git a/test/test_projects/c.py b/test/test_projects/c.py index 9643e4cd..6771ccf7 100644 --- a/test/test_projects/c.py +++ b/test/test_projects/c.py @@ -1,7 +1,3 @@ -import jinja2 - -from .base import TestProject - SPAM_C_TEMPLATE = r""" #include @@ -39,70 +35,3 @@ PyMODINIT_FUNC PyInit_spam(void) return PyModule_Create(&moduledef); } """ - -SETUP_PY_TEMPLATE = r""" -import os -import sys - -from setuptools import setup, Extension - -{{ setup_py_add }} - -libraries = [] -# Emscripten fails if you pass -lc... -# See: https://github.com/emscripten-core/emscripten/issues/16680 -if sys.platform.startswith('linux') and "emscripten" not in os.environ.get("_PYTHON_HOST_PLATFORM", ""): - libraries.extend(['m', 'c']) - - -setup( - ext_modules=[Extension( - 'spam', - sources=['spam.c'], - libraries=libraries, - {{ setup_py_extension_args_add | indent(8) }} - )], - {{ setup_py_setup_args_add | indent(4) }} -) -""" - -SETUP_CFG_TEMPLATE = r""" -[metadata] -name = spam -version = 0.1.0 - -{{ setup_cfg_add }} -""" - - -def new_c_project( - *, - spam_c_top_level_add: str = "", - spam_c_function_add: str = "", - setup_py_add: str = "", - setup_py_extension_args_add: str = "", - setup_py_setup_args_add: str = "", - setup_cfg_add: str = "", -) -> TestProject: - project = TestProject() - - project.files.update( - { - "spam.c": jinja2.Template(SPAM_C_TEMPLATE), - "setup.py": jinja2.Template(SETUP_PY_TEMPLATE), - "setup.cfg": jinja2.Template(SETUP_CFG_TEMPLATE), - } - ) - - project.template_context.update( - { - "spam_c_top_level_add": spam_c_top_level_add, - "spam_c_function_add": spam_c_function_add, - "setup_py_add": setup_py_add, - "setup_py_extension_args_add": setup_py_extension_args_add, - "setup_py_setup_args_add": setup_py_setup_args_add, - "setup_cfg_add": setup_cfg_add, - } - ) - - return project diff --git a/test/test_projects/meson.py b/test/test_projects/meson.py new file mode 100644 index 00000000..c3b14e17 --- /dev/null +++ b/test/test_projects/meson.py @@ -0,0 +1,60 @@ +import jinja2 + +from .base import TestProject +from .c import SPAM_C_TEMPLATE + +PYPROJECT_TOML_TEMPLATE = r""" +[build-system] +requires = ["meson-python"] +build-backend = "mesonpy" + +[project] +name = "spam" +version = "0.1.0" + +[tool.cibuildwheel.windows] +config-settings = { "setup-args" = "--vsenv" } +# building win32 wheels on a 64-bit CI machine with meson-python +# requires a few extra steps outside cibuildwheel. See +# https://github.com/pypa/cibuildwheel/pull/2718 +archs = ["auto64"] +""" + +MESON_BUILD_TEMPLATE = r""" +project('spam', 'c', + version: '0.1.0', + default_options: ['warning_level=2'], +) + +py = import('python').find_installation(pure: false) + +py.extension_module('spam', + 'spam.c', + install: true, +) +""" + + +def new_meson_project( + *, + spam_c_top_level_add: str = "", + spam_c_function_add: str = "", +) -> TestProject: + project = TestProject() + + project.files.update( + { + "spam.c": jinja2.Template(SPAM_C_TEMPLATE), + "pyproject.toml": jinja2.Template(PYPROJECT_TOML_TEMPLATE), + "meson.build": jinja2.Template(MESON_BUILD_TEMPLATE), + } + ) + + project.template_context.update( + { + "spam_c_top_level_add": spam_c_top_level_add, + "spam_c_function_add": spam_c_function_add, + } + ) + + return project diff --git a/test/test_projects/setuptools.py b/test/test_projects/setuptools.py new file mode 100644 index 00000000..30b33caf --- /dev/null +++ b/test/test_projects/setuptools.py @@ -0,0 +1,71 @@ +import jinja2 + +from .base import TestProject +from .c import SPAM_C_TEMPLATE + +SETUP_PY_TEMPLATE = r""" +import os +import sys + +from setuptools import setup, Extension + +{{ setup_py_add }} + +libraries = [] +# Emscripten fails if you pass -lc... +# See: https://github.com/emscripten-core/emscripten/issues/16680 +if sys.platform.startswith('linux') and "emscripten" not in os.environ.get("_PYTHON_HOST_PLATFORM", ""): + libraries.extend(['m', 'c']) + + +setup( + ext_modules=[Extension( + 'spam', + sources=['spam.c'], + libraries=libraries, + {{ setup_py_extension_args_add | indent(8) }} + )], + {{ setup_py_setup_args_add | indent(4) }} +) +""" + +SETUP_CFG_TEMPLATE = r""" +[metadata] +name = spam +version = 0.1.0 + +{{ setup_cfg_add }} +""" + + +def new_c_project( + *, + spam_c_top_level_add: str = "", + spam_c_function_add: str = "", + setup_py_add: str = "", + setup_py_extension_args_add: str = "", + setup_py_setup_args_add: str = "", + setup_cfg_add: str = "", +) -> TestProject: + project = TestProject() + + project.files.update( + { + "spam.c": jinja2.Template(SPAM_C_TEMPLATE), + "setup.py": jinja2.Template(SETUP_PY_TEMPLATE), + "setup.cfg": jinja2.Template(SETUP_CFG_TEMPLATE), + } + ) + + project.template_context.update( + { + "spam_c_top_level_add": spam_c_top_level_add, + "spam_c_function_add": spam_c_function_add, + "setup_py_add": setup_py_add, + "setup_py_extension_args_add": setup_py_extension_args_add, + "setup_py_setup_args_add": setup_py_setup_args_add, + "setup_cfg_add": setup_cfg_add, + } + ) + + return project