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 <HenrySchreinerIII@gmail.com>

---------

Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
This commit is contained in:
Joe Rickerby
2026-02-08 10:36:47 +00:00
committed by GitHub
co-authored by Henry Schreiner
parent 0d2547bf6f
commit a2f965f62f
8 changed files with 194 additions and 81 deletions
+14 -5
View File
@@ -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)
+15
View File
@@ -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.
+2 -3
View File
@@ -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)
+29
View File
@@ -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
+3 -2
View File
@@ -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")
-71
View File
@@ -1,7 +1,3 @@
import jinja2
from .base import TestProject
SPAM_C_TEMPLATE = r"""
#include <Python.h>
@@ -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
+60
View File
@@ -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
+71
View File
@@ -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