Skip to content

Commit f380f9c

Browse files
authored
Meson Plugin + CMakeConfigDeps (synodic#135)
1 parent f97d2ce commit f380f9c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2159
-700
lines changed

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# AGENTS.md
2+
3+
This repository doesn't contain any agent specific instructions other than its README.md and its linked resources.

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,35 @@
11
# CPPython
2-
A Python management solution for C++ dependencies
2+
3+
A transparent Python management solution for C++ dependencies and building.
4+
5+
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
6+
[![PyPI version](https://img.shields.io/pypi/v/cppython.svg)](https://pypi.org/project/cppython/)
7+
8+
## Goals
9+
10+
1. **CLI** — Provide imperative commands (`build`, `test`, `bench`, `run`, `install`) for managing C++ projects within a Python ecosystem.
11+
2. **Plugin Architecture** — Support pluggable generators (CMake, Meson) and providers (Conan, vcpkg) so users can mix and match toolchains.
12+
3. **PEP 517 Build Backend** — Act as a transparent build backend that delegates to scikit-build-core or meson-python after ensuring C++ dependencies are in place.
13+
4. **Package Manager Integration** — Integrate with Python package managers so that `<manager> install` seamlessly handles C++ dependency installation alongside Python dependencies.
14+
15+
## Features
16+
17+
## Setup
18+
19+
See [Setup](https://synodic.github.io/cppython/setup) for setup instructions.
20+
21+
## Development
22+
23+
We use [pdm](https://pdm-project.org/en/latest/) as our build system and package manager. Scripts for development tasks are defined in `pyproject.toml` under the `[tool.pdm.scripts]` section.
24+
25+
See [Development](https://synodic.github.io/cppython/development) for additional build, test, and installation instructions.
26+
27+
For contribution guidelines, see [CONTRIBUTING.md](https://github.com/synodic/.github/blob/stable/CONTRIBUTING.md).
28+
29+
## Documentation
30+
31+
## License
32+
33+
This project is licensed under the MIT License — see [LICENSE.md](LICENSE.md) for details.
34+
35+
Copyright © 2026 Synodic Software

cppython/build/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
"""CPPython build backend wrapping scikit-build-core.
1+
"""CPPython build backend wrapping scikit-build-core and meson-python.
22
3-
This module provides PEP 517/518 build backend hooks that wrap scikit-build-core,
4-
automatically running CPPython's provider workflow before building
5-
to inject the generated toolchain file into the CMake configuration.
3+
This module provides PEP 517/518 build backend hooks that wrap scikit-build-core
4+
or meson-python depending on the active generator, automatically running
5+
CPPython's provider workflow before building to inject the generated
6+
toolchain or native/cross files into the build configuration.
67
78
Usage in pyproject.toml:
89
[build-system]

cppython/build/backend.py

Lines changed: 134 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,65 @@
1-
"""PEP 517 build backend implementation wrapping scikit-build-core.
1+
"""PEP 517 build backend implementation wrapping scikit-build-core and meson-python.
22
3-
This module provides the actual build hooks that delegate to scikit-build-core
3+
This module provides the actual build hooks that delegate to the appropriate
4+
underlying build backend (scikit-build-core for CMake, meson-python for Meson)
45
after running CPPython's preparation workflow.
56
"""
67

78
import logging
9+
import tomllib
810
from pathlib import Path
11+
from types import ModuleType
912
from typing import Any
1013

14+
import mesonpy
1115
from scikit_build_core import build as skbuild
1216

13-
from cppython.build.prepare import prepare_build
17+
from cppython.build.prepare import BuildPreparationResult, prepare_build
18+
from cppython.plugins.cmake.schema import CMakeSyncData
19+
from cppython.plugins.meson.schema import MesonSyncData
1420

1521
logger = logging.getLogger('cppython.build')
1622

1723

18-
def _inject_toolchain(config_settings: dict[str, Any] | None, toolchain_file: Path | None) -> dict[str, Any]:
24+
def _is_meson_project() -> bool:
25+
"""Detect if the current project uses Meson by checking pyproject.toml.
26+
27+
Looks for ``[tool.cppython.generator]`` containing "meson" or the
28+
presence of a ``meson.build`` file in the source directory.
29+
30+
Returns:
31+
True if the project appears to be Meson-based
32+
"""
33+
source_dir = Path.cwd()
34+
35+
# Check pyproject.toml for cppython generator configuration
36+
pyproject_path = source_dir / 'pyproject.toml'
37+
if pyproject_path.exists():
38+
with open(pyproject_path, 'rb') as f:
39+
data = tomllib.load(f)
40+
generator = data.get('tool', {}).get('cppython', {}).get('generator', '')
41+
if isinstance(generator, str) and 'meson' in generator.lower():
42+
return True
43+
44+
# Fallback: check for meson.build file
45+
return (source_dir / 'meson.build').exists()
46+
47+
48+
def _get_backend(is_meson: bool) -> ModuleType:
49+
"""Get the appropriate backend module.
50+
51+
Args:
52+
is_meson: Whether to use meson-python instead of scikit-build-core
53+
54+
Returns:
55+
The backend module (mesonpy or scikit_build_core.build)
56+
"""
57+
if is_meson:
58+
return mesonpy
59+
return skbuild
60+
61+
62+
def _inject_cmake_toolchain(config_settings: dict[str, Any] | None, toolchain_file: Path | None) -> dict[str, Any]:
1963
"""Inject the toolchain file into config settings for scikit-build-core.
2064
2165
Args:
@@ -49,175 +93,154 @@ def _inject_toolchain(config_settings: dict[str, Any] | None, toolchain_file: Pa
4993
return settings
5094

5195

52-
def _prepare_and_get_settings(
96+
def _inject_meson_files(
5397
config_settings: dict[str, Any] | None,
98+
native_file: Path | None,
99+
cross_file: Path | None,
54100
) -> dict[str, Any]:
55-
"""Run CPPython preparation and merge toolchain into config settings.
101+
"""Inject native/cross files into config settings for meson-python.
102+
103+
Args:
104+
config_settings: The original config settings (may be None)
105+
native_file: Path to the Meson native file to inject
106+
cross_file: Path to the Meson cross file to inject
107+
108+
Returns:
109+
Updated config settings with Meson files injected
110+
"""
111+
settings = dict(config_settings) if config_settings else {}
112+
113+
setup_args_key = 'setup-args'
114+
existing_args = settings.get(setup_args_key, '')
115+
116+
args_to_add: list[str] = []
117+
118+
if native_file and native_file.exists():
119+
native_arg = f'--native-file={native_file.absolute()}'
120+
if '--native-file' not in existing_args:
121+
args_to_add.append(native_arg)
122+
logger.info('CPPython: Injected --native-file=%s', native_file)
123+
else:
124+
logger.info('CPPython: User-specified native file takes precedence')
125+
126+
if cross_file and cross_file.exists():
127+
cross_arg = f'--cross-file={cross_file.absolute()}'
128+
if '--cross-file' not in existing_args:
129+
args_to_add.append(cross_arg)
130+
logger.info('CPPython: Injected --cross-file=%s', cross_file)
131+
else:
132+
logger.info('CPPython: User-specified cross file takes precedence')
133+
134+
if args_to_add:
135+
if existing_args:
136+
settings[setup_args_key] = f'{existing_args};' + ';'.join(args_to_add)
137+
else:
138+
settings[setup_args_key] = ';'.join(args_to_add)
139+
140+
return settings
141+
142+
143+
def _prepare_and_get_result(
144+
config_settings: dict[str, Any] | None,
145+
) -> tuple[BuildPreparationResult, dict[str, Any]]:
146+
"""Run CPPython preparation and merge config into settings.
56147
57148
Args:
58149
config_settings: The original config settings
59150
60151
Returns:
61-
Config settings with CPPython toolchain injected
152+
Tuple of (preparation result, updated config settings)
62153
"""
63154
# Determine source directory (current working directory during build)
64155
source_dir = Path.cwd()
65156

66157
# Run CPPython preparation
67-
toolchain_file = prepare_build(source_dir)
158+
result = prepare_build(source_dir)
68159

69-
# Inject toolchain into config settings
70-
return _inject_toolchain(config_settings, toolchain_file)
160+
# Inject settings based on sync data type
161+
settings = dict(config_settings) if config_settings else {}
71162

163+
if result.sync_data is not None:
164+
if isinstance(result.sync_data, CMakeSyncData):
165+
settings = _inject_cmake_toolchain(settings, result.sync_data.toolchain_file)
166+
elif isinstance(result.sync_data, MesonSyncData):
167+
settings = _inject_meson_files(settings, result.sync_data.native_file, result.sync_data.cross_file)
72168

73-
# PEP 517 Hooks - delegating to scikit-build-core after preparation
169+
return result, settings
74170

75171

76-
def get_requires_for_build_wheel(
77-
config_settings: dict[str, Any] | None = None,
78-
) -> list[str]:
79-
"""Get additional requirements for building a wheel.
172+
def _is_meson_build(result: BuildPreparationResult) -> bool:
173+
"""Determine if the build should use meson-python based on sync data.
80174
81175
Args:
82-
config_settings: Build configuration settings
176+
result: The build preparation result
83177
84178
Returns:
85-
List of additional requirements
179+
True if meson-python should be used, False for scikit-build-core
86180
"""
87-
return skbuild.get_requires_for_build_wheel(config_settings)
181+
return isinstance(result.sync_data, MesonSyncData)
88182

89183

90-
def get_requires_for_build_sdist(
91-
config_settings: dict[str, Any] | None = None,
92-
) -> list[str]:
93-
"""Get additional requirements for building an sdist.
184+
# PEP 517 Hooks - dispatching to the appropriate backend after preparation
94185

95-
Args:
96-
config_settings: Build configuration settings
97186

98-
Returns:
99-
List of additional requirements
100-
"""
101-
return skbuild.get_requires_for_build_sdist(config_settings)
187+
def get_requires_for_build_wheel(config_settings: dict[str, Any] | None = None) -> list[str]:
188+
"""Get additional requirements for building a wheel."""
189+
return _get_backend(_is_meson_project()).get_requires_for_build_wheel(config_settings)
102190

103191

104-
def get_requires_for_build_editable(
105-
config_settings: dict[str, Any] | None = None,
106-
) -> list[str]:
107-
"""Get additional requirements for building an editable install.
192+
def get_requires_for_build_sdist(config_settings: dict[str, Any] | None = None) -> list[str]:
193+
"""Get additional requirements for building an sdist."""
194+
return _get_backend(_is_meson_project()).get_requires_for_build_sdist(config_settings)
108195

109-
Args:
110-
config_settings: Build configuration settings
111196

112-
Returns:
113-
List of additional requirements
114-
"""
115-
return skbuild.get_requires_for_build_editable(config_settings)
197+
def get_requires_for_build_editable(config_settings: dict[str, Any] | None = None) -> list[str]:
198+
"""Get additional requirements for building an editable install."""
199+
return _get_backend(_is_meson_project()).get_requires_for_build_editable(config_settings)
116200

117201

118202
def build_wheel(
119203
wheel_directory: str,
120204
config_settings: dict[str, Any] | None = None,
121205
metadata_directory: str | None = None,
122206
) -> str:
123-
"""Build a wheel from the source distribution.
124-
125-
This runs CPPython's provider workflow first to ensure C++ dependencies
126-
are installed and the toolchain file is generated, then delegates to
127-
scikit-build-core for the actual wheel build.
128-
129-
Args:
130-
wheel_directory: Directory to place the built wheel
131-
config_settings: Build configuration settings
132-
metadata_directory: Directory containing wheel metadata
133-
134-
Returns:
135-
The basename of the built wheel
136-
"""
207+
"""Build a wheel, running CPPython preparation first."""
137208
logger.info('CPPython: Starting wheel build')
138-
139-
# Prepare CPPython and get updated settings
140-
settings = _prepare_and_get_settings(config_settings)
141-
142-
# Delegate to scikit-build-core
143-
return skbuild.build_wheel(wheel_directory, settings, metadata_directory)
209+
result, settings = _prepare_and_get_result(config_settings)
210+
return _get_backend(_is_meson_build(result)).build_wheel(wheel_directory, settings, metadata_directory)
144211

145212

146213
def build_sdist(
147214
sdist_directory: str,
148215
config_settings: dict[str, Any] | None = None,
149216
) -> str:
150-
"""Build a source distribution.
151-
152-
For sdist, we don't run the full CPPython workflow since the C++ dependencies
153-
should be resolved at wheel build time, not sdist creation time.
154-
155-
Args:
156-
sdist_directory: Directory to place the built sdist
157-
config_settings: Build configuration settings
158-
159-
Returns:
160-
The basename of the built sdist
161-
"""
217+
"""Build a source distribution (no CPPython workflow needed)."""
162218
logger.info('CPPython: Starting sdist build')
163-
164-
# Delegate directly to scikit-build-core (no preparation needed for sdist)
165-
return skbuild.build_sdist(sdist_directory, config_settings)
219+
return _get_backend(_is_meson_project()).build_sdist(sdist_directory, config_settings)
166220

167221

168222
def build_editable(
169223
wheel_directory: str,
170224
config_settings: dict[str, Any] | None = None,
171225
metadata_directory: str | None = None,
172226
) -> str:
173-
"""Build an editable wheel.
174-
175-
This runs CPPython's provider workflow first, similar to build_wheel.
176-
177-
Args:
178-
wheel_directory: Directory to place the built wheel
179-
config_settings: Build configuration settings
180-
metadata_directory: Directory containing wheel metadata
181-
182-
Returns:
183-
The basename of the built wheel
184-
"""
227+
"""Build an editable wheel, running CPPython preparation first."""
185228
logger.info('CPPython: Starting editable build')
186-
187-
# Prepare CPPython and get updated settings
188-
settings = _prepare_and_get_settings(config_settings)
189-
190-
# Delegate to scikit-build-core
191-
return skbuild.build_editable(wheel_directory, settings, metadata_directory)
229+
result, settings = _prepare_and_get_result(config_settings)
230+
return _get_backend(_is_meson_build(result)).build_editable(wheel_directory, settings, metadata_directory)
192231

193232

194233
def prepare_metadata_for_build_wheel(
195234
metadata_directory: str,
196235
config_settings: dict[str, Any] | None = None,
197236
) -> str:
198-
"""Prepare metadata for wheel build.
199-
200-
Args:
201-
metadata_directory: Directory to place the metadata
202-
config_settings: Build configuration settings
203-
204-
Returns:
205-
The basename of the metadata directory
206-
"""
207-
return skbuild.prepare_metadata_for_build_wheel(metadata_directory, config_settings)
237+
"""Prepare metadata for wheel build."""
238+
return _get_backend(_is_meson_project()).prepare_metadata_for_build_wheel(metadata_directory, config_settings)
208239

209240

210241
def prepare_metadata_for_build_editable(
211242
metadata_directory: str,
212243
config_settings: dict[str, Any] | None = None,
213244
) -> str:
214-
"""Prepare metadata for editable build.
215-
216-
Args:
217-
metadata_directory: Directory to place the metadata
218-
config_settings: Build configuration settings
219-
220-
Returns:
221-
The basename of the metadata directory
222-
"""
223-
return skbuild.prepare_metadata_for_build_editable(metadata_directory, config_settings)
245+
"""Prepare metadata for editable build."""
246+
return _get_backend(_is_meson_project()).prepare_metadata_for_build_editable(metadata_directory, config_settings)

0 commit comments

Comments
 (0)