Skip to content

Commit 0285618

Browse files
committed
Cleanup API Layers
1 parent 6c3d8b3 commit 0285618

File tree

21 files changed

+427
-572
lines changed

21 files changed

+427
-572
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ A transparent Python management solution for C++ dependencies and building.
55
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
66
[![PyPI version](https://img.shields.io/pypi/v/cppython.svg)](https://pypi.org/project/cppython/)
77

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+
815
## Features
916

1017
## Setup

cppython/build/backend.py

Lines changed: 35 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
import logging
99
import tomllib
1010
from pathlib import Path
11+
from types import ModuleType
1112
from typing import Any
1213

14+
import mesonpy
1315
from scikit_build_core import build as skbuild
1416

1517
from cppython.build.prepare import BuildPreparationResult, prepare_build
@@ -40,10 +42,21 @@ def _is_meson_project() -> bool:
4042
return True
4143

4244
# Fallback: check for meson.build file
43-
if (source_dir / 'meson.build').exists():
44-
return True
45+
return (source_dir / 'meson.build').exists()
4546

46-
return False
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
4760

4861

4962
def _inject_cmake_toolchain(config_settings: dict[str, Any] | None, toolchain_file: Path | None) -> dict[str, Any]:
@@ -171,185 +184,63 @@ def _is_meson_build(result: BuildPreparationResult) -> bool:
171184
# PEP 517 Hooks - dispatching to the appropriate backend after preparation
172185

173186

174-
def get_requires_for_build_wheel(
175-
config_settings: dict[str, Any] | None = None,
176-
) -> list[str]:
177-
"""Get additional requirements for building a wheel.
178-
179-
Args:
180-
config_settings: Build configuration settings
181-
182-
Returns:
183-
List of additional requirements
184-
"""
185-
if _is_meson_project():
186-
import mesonpy
187-
188-
return mesonpy.get_requires_for_build_wheel(config_settings)
189-
return skbuild.get_requires_for_build_wheel(config_settings)
190-
191-
192-
def get_requires_for_build_sdist(
193-
config_settings: dict[str, Any] | None = None,
194-
) -> list[str]:
195-
"""Get additional requirements for building an sdist.
196-
197-
Args:
198-
config_settings: Build configuration settings
199-
200-
Returns:
201-
List of additional requirements
202-
"""
203-
if _is_meson_project():
204-
import mesonpy
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)
205190

206-
return mesonpy.get_requires_for_build_sdist(config_settings)
207-
return skbuild.get_requires_for_build_sdist(config_settings)
208191

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)
209195

210-
def get_requires_for_build_editable(
211-
config_settings: dict[str, Any] | None = None,
212-
) -> list[str]:
213-
"""Get additional requirements for building an editable install.
214196

215-
Args:
216-
config_settings: Build configuration settings
217-
218-
Returns:
219-
List of additional requirements
220-
"""
221-
if _is_meson_project():
222-
import mesonpy
223-
224-
return mesonpy.get_requires_for_build_editable(config_settings)
225-
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)
226200

227201

228202
def build_wheel(
229203
wheel_directory: str,
230204
config_settings: dict[str, Any] | None = None,
231205
metadata_directory: str | None = None,
232206
) -> str:
233-
"""Build a wheel from the source distribution.
234-
235-
This runs CPPython's provider workflow first to ensure C++ dependencies
236-
are installed, then delegates to the appropriate build backend
237-
(scikit-build-core for CMake, meson-python for Meson).
238-
239-
Args:
240-
wheel_directory: Directory to place the built wheel
241-
config_settings: Build configuration settings
242-
metadata_directory: Directory containing wheel metadata
243-
244-
Returns:
245-
The basename of the built wheel
246-
"""
207+
"""Build a wheel, running CPPython preparation first."""
247208
logger.info('CPPython: Starting wheel build')
248-
249-
# Prepare CPPython and get updated settings
250209
result, settings = _prepare_and_get_result(config_settings)
251-
252-
# Delegate to the appropriate backend
253-
if _is_meson_build(result):
254-
import mesonpy
255-
256-
return mesonpy.build_wheel(wheel_directory, settings, metadata_directory)
257-
258-
return skbuild.build_wheel(wheel_directory, settings, metadata_directory)
210+
return _get_backend(_is_meson_build(result)).build_wheel(wheel_directory, settings, metadata_directory)
259211

260212

261213
def build_sdist(
262214
sdist_directory: str,
263215
config_settings: dict[str, Any] | None = None,
264216
) -> str:
265-
"""Build a source distribution.
266-
267-
For sdist, we don't run the full CPPython workflow since the C++ dependencies
268-
should be resolved at wheel build time, not sdist creation time.
269-
270-
Args:
271-
sdist_directory: Directory to place the built sdist
272-
config_settings: Build configuration settings
273-
274-
Returns:
275-
The basename of the built sdist
276-
"""
217+
"""Build a source distribution (no CPPython workflow needed)."""
277218
logger.info('CPPython: Starting sdist build')
278-
279-
# Delegate to the appropriate backend
280-
if _is_meson_project():
281-
import mesonpy
282-
283-
return mesonpy.build_sdist(sdist_directory, config_settings)
284-
return skbuild.build_sdist(sdist_directory, config_settings)
219+
return _get_backend(_is_meson_project()).build_sdist(sdist_directory, config_settings)
285220

286221

287222
def build_editable(
288223
wheel_directory: str,
289224
config_settings: dict[str, Any] | None = None,
290225
metadata_directory: str | None = None,
291226
) -> str:
292-
"""Build an editable wheel.
293-
294-
This runs CPPython's provider workflow first, similar to build_wheel.
295-
296-
Args:
297-
wheel_directory: Directory to place the built wheel
298-
config_settings: Build configuration settings
299-
metadata_directory: Directory containing wheel metadata
300-
301-
Returns:
302-
The basename of the built wheel
303-
"""
227+
"""Build an editable wheel, running CPPython preparation first."""
304228
logger.info('CPPython: Starting editable build')
305-
306-
# Prepare CPPython and get updated settings
307229
result, settings = _prepare_and_get_result(config_settings)
308-
309-
# Delegate to the appropriate backend
310-
if _is_meson_build(result):
311-
import mesonpy
312-
313-
return mesonpy.build_editable(wheel_directory, settings, metadata_directory)
314-
315-
return skbuild.build_editable(wheel_directory, settings, metadata_directory)
230+
return _get_backend(_is_meson_build(result)).build_editable(wheel_directory, settings, metadata_directory)
316231

317232

318233
def prepare_metadata_for_build_wheel(
319234
metadata_directory: str,
320235
config_settings: dict[str, Any] | None = None,
321236
) -> str:
322-
"""Prepare metadata for wheel build.
323-
324-
Args:
325-
metadata_directory: Directory to place the metadata
326-
config_settings: Build configuration settings
327-
328-
Returns:
329-
The basename of the metadata directory
330-
"""
331-
if _is_meson_project():
332-
import mesonpy
333-
334-
return mesonpy.prepare_metadata_for_build_wheel(metadata_directory, config_settings)
335-
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)
336239

337240

338241
def prepare_metadata_for_build_editable(
339242
metadata_directory: str,
340243
config_settings: dict[str, Any] | None = None,
341244
) -> str:
342-
"""Prepare metadata for editable build.
343-
344-
Args:
345-
metadata_directory: Directory to place the metadata
346-
config_settings: Build configuration settings
347-
348-
Returns:
349-
The basename of the metadata directory
350-
"""
351-
if _is_meson_project():
352-
import mesonpy
353-
354-
return mesonpy.prepare_metadata_for_build_editable(metadata_directory, config_settings)
355-
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)

cppython/build/prepare.py

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
from pathlib import Path
1212
from typing import Any
1313

14-
from cppython.core.schema import Interface, ProjectConfiguration, SyncData
14+
from cppython.core.interface import NoOpInterface
15+
from cppython.core.schema import ProjectConfiguration, SyncData
1516
from cppython.project import Project
17+
from cppython.utility.exception import InstallationVerificationError
1618

1719

1820
@dataclass
@@ -27,17 +29,8 @@ class BuildPreparationResult:
2729
sync_data: SyncData | None = None
2830

2931

30-
class BuildInterface(Interface):
31-
"""Minimal interface implementation for build backend usage."""
32-
33-
def write_pyproject(self) -> None:
34-
"""No-op for build backend - we don't modify pyproject.toml during builds."""
35-
36-
def write_configuration(self) -> None:
37-
"""No-op for build backend - we don't modify configuration during builds."""
38-
39-
def write_user_configuration(self) -> None:
40-
"""No-op for build backend - we don't modify user configuration during builds."""
32+
BuildInterface = NoOpInterface
33+
"""Interface implementation for the build backend (no-op write-backs)."""
4134

4235

4336
class BuildPreparation:
@@ -68,32 +61,18 @@ def _load_pyproject(self) -> dict[str, Any]:
6861
with open(pyproject_path, 'rb') as f:
6962
return tomllib.load(f)
7063

71-
def _get_sync_data(self, project: Project) -> SyncData | None:
72-
"""Extract sync data from the project's provider for the active generator.
73-
74-
Args:
75-
project: The initialized CPPython project
76-
77-
Returns:
78-
The sync data from the provider, or None if not available
79-
"""
80-
if not project.enabled:
81-
return None
82-
83-
# Access the internal data to get sync information
84-
data = project._data # noqa: SLF001
85-
86-
# Get sync data from provider for the generator
87-
return data.plugins.provider.sync_data(data.plugins.generator)
88-
8964
def prepare(self) -> BuildPreparationResult:
9065
"""Run CPPython preparation and return the build preparation result.
9166
92-
This runs the provider workflow (download tools, sync, install)
93-
and extracts the sync data for injection into the build backend.
67+
Syncs provider config and verifies that C++ dependencies have been
68+
installed by a prior ``install()`` call. Does **not** install
69+
dependencies itself — the build backend is not responsible for that.
9470
9571
Returns:
9672
BuildPreparationResult containing sync data for the active generator
73+
74+
Raises:
75+
InstallationVerificationError: If provider artifacts are missing
9776
"""
9877
self.logger.info('CPPython: Preparing build environment')
9978

@@ -124,12 +103,16 @@ def prepare(self) -> BuildPreparationResult:
124103
self.logger.info('CPPython: Project not enabled, skipping preparation')
125104
return BuildPreparationResult()
126105

127-
# Run the install workflow to ensure dependencies are ready
128-
self.logger.info('CPPython: Installing C++ dependencies')
129-
project.install()
106+
# Sync and verify — does NOT install dependencies
107+
self.logger.info('CPPython: Verifying C++ dependencies are installed')
130108

131-
# Extract sync data
132-
sync_data = self._get_sync_data(project)
109+
try:
110+
sync_data = project.prepare_build()
111+
except InstallationVerificationError:
112+
self.logger.error(
113+
"CPPython: C++ dependencies not installed. Run 'cppython install' or 'pdm install' before building."
114+
)
115+
raise
133116

134117
if sync_data:
135118
self.logger.info('CPPython: Sync data obtained from provider: %s', type(sync_data).__name__)

0 commit comments

Comments
 (0)