Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# AGENTS.md

This repository doesn't contain any agent specific instructions other than its README.md and its linked resources.
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,35 @@
# CPPython
A Python management solution for C++ dependencies

A transparent Python management solution for C++ dependencies and building.

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
[![PyPI version](https://img.shields.io/pypi/v/cppython.svg)](https://pypi.org/project/cppython/)

## Goals

1. **CLI** — Provide imperative commands (`build`, `test`, `bench`, `run`, `install`) for managing C++ projects within a Python ecosystem.
2. **Plugin Architecture** — Support pluggable generators (CMake, Meson) and providers (Conan, vcpkg) so users can mix and match toolchains.
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.
4. **Package Manager Integration** — Integrate with Python package managers so that `<manager> install` seamlessly handles C++ dependency installation alongside Python dependencies.

## Features

## Setup

See [Setup](https://synodic.github.io/cppython/setup) for setup instructions.

## Development

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.

See [Development](https://synodic.github.io/cppython/development) for additional build, test, and installation instructions.

For contribution guidelines, see [CONTRIBUTING.md](https://github.com/synodic/.github/blob/stable/CONTRIBUTING.md).

## Documentation

## License

This project is licensed under the MIT License — see [LICENSE.md](LICENSE.md) for details.

Copyright © 2026 Synodic Software
9 changes: 5 additions & 4 deletions cppython/build/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""CPPython build backend wrapping scikit-build-core.
"""CPPython build backend wrapping scikit-build-core and meson-python.

This module provides PEP 517/518 build backend hooks that wrap scikit-build-core,
automatically running CPPython's provider workflow before building
to inject the generated toolchain file into the CMake configuration.
This module provides PEP 517/518 build backend hooks that wrap scikit-build-core
or meson-python depending on the active generator, automatically running
CPPython's provider workflow before building to inject the generated
toolchain or native/cross files into the build configuration.

Usage in pyproject.toml:
[build-system]
Expand Down
245 changes: 134 additions & 111 deletions cppython/build/backend.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,65 @@
"""PEP 517 build backend implementation wrapping scikit-build-core.
"""PEP 517 build backend implementation wrapping scikit-build-core and meson-python.

This module provides the actual build hooks that delegate to scikit-build-core
This module provides the actual build hooks that delegate to the appropriate
underlying build backend (scikit-build-core for CMake, meson-python for Meson)
after running CPPython's preparation workflow.
"""

import logging
import tomllib
from pathlib import Path
from types import ModuleType
from typing import Any

import mesonpy
from scikit_build_core import build as skbuild

from cppython.build.prepare import prepare_build
from cppython.build.prepare import BuildPreparationResult, prepare_build
from cppython.plugins.cmake.schema import CMakeSyncData
from cppython.plugins.meson.schema import MesonSyncData

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


def _inject_toolchain(config_settings: dict[str, Any] | None, toolchain_file: Path | None) -> dict[str, Any]:
def _is_meson_project() -> bool:
"""Detect if the current project uses Meson by checking pyproject.toml.

Looks for ``[tool.cppython.generator]`` containing "meson" or the
presence of a ``meson.build`` file in the source directory.

Returns:
True if the project appears to be Meson-based
"""
source_dir = Path.cwd()

# Check pyproject.toml for cppython generator configuration
pyproject_path = source_dir / 'pyproject.toml'
if pyproject_path.exists():
with open(pyproject_path, 'rb') as f:
data = tomllib.load(f)
generator = data.get('tool', {}).get('cppython', {}).get('generator', '')
if isinstance(generator, str) and 'meson' in generator.lower():
return True

# Fallback: check for meson.build file
return (source_dir / 'meson.build').exists()


def _get_backend(is_meson: bool) -> ModuleType:
"""Get the appropriate backend module.

Args:
is_meson: Whether to use meson-python instead of scikit-build-core

Returns:
The backend module (mesonpy or scikit_build_core.build)
"""
if is_meson:
return mesonpy
return skbuild


def _inject_cmake_toolchain(config_settings: dict[str, Any] | None, toolchain_file: Path | None) -> dict[str, Any]:
"""Inject the toolchain file into config settings for scikit-build-core.

Args:
Expand Down Expand Up @@ -49,175 +93,154 @@ def _inject_toolchain(config_settings: dict[str, Any] | None, toolchain_file: Pa
return settings


def _prepare_and_get_settings(
def _inject_meson_files(
config_settings: dict[str, Any] | None,
native_file: Path | None,
cross_file: Path | None,
) -> dict[str, Any]:
"""Run CPPython preparation and merge toolchain into config settings.
"""Inject native/cross files into config settings for meson-python.

Args:
config_settings: The original config settings (may be None)
native_file: Path to the Meson native file to inject
cross_file: Path to the Meson cross file to inject

Returns:
Updated config settings with Meson files injected
"""
settings = dict(config_settings) if config_settings else {}

setup_args_key = 'setup-args'
existing_args = settings.get(setup_args_key, '')

args_to_add: list[str] = []

if native_file and native_file.exists():
native_arg = f'--native-file={native_file.absolute()}'
if '--native-file' not in existing_args:
args_to_add.append(native_arg)
logger.info('CPPython: Injected --native-file=%s', native_file)
else:
logger.info('CPPython: User-specified native file takes precedence')

if cross_file and cross_file.exists():
cross_arg = f'--cross-file={cross_file.absolute()}'
if '--cross-file' not in existing_args:
args_to_add.append(cross_arg)
logger.info('CPPython: Injected --cross-file=%s', cross_file)
else:
logger.info('CPPython: User-specified cross file takes precedence')

if args_to_add:
if existing_args:
settings[setup_args_key] = f'{existing_args};' + ';'.join(args_to_add)
else:
settings[setup_args_key] = ';'.join(args_to_add)

return settings


def _prepare_and_get_result(
config_settings: dict[str, Any] | None,
) -> tuple[BuildPreparationResult, dict[str, Any]]:
"""Run CPPython preparation and merge config into settings.

Args:
config_settings: The original config settings

Returns:
Config settings with CPPython toolchain injected
Tuple of (preparation result, updated config settings)
"""
# Determine source directory (current working directory during build)
source_dir = Path.cwd()

# Run CPPython preparation
toolchain_file = prepare_build(source_dir)
result = prepare_build(source_dir)

# Inject toolchain into config settings
return _inject_toolchain(config_settings, toolchain_file)
# Inject settings based on sync data type
settings = dict(config_settings) if config_settings else {}

if result.sync_data is not None:
if isinstance(result.sync_data, CMakeSyncData):
settings = _inject_cmake_toolchain(settings, result.sync_data.toolchain_file)
elif isinstance(result.sync_data, MesonSyncData):
settings = _inject_meson_files(settings, result.sync_data.native_file, result.sync_data.cross_file)

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


def get_requires_for_build_wheel(
config_settings: dict[str, Any] | None = None,
) -> list[str]:
"""Get additional requirements for building a wheel.
def _is_meson_build(result: BuildPreparationResult) -> bool:
"""Determine if the build should use meson-python based on sync data.

Args:
config_settings: Build configuration settings
result: The build preparation result

Returns:
List of additional requirements
True if meson-python should be used, False for scikit-build-core
"""
return skbuild.get_requires_for_build_wheel(config_settings)
return isinstance(result.sync_data, MesonSyncData)


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

Args:
config_settings: Build configuration settings

Returns:
List of additional requirements
"""
return skbuild.get_requires_for_build_sdist(config_settings)
def get_requires_for_build_wheel(config_settings: dict[str, Any] | None = None) -> list[str]:
"""Get additional requirements for building a wheel."""
return _get_backend(_is_meson_project()).get_requires_for_build_wheel(config_settings)


def get_requires_for_build_editable(
config_settings: dict[str, Any] | None = None,
) -> list[str]:
"""Get additional requirements for building an editable install.
def get_requires_for_build_sdist(config_settings: dict[str, Any] | None = None) -> list[str]:
"""Get additional requirements for building an sdist."""
return _get_backend(_is_meson_project()).get_requires_for_build_sdist(config_settings)

Args:
config_settings: Build configuration settings

Returns:
List of additional requirements
"""
return skbuild.get_requires_for_build_editable(config_settings)
def get_requires_for_build_editable(config_settings: dict[str, Any] | None = None) -> list[str]:
"""Get additional requirements for building an editable install."""
return _get_backend(_is_meson_project()).get_requires_for_build_editable(config_settings)


def build_wheel(
wheel_directory: str,
config_settings: dict[str, Any] | None = None,
metadata_directory: str | None = None,
) -> str:
"""Build a wheel from the source distribution.

This runs CPPython's provider workflow first to ensure C++ dependencies
are installed and the toolchain file is generated, then delegates to
scikit-build-core for the actual wheel build.

Args:
wheel_directory: Directory to place the built wheel
config_settings: Build configuration settings
metadata_directory: Directory containing wheel metadata

Returns:
The basename of the built wheel
"""
"""Build a wheel, running CPPython preparation first."""
logger.info('CPPython: Starting wheel build')

# Prepare CPPython and get updated settings
settings = _prepare_and_get_settings(config_settings)

# Delegate to scikit-build-core
return skbuild.build_wheel(wheel_directory, settings, metadata_directory)
result, settings = _prepare_and_get_result(config_settings)
return _get_backend(_is_meson_build(result)).build_wheel(wheel_directory, settings, metadata_directory)


def build_sdist(
sdist_directory: str,
config_settings: dict[str, Any] | None = None,
) -> str:
"""Build a source distribution.

For sdist, we don't run the full CPPython workflow since the C++ dependencies
should be resolved at wheel build time, not sdist creation time.

Args:
sdist_directory: Directory to place the built sdist
config_settings: Build configuration settings

Returns:
The basename of the built sdist
"""
"""Build a source distribution (no CPPython workflow needed)."""
logger.info('CPPython: Starting sdist build')

# Delegate directly to scikit-build-core (no preparation needed for sdist)
return skbuild.build_sdist(sdist_directory, config_settings)
return _get_backend(_is_meson_project()).build_sdist(sdist_directory, config_settings)


def build_editable(
wheel_directory: str,
config_settings: dict[str, Any] | None = None,
metadata_directory: str | None = None,
) -> str:
"""Build an editable wheel.

This runs CPPython's provider workflow first, similar to build_wheel.

Args:
wheel_directory: Directory to place the built wheel
config_settings: Build configuration settings
metadata_directory: Directory containing wheel metadata

Returns:
The basename of the built wheel
"""
"""Build an editable wheel, running CPPython preparation first."""
logger.info('CPPython: Starting editable build')

# Prepare CPPython and get updated settings
settings = _prepare_and_get_settings(config_settings)

# Delegate to scikit-build-core
return skbuild.build_editable(wheel_directory, settings, metadata_directory)
result, settings = _prepare_and_get_result(config_settings)
return _get_backend(_is_meson_build(result)).build_editable(wheel_directory, settings, metadata_directory)


def prepare_metadata_for_build_wheel(
metadata_directory: str,
config_settings: dict[str, Any] | None = None,
) -> str:
"""Prepare metadata for wheel build.

Args:
metadata_directory: Directory to place the metadata
config_settings: Build configuration settings

Returns:
The basename of the metadata directory
"""
return skbuild.prepare_metadata_for_build_wheel(metadata_directory, config_settings)
"""Prepare metadata for wheel build."""
return _get_backend(_is_meson_project()).prepare_metadata_for_build_wheel(metadata_directory, config_settings)


def prepare_metadata_for_build_editable(
metadata_directory: str,
config_settings: dict[str, Any] | None = None,
) -> str:
"""Prepare metadata for editable build.

Args:
metadata_directory: Directory to place the metadata
config_settings: Build configuration settings

Returns:
The basename of the metadata directory
"""
return skbuild.prepare_metadata_for_build_editable(metadata_directory, config_settings)
"""Prepare metadata for editable build."""
return _get_backend(_is_meson_project()).prepare_metadata_for_build_editable(metadata_directory, config_settings)
Loading
Loading