From f78eb39b75d851c8f10808f2967f9f32a631788f Mon Sep 17 00:00:00 2001 From: "Leandro G. Almeida" Date: Mon, 1 Jun 2026 09:26:00 -0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20template:=20package=20C++=20Python?= =?UTF-8?q?=20bindings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/template-structure.md | 2 +- .../cpp_template/template/.gitignore.jinja | 1 + .../cpp_template/template/Makefile.jinja | 18 +++++++++++++++-- .../cpp_template/template/README.md.jinja | 6 ++++++ .../docs/development/build-and-test.md.jinja | 6 ++++++ .../cpp_template/template/meson.build.jinja | 3 +++ .../template/pyproject.toml.jinja | 20 +++++++++++++++++-- tests/test_template/test_cpp_template.py | 13 +++++++++++- 8 files changed, 63 insertions(+), 6 deletions(-) diff --git a/docs/template-structure.md b/docs/template-structure.md index 60a402f..13889b2 100644 --- a/docs/template-structure.md +++ b/docs/template-structure.md @@ -9,7 +9,7 @@ - **Docs-only:** If `docs_only` is true, source package and test files are omitted. - **Notebooks:** If `python_notebooks` is true, the template generates a `notebooks/` folder. - **Paper:** If `latex_paper` is true, the template generates a `paper/` folder with a LaTeX manuscript scaffold and Make targets. -- **C++:** The bundled `cpp_template` is a separate Meson-based template with `include/`, `src/`, `tests/`, `examples/`, optional pybind11 Python bindings, Doxygen hooks, and the same repoman Make target names used by CI. +- **C++:** The bundled `cpp_template` is a separate Meson-based template with `include/`, `src/`, `tests/`, `examples/`, optional pybind11 Python bindings, meson-python wheel packaging, Doxygen hooks, and the same repoman Make target names used by CI. ## High-level generated layout diff --git a/src/repoman/cpp_template/template/.gitignore.jinja b/src/repoman/cpp_template/template/.gitignore.jinja index a7f1249..16c5bec 100644 --- a/src/repoman/cpp_template/template/.gitignore.jinja +++ b/src/repoman/cpp_template/template/.gitignore.jinja @@ -18,6 +18,7 @@ # docs and python tooling .venv/ +.wheel-venv/ .venvs/ uv.lock *.egg-info/ diff --git a/src/repoman/cpp_template/template/Makefile.jinja b/src/repoman/cpp_template/template/Makefile.jinja index 37fbd36..932cd7c 100644 --- a/src/repoman/cpp_template/template/Makefile.jinja +++ b/src/repoman/cpp_template/template/Makefile.jinja @@ -3,16 +3,18 @@ SHELL := bash version := 0.1.0 BUILD_DIR ?= build +DIST_DIR ?= dist IMAGE ?= {{ project_name }}-dev DOCS_CONFIG ?= config/mkdocs.yml MESON ?= uv run meson NINJA ?= uv run ninja CLANG_FORMAT ?= clang-format DOXYGEN ?= doxygen +WHEEL_VENV ?= .wheel-venv cxx.format.files := $(shell find include src tests examples -type f \( -name "*.hpp" -o -name "*.cpp" \) 2>/dev/null || :) -.PHONY: setup configure build test clean distclean format format-check lint check-types type-check check-quality check docs docs-serve docs-check check-docs docs-api docs-check-api docker-build docker-shell help +.PHONY: setup configure build test clean distclean format format-check lint check-types type-check check-quality check build-wheel check-wheel docs docs-serve docs-check check-docs docs-api docs-check-api docker-build docker-shell help setup: ## Install Python-hosted developer tooling. @$(call i, Installing developer tooling) @@ -34,7 +36,7 @@ clean: ## Clean compiled outputs while keeping the Meson build directory. @if [ -d "$(BUILD_DIR)" ]; then $(NINJA) -C "$(BUILD_DIR)" clean; fi distclean: ## Remove all generated build and docs outputs. - rm -rf "$(BUILD_DIR)" site + rm -rf "$(BUILD_DIR)" "$(DIST_DIR)" "$(WHEEL_VENV)" site format: ## Format C++ sources with clang-format. @$(call i, Formatting C++ sources) @@ -55,6 +57,18 @@ check-quality: format-check lint ## Run formatting and compiler-warning checks. check: check-quality check-types ## Run all quality checks. +build-wheel: ## Build a Python wheel for the pybind11 bindings. + @$(call i, Building Python wheel) + uv build --wheel --out-dir "$(DIST_DIR)" + +check-wheel: ## Build, install, and import-check the Python wheel in a clean venv. + @$(call i, Checking Python wheel) + rm -rf "$(WHEEL_VENV)" "$(DIST_DIR)" + python_version="$$(uv run python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"; uv venv --python "$$python_version" "$(WHEEL_VENV)" + uv build --wheel --python "$(WHEEL_VENV)/bin/python" --out-dir "$(DIST_DIR)" + wheel_path="$$(find "$(DIST_DIR)" -maxdepth 1 -type f -name "*.whl" | sort | tail -n 1)"; uv pip install --python "$(WHEEL_VENV)/bin/python" "$$wheel_path" + "$(WHEEL_VENV)/bin/python" -c "import {{ cpp_namespace }}; assert {{ cpp_namespace }}.add(2, 3) == 5; assert {{ cpp_namespace }}.__version__ == '0.1.0'" + docs-api: ## Generate Doxygen API documentation. @$(call i, Generating Doxygen documentation) $(DOXYGEN) Doxyfile diff --git a/src/repoman/cpp_template/template/README.md.jinja b/src/repoman/cpp_template/template/README.md.jinja index 5bf77a0..8057935 100644 --- a/src/repoman/cpp_template/template/README.md.jinja +++ b/src/repoman/cpp_template/template/README.md.jinja @@ -53,6 +53,12 @@ Disable them with: uv run meson configure build -Dbuild_python_bindings=false ``` +Build and validate a local wheel for the Python bindings: + +```bash +make check-wheel +``` + ## Layout ```text diff --git a/src/repoman/cpp_template/template/docs/development/build-and-test.md.jinja b/src/repoman/cpp_template/template/docs/development/build-and-test.md.jinja index 4e619f5..c2a4279 100644 --- a/src/repoman/cpp_template/template/docs/development/build-and-test.md.jinja +++ b/src/repoman/cpp_template/template/docs/development/build-and-test.md.jinja @@ -30,3 +30,9 @@ Run all local checks: ```bash make check ``` + +Build and validate the Python wheel in a clean temporary environment: + +```bash +make check-wheel +``` diff --git a/src/repoman/cpp_template/template/meson.build.jinja b/src/repoman/cpp_template/template/meson.build.jinja index dc91471..469382b 100644 --- a/src/repoman/cpp_template/template/meson.build.jinja +++ b/src/repoman/cpp_template/template/meson.build.jinja @@ -26,6 +26,7 @@ public_include = include_directories('include') {{ cpp_library_name }}_sources, include_directories: public_include, install: true, + install_tag: 'runtime', ) {{ cpp_library_name }}_dep = declare_dependency( @@ -39,6 +40,7 @@ if get_option('build_cli') 'src/cli.cpp', dependencies: [{{ cpp_library_name }}_dep], install: true, + install_tag: 'bin', ) endif @@ -54,6 +56,7 @@ if get_option('build_python_bindings') pybind11_dep, ], install: true, + install_tag: 'python-runtime', ) endif diff --git a/src/repoman/cpp_template/template/pyproject.toml.jinja b/src/repoman/cpp_template/template/pyproject.toml.jinja index b14dbdc..77ed427 100644 --- a/src/repoman/cpp_template/template/pyproject.toml.jinja +++ b/src/repoman/cpp_template/template/pyproject.toml.jinja @@ -1,7 +1,19 @@ +[build-system] +build-backend = "mesonpy" +requires = [ + "meson>=1.5.0", + "meson-python>=0.17.0", + "ninja>=1.11.1", + "pybind11>=2.13.6", +] + [project] -name = "{{ project_name }}-docs" +name = "{{ project_name }}" version = "0.1.0" -description = "Documentation and developer tooling for {{ project_name }}" +description = "{{ project_description }}" +authors = [{name = "{{ author_fullname }}", email = "{{ author_email }}"}] +license = "{{ copyright_license }}" +license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.11" dependencies = [] @@ -10,9 +22,13 @@ dependencies = [] package = false default-groups = ["ci", "docs"] +[tool.meson-python.args] +install = ["--tags=runtime,python-runtime"] + [dependency-groups] ci = [ "meson>=1.5.0", + "meson-python>=0.17.0", "ninja>=1.11.1", "pybind11>=2.13.6", ] diff --git a/tests/test_template/test_cpp_template.py b/tests/test_template/test_cpp_template.py index 545da1d..d7b3026 100644 --- a/tests/test_template/test_cpp_template.py +++ b/tests/test_template/test_cpp_template.py @@ -64,15 +64,26 @@ def test_cpp_template_renders_spinach_style_structure(tmp_path: Path) -> None: assert "'cpp_std=c++17'" in meson assert "python.extension_module(" in meson assert "dependency('pybind11')" in meson + assert "install_tag: 'runtime'" in meson + assert "install_tag: 'bin'" in meson + assert "install_tag: 'python-runtime'" in meson assert "option('build_python_bindings'" in meson_options + assert 'build-backend = "mesonpy"' in pyproject + assert 'name = "sample-cpp"' in pyproject + assert 'description = "Sample C++ library"' in pyproject + assert '"meson-python>=0.17.0"' in pyproject assert '"pybind11>=2.13.6"' in pyproject + assert 'install = ["--tags=runtime,python-runtime"]' in pyproject assert 'module.attr("__version__") = sample_cpp::version();' in python_bindings - assert 'module.add(2, 3) == 5' in python_bindings_test + assert "module.add(2, 3) == 5" in python_bindings_test assert 'module.version() == "0.1.0"' in python_bindings_test assert 'module.__version__ == "0.1.0"' in python_bindings_test assert "assert module.__doc__" in python_bindings_test assert "except TypeError:" in python_bindings_test assert "PYTHONPATH=build uv run python" in readme + assert "make check-wheel" in readme + assert "uv build --wheel" in makefile + assert "uv pip install --python" in makefile assert "make check-docs" in github_ci assert "make check" in github_ci assert "make test" in github_ci