From 919626f54de6240ff7305531c29801e75c3a2343 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Sun, 30 Nov 2025 21:02:18 -0800 Subject: [PATCH 01/13] Conan/Cmake: Handle Spaces in Binary Path --- cppython/plugins/conan/builder.py | 30 +------------- cppython/plugins/conan/plugin.py | 1 - cppython/plugins/conan/schema.py | 1 - tests/unit/plugins/conan/test_builder.py | 52 ------------------------ 4 files changed, 2 insertions(+), 82 deletions(-) diff --git a/cppython/plugins/conan/builder.py b/cppython/plugins/conan/builder.py index 693bd32..0176eef 100644 --- a/cppython/plugins/conan/builder.py +++ b/cppython/plugins/conan/builder.py @@ -1,6 +1,5 @@ """Construction of Conan data""" -import shutil from pathlib import Path from pydantic import DirectoryPath @@ -20,7 +19,6 @@ def _create_base_conanfile( base_file: Path, dependencies: list[ConanDependency], dependency_groups: dict[str, list[ConanDependency]], - cmake_binary: Path | None = None, ) -> None: """Creates a conanfile_base.py with CPPython managed dependencies. @@ -28,7 +26,6 @@ def _create_base_conanfile( base_file: Path to write the base conanfile dependencies: List of main dependencies dependency_groups: Dictionary of dependency groups (e.g., 'test') - cmake_binary: Optional path to CMake binary to use """ test_dependencies = dependency_groups.get('test', []) @@ -46,16 +43,6 @@ def _create_base_conanfile( '\n'.join(test_requires_lines) if test_requires_lines else ' pass # No test requirements' ) - # Generate configure method content for cmake_program if specified - if cmake_binary: - # Use forward slashes for cross-platform compatibility in Conan - cmake_path_str = str(cmake_binary.resolve()).replace('\\', '/') - configure_content = f''' def configure(self): - """CPPython managed configuration.""" - self.conf.define("tools.cmake:cmake_program", "{cmake_path_str}")''' - else: - configure_content = '' - content = f'''"""CPPython managed base ConanFile. This file is auto-generated by CPPython. Do not edit manually. @@ -67,7 +54,6 @@ def _create_base_conanfile( class CPPythonBase(ConanFile): """Base ConanFile with CPPython managed dependencies.""" -{configure_content} def requirements(self): """CPPython managed requirements.""" @@ -164,25 +150,13 @@ def generate_conanfile( Args: directory: The project directory - data: Generation data containing dependencies, project info, and cmake binary path. - If cmake_binary is not provided, attempts to find cmake in the current - Python environment. + data: Generation data containing dependencies and project info. """ directory.mkdir(parents=True, exist_ok=True) - # Resolve cmake binary path - resolved_cmake: Path | None = None - if data.cmake_binary and data.cmake_binary != 'cmake': - resolved_cmake = Path(data.cmake_binary).resolve() - else: - # Try to find cmake in the current Python environment (venv) - cmake_path = shutil.which('cmake') - if cmake_path: - resolved_cmake = Path(cmake_path).resolve() - # Always regenerate the base conanfile with managed dependencies base_file = directory / 'conanfile_base.py' - self._create_base_conanfile(base_file, data.dependencies, data.dependency_groups, resolved_cmake) + self._create_base_conanfile(base_file, data.dependencies, data.dependency_groups) # Only create conanfile.py if it doesn't exist conan_file = directory / self._filename diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index 362efe9..c705d95 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -121,7 +121,6 @@ def _prepare_installation(self, groups: list[str] | None = None) -> Path: dependency_groups=resolved_dependency_groups, name=self.core_data.pep621_data.name, version=self.core_data.pep621_data.version, - cmake_binary=self._cmake_binary, ) self.builder.generate_conanfile( diff --git a/cppython/plugins/conan/schema.py b/cppython/plugins/conan/schema.py index eb81f9f..67e65c8 100644 --- a/cppython/plugins/conan/schema.py +++ b/cppython/plugins/conan/schema.py @@ -307,7 +307,6 @@ class ConanfileGenerationData(CPPythonModel): dependency_groups: dict[str, list[ConanDependency]] name: str version: str - cmake_binary: str | None = None class ConanConfiguration(CPPythonModel): diff --git a/tests/unit/plugins/conan/test_builder.py b/tests/unit/plugins/conan/test_builder.py index 7eea5a4..f04d859 100644 --- a/tests/unit/plugins/conan/test_builder.py +++ b/tests/unit/plugins/conan/test_builder.py @@ -171,55 +171,3 @@ def test_inheritance_chain(self, builder: Builder, tmp_path: Path) -> None: assert 'class TestProjectPackage(CPPythonBase):' in user_content assert 'super().requirements()' in user_content assert 'super().build_requirements()' in user_content - - def test_cmake_binary_configure(self, builder: Builder, tmp_path: Path) -> None: - """Test that cmake_binary generates configure() with forward slashes.""" - base_file = tmp_path / 'conanfile_base.py' - cmake_path = Path('C:/Program Files/CMake/bin/cmake.exe') - - builder._create_base_conanfile(base_file, [], {}, cmake_binary=cmake_path) - - content = base_file.read_text(encoding='utf-8') - assert 'def configure(self):' in content - assert 'self.conf.define("tools.cmake:cmake_program"' in content - assert 'C:/Program Files/CMake/bin/cmake.exe' in content - assert '\\' not in content.split('tools.cmake:cmake_program')[1].split('"')[1] - - def test_no_cmake_binary(self, builder: Builder, tmp_path: Path) -> None: - """Test that no cmake_binary means no configure() method.""" - base_file = tmp_path / 'conanfile_base.py' - - builder._create_base_conanfile(base_file, [], {}, cmake_binary=None) - - content = base_file.read_text(encoding='utf-8') - assert 'def configure(self):' not in content - - @pytest.mark.parametrize( - ('venv_cmake', 'expect_configure'), - [ - ('/path/to/venv/bin/cmake', True), - (None, False), - ], - ) - def test_cmake_binary_venv_fallback( - self, - builder: Builder, - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - venv_cmake: str | None, - expect_configure: bool, - ) -> None: - """Test venv cmake fallback when cmake_binary is default.""" - monkeypatch.setattr('cppython.plugins.conan.builder.shutil.which', lambda _: venv_cmake) - - data = ConanfileGenerationData( - dependencies=[], - dependency_groups={}, - name='test-project', - version='1.0.0', - cmake_binary='cmake', - ) - builder.generate_conanfile(directory=tmp_path, data=data) - - content = (tmp_path / 'conanfile_base.py').read_text(encoding='utf-8') - assert ('def configure(self):' in content) == expect_configure From fa05a14d5febade4f9dd4f94bea9c7a5257ffb25 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Mon, 1 Dec 2025 18:52:45 -0800 Subject: [PATCH 02/13] Update Chore --- pdm.lock | 70 +++++++++++++++++++++++++++++++++++--------------- pyproject.toml | 37 ++++++++++++++++++++------ 2 files changed, 78 insertions(+), 29 deletions(-) diff --git a/pdm.lock b/pdm.lock index f95c924..bff2dc4 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "cmake", "conan", "docs", "git", "lint", "pdm", "pytest", "test"] strategy = [] lock_version = "4.5.0" -content_hash = "sha256:26e6ec12b531e5ea0002ae0088c90f71d24b48e2d192831e4fc9b0140b1ddf48" +content_hash = "sha256:041e2527f6dfc7446b63a6e7251946f9f50f1000ae097aa97fcb59d330e0b31f" [[metadata.targets]] requires_python = ">=3.14" @@ -550,6 +550,16 @@ files = [ {file = "patch-ng-1.18.1.tar.gz", hash = "sha256:52fd46ee46f6c8667692682c1fd7134edc65a2d2d084ebec1d295a6087fc0291"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +requires_python = ">=3.8" +summary = "Utility library for gitignore style pattern matching of file paths." +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "pbs-installer" version = "2025.10.28" @@ -854,29 +864,47 @@ files = [ [[package]] name = "ruff" -version = "0.14.6" +version = "0.14.7" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." files = [ - {file = "ruff-0.14.6-py3-none-linux_armv6l.whl", hash = "sha256:d724ac2f1c240dbd01a2ae98db5d1d9a5e1d9e96eba999d1c48e30062df578a3"}, - {file = "ruff-0.14.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9f7539ea257aa4d07b7ce87aed580e485c40143f2473ff2f2b75aee003186004"}, - {file = "ruff-0.14.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f6007e55b90a2a7e93083ba48a9f23c3158c433591c33ee2e99a49b889c6332"}, - {file = "ruff-0.14.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8e7b9d73d8728b68f632aa8e824ef041d068d231d8dbc7808532d3629a6bef"}, - {file = "ruff-0.14.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d50d45d4553a3ebcbd33e7c5e0fe6ca4aafd9a9122492de357205c2c48f00775"}, - {file = "ruff-0.14.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:118548dd121f8a21bfa8ab2c5b80e5b4aed67ead4b7567790962554f38e598ce"}, - {file = "ruff-0.14.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:57256efafbfefcb8748df9d1d766062f62b20150691021f8ab79e2d919f7c11f"}, - {file = "ruff-0.14.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff18134841e5c68f8e5df1999a64429a02d5549036b394fafbe410f886e1989d"}, - {file = "ruff-0.14.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c4b7ec1e66a105d5c27bd57fa93203637d66a26d10ca9809dc7fc18ec58440"}, - {file = "ruff-0.14.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167843a6f78680746d7e226f255d920aeed5e4ad9c03258094a2d49d3028b105"}, - {file = "ruff-0.14.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:16a33af621c9c523b1ae006b1b99b159bf5ac7e4b1f20b85b2572455018e0821"}, - {file = "ruff-0.14.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1432ab6e1ae2dc565a7eea707d3b03a0c234ef401482a6f1621bc1f427c2ff55"}, - {file = "ruff-0.14.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c55cfbbe7abb61eb914bfd20683d14cdfb38a6d56c6c66efa55ec6570ee4e71"}, - {file = "ruff-0.14.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:efea3c0f21901a685fff4befda6d61a1bf4cb43de16da87e8226a281d614350b"}, - {file = "ruff-0.14.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:344d97172576d75dc6afc0e9243376dbe1668559c72de1864439c4fc95f78185"}, - {file = "ruff-0.14.6-py3-none-win32.whl", hash = "sha256:00169c0c8b85396516fdd9ce3446c7ca20c2a8f90a77aa945ba6b8f2bfe99e85"}, - {file = "ruff-0.14.6-py3-none-win_amd64.whl", hash = "sha256:390e6480c5e3659f8a4c8d6a0373027820419ac14fa0d2713bd8e6c3e125b8b9"}, - {file = "ruff-0.14.6-py3-none-win_arm64.whl", hash = "sha256:d43c81fbeae52cfa8728d8766bbf46ee4298c888072105815b392da70ca836b2"}, - {file = "ruff-0.14.6.tar.gz", hash = "sha256:6f0c742ca6a7783a736b867a263b9a7a80a45ce9bee391eeda296895f1b4e1cc"}, + {file = "ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca"}, + {file = "ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015"}, + {file = "ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554"}, + {file = "ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94"}, + {file = "ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1"}, + {file = "ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b"}, + {file = "ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad"}, + {file = "ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50"}, + {file = "ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9"}, + {file = "ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4"}, + {file = "ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682"}, + {file = "ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143"}, + {file = "ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784"}, + {file = "ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e"}, + {file = "ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc"}, + {file = "ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa"}, + {file = "ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6"}, + {file = "ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228"}, + {file = "ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5"}, +] + +[[package]] +name = "scikit-build-core" +version = "0.11.6" +requires_python = ">=3.8" +summary = "Build backend for CMake based projects" +dependencies = [ + "exceptiongroup>=1.0; python_version < \"3.11\"", + "importlib-resources>=1.3; python_version < \"3.9\"", + "packaging>=23.2", + "pathspec>=0.10.1", + "tomli>=1.2.2; python_version < \"3.11\"", + "typing-extensions>=3.10.0; python_version < \"3.9\"", +] +files = [ + {file = "scikit_build_core-0.11.6-py3-none-any.whl", hash = "sha256:ce6d8fe64e6b4c759ea0fb95d2f8a68f60d2df31c2989838633b8ec930736360"}, + {file = "scikit_build_core-0.11.6.tar.gz", hash = "sha256:5982ccd839735be99cfd3b92a8847c6c196692f476c215da84b79d2ad12f9f1b"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index e4b6796..9dcf4d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,14 +17,26 @@ dependencies = [ "packaging>=25.0", "requests>=2.32.5", "types-requests>=2.32.4.20250913", + "scikit-build-core>=0.11.6", ] [project.optional-dependencies] -pytest = ["pytest>=9.0.1", "pytest-mock>=3.15.1"] -git = ["dulwich>=0.24.10"] -pdm = ["pdm>=2.26.2"] -cmake = ["cmake>=4.2.0"] -conan = ["conan>=2.23.0"] +pytest = [ + "pytest>=9.0.1", + "pytest-mock>=3.15.1", +] +git = [ + "dulwich>=0.24.10", +] +pdm = [ + "pdm>=2.26.2", +] +cmake = [ + "cmake>=4.2.0", +] +conan = [ + "conan>=2.23.0", +] [project.urls] homepage = "https://github.com/Synodic-Software/CPPython" @@ -47,9 +59,18 @@ cppython = "cppython.plugins.pdm.plugin:CPPythonPlugin" cppython = "cppython.test.pytest.fixtures" [dependency-groups] -lint = ["ruff>=0.14.6", "pyrefly>=0.43.1"] -test = ["pytest>=9.0.1", "pytest-cov>=7.0.0", "pytest-mock>=3.15.1"] -docs = ["zensical>=0.0.10"] +lint = [ + "ruff>=0.14.7", + "pyrefly>=0.43.1", +] +test = [ + "pytest>=9.0.1", + "pytest-cov>=7.0.0", + "pytest-mock>=3.15.1", +] +docs = [ + "zensical>=0.0.10", +] [project.scripts] cppython = "cppython.console.entry:app" From 9d6d387d6bc1386fa85b8500415cd8452e378618 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Mon, 1 Dec 2025 18:52:56 -0800 Subject: [PATCH 03/13] Toolchain {sourceDir} --- cppython/plugins/cmake/builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cppython/plugins/cmake/builder.py b/cppython/plugins/cmake/builder.py index 3346eef..ee76c4c 100644 --- a/cppython/plugins/cmake/builder.py +++ b/cppython/plugins/cmake/builder.py @@ -48,7 +48,7 @@ def generate_cppython_preset( if provider_data.toolchain_file: relative_toolchain = provider_data.toolchain_file.relative_to(project_root, walk_up=True) - default_configure.toolchainFile = relative_toolchain.as_posix() + default_configure.toolchainFile = '${sourceDir}/' + relative_toolchain.as_posix() configure_presets.append(default_configure) From 7e07328264ad7dca6fa088df44405089bd5d8587 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Mon, 1 Dec 2025 20:43:34 -0800 Subject: [PATCH 04/13] `scikit-build-core` Extension Example --- .vscode/settings.json | 6 +- cppython/build/__init__.py | 33 +++ cppython/build/backend.py | 223 ++++++++++++++++ cppython/build/prepare.py | 146 +++++++++++ cppython/plugins/conan/plugin.py | 11 +- cppython/plugins/conan/resolution.py | 1 + cppython/plugins/conan/schema.py | 11 + cppython/plugins/pdm/plugin.py | 2 +- docs/build-backend/configuration.md | 174 +++++++++++++ docs/build-backend/index.md | 245 ++++++++++++++++++ docs/build-backend/integration.md | 220 ++++++++++++++++ examples/conan_cmake/extension/CMakeLists.txt | 19 ++ .../conan_cmake/extension/CMakePresets.json | 10 + examples/conan_cmake/extension/README.md | 18 ++ examples/conan_cmake/extension/pyproject.toml | 31 +++ .../src/example_extension/__init__.py | 5 + .../extension/src/example_extension/_core.cpp | 46 ++++ examples/conan_cmake/library/pyproject.toml | 2 - examples/conan_cmake/simple/pyproject.toml | 3 - pyproject.toml | 36 +-- .../integration/examples/test_conan_cmake.py | 41 +++ 21 files changed, 1247 insertions(+), 36 deletions(-) create mode 100644 cppython/build/__init__.py create mode 100644 cppython/build/backend.py create mode 100644 cppython/build/prepare.py create mode 100644 docs/build-backend/configuration.md create mode 100644 docs/build-backend/index.md create mode 100644 docs/build-backend/integration.md create mode 100644 examples/conan_cmake/extension/CMakeLists.txt create mode 100644 examples/conan_cmake/extension/CMakePresets.json create mode 100644 examples/conan_cmake/extension/README.md create mode 100644 examples/conan_cmake/extension/pyproject.toml create mode 100644 examples/conan_cmake/extension/src/example_extension/__init__.py create mode 100644 examples/conan_cmake/extension/src/example_extension/_core.cpp diff --git a/.vscode/settings.json b/.vscode/settings.json index 30db100..09fb406 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,5 +15,9 @@ }, "editor.defaultFormatter": "charliermarsh.ruff" }, - "cmake.ignoreCMakeListsMissing": true + "cmake.ignoreCMakeListsMissing": true, + "files.associations": { + "*.md": "markdown", + "xstring": "cpp" + } } \ No newline at end of file diff --git a/cppython/build/__init__.py b/cppython/build/__init__.py new file mode 100644 index 0000000..72daf34 --- /dev/null +++ b/cppython/build/__init__.py @@ -0,0 +1,33 @@ +"""CPPython build backend wrapping scikit-build-core. + +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. + +Usage in pyproject.toml: + [build-system] + requires = ["cppython[conan, cmake]"] + build-backend = "cppython.build" +""" + +from cppython.build.backend import ( + build_editable, + build_sdist, + build_wheel, + get_requires_for_build_editable, + get_requires_for_build_sdist, + get_requires_for_build_wheel, + prepare_metadata_for_build_editable, + prepare_metadata_for_build_wheel, +) + +__all__ = [ + 'build_editable', + 'build_sdist', + 'build_wheel', + 'get_requires_for_build_editable', + 'get_requires_for_build_sdist', + 'get_requires_for_build_wheel', + 'prepare_metadata_for_build_editable', + 'prepare_metadata_for_build_wheel', +] diff --git a/cppython/build/backend.py b/cppython/build/backend.py new file mode 100644 index 0000000..0f47a03 --- /dev/null +++ b/cppython/build/backend.py @@ -0,0 +1,223 @@ +"""PEP 517 build backend implementation wrapping scikit-build-core. + +This module provides the actual build hooks that delegate to scikit-build-core +after running CPPython's preparation workflow. +""" + +import logging +from pathlib import Path +from typing import Any + +from scikit_build_core import build as skbuild + +from cppython.build.prepare import prepare_build + +logger = logging.getLogger('cppython.build') + + +def _inject_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: + config_settings: The original config settings (may be None) + toolchain_file: Path to the toolchain file to inject + + Returns: + Updated config settings with toolchain file injected + """ + settings = dict(config_settings) if config_settings else {} + + if toolchain_file and toolchain_file.exists(): + # scikit-build-core accepts cmake.args for passing CMake arguments + # Using cmake.args passes the toolchain via -DCMAKE_TOOLCHAIN_FILE=... + args_key = 'cmake.args' + toolchain_arg = f'-DCMAKE_TOOLCHAIN_FILE={toolchain_file.absolute()}' + + # Append to existing args or create new + if args_key in settings: + existing = settings[args_key] + # Check if toolchain is already specified + if 'CMAKE_TOOLCHAIN_FILE' not in existing: + settings[args_key] = f'{existing};{toolchain_arg}' + logger.info('CPPython: Appended CMAKE_TOOLCHAIN_FILE to cmake.args') + else: + logger.info('CPPython: User-specified toolchain file takes precedence') + else: + settings[args_key] = toolchain_arg + logger.info('CPPython: Injected CMAKE_TOOLCHAIN_FILE=%s', toolchain_file) + + return settings + + +def _prepare_and_get_settings( + config_settings: dict[str, Any] | None, +) -> dict[str, Any]: + """Run CPPython preparation and merge toolchain into config settings. + + Args: + config_settings: The original config settings + + Returns: + Config settings with CPPython toolchain injected + """ + # Determine source directory (current working directory during build) + source_dir = Path.cwd() + + # Run CPPython preparation + toolchain_file = prepare_build(source_dir) + + # Inject toolchain into config settings + return _inject_toolchain(config_settings, toolchain_file) + + +# PEP 517 Hooks - delegating to scikit-build-core after preparation + + +def get_requires_for_build_wheel( + config_settings: dict[str, Any] | None = None, +) -> list[str]: + """Get additional requirements for building a wheel. + + Args: + config_settings: Build configuration settings + + Returns: + List of additional requirements + """ + return skbuild.get_requires_for_build_wheel(config_settings) + + +def get_requires_for_build_sdist( + config_settings: dict[str, Any] | None = None, +) -> list[str]: + """Get additional requirements for building an sdist. + + 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_editable( + config_settings: dict[str, Any] | None = None, +) -> list[str]: + """Get additional requirements for building an editable install. + + Args: + config_settings: Build configuration settings + + Returns: + List of additional requirements + """ + return skbuild.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 + """ + 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) + + +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 + """ + 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) + + +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 + """ + 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) + + +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) + + +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) diff --git a/cppython/build/prepare.py b/cppython/build/prepare.py new file mode 100644 index 0000000..4084367 --- /dev/null +++ b/cppython/build/prepare.py @@ -0,0 +1,146 @@ +"""Build preparation utilities for CPPython. + +This module handles the pre-build workflow: running CPPython's provider +to install C++ dependencies and extract the toolchain file path for +injection into scikit-build-core's CMake configuration. +""" + +import logging +import tomllib +from pathlib import Path +from typing import Any + +from cppython.core.schema import Interface, ProjectConfiguration +from cppython.plugins.cmake.schema import CMakeSyncData +from cppython.project import Project + + +class BuildInterface(Interface): + """Minimal interface implementation for build backend usage.""" + + def write_pyproject(self) -> None: + """No-op for build backend - we don't modify pyproject.toml during builds.""" + + def write_configuration(self) -> None: + """No-op for build backend - we don't modify configuration during builds.""" + + def write_user_configuration(self) -> None: + """No-op for build backend - we don't modify user configuration during builds.""" + + +class BuildPreparation: + """Handles CPPython preparation before scikit-build-core runs.""" + + def __init__(self, source_dir: Path) -> None: + """Initialize build preparation. + + Args: + source_dir: The source directory containing pyproject.toml + """ + self.source_dir = source_dir.absolute() + self.logger = logging.getLogger('cppython.build') + + def _load_pyproject(self) -> dict[str, Any]: + """Load pyproject.toml from the source directory. + + Returns: + The parsed pyproject.toml contents + + Raises: + FileNotFoundError: If pyproject.toml doesn't exist + """ + pyproject_path = self.source_dir / 'pyproject.toml' + if not pyproject_path.exists(): + raise FileNotFoundError(f'pyproject.toml not found at {pyproject_path}') + + with open(pyproject_path, 'rb') as f: + return tomllib.load(f) + + def _get_toolchain_file(self, project: Project) -> Path | None: + """Extract the toolchain file path from the project's sync data. + + Args: + project: The initialized CPPython project + + Returns: + Path to the toolchain file, or None if not available + """ + if not project.enabled: + return None + + # Access the internal data to get sync information + # The toolchain file is generated during the sync process + data = project._data # noqa: SLF001 + + # Get sync data from provider for the generator + sync_data = data.plugins.provider.sync_data(data.plugins.generator) + + if isinstance(sync_data, CMakeSyncData): + return sync_data.toolchain_file + + return None + + def prepare(self) -> Path | None: + """Run CPPython preparation and return the toolchain file path. + + This runs the provider workflow (download tools, sync, install) + and extracts the generated toolchain file path. + + Returns: + Path to the generated toolchain file, or None if CPPython is not configured + """ + self.logger.info('CPPython: Preparing build environment') + + pyproject_data = self._load_pyproject() + + # Check if CPPython is configured + tool_data = pyproject_data.get('tool', {}) + if 'cppython' not in tool_data: + self.logger.info('CPPython: No [tool.cppython] configuration found, skipping preparation') + return None + + # Get version from pyproject if available + project_data = pyproject_data.get('project', {}) + version = project_data.get('version') + + # Create project configuration + project_config = ProjectConfiguration( + project_root=self.source_dir, + version=version, + verbosity=1, + ) + + # Create the CPPython project + interface = BuildInterface() + project = Project(project_config, interface, pyproject_data) + + if not project.enabled: + self.logger.info('CPPython: Project not enabled, skipping preparation') + return None + + # Run the install workflow to ensure dependencies are ready + self.logger.info('CPPython: Installing C++ dependencies') + project.install() + + # Extract the toolchain file path + toolchain_file = self._get_toolchain_file(project) + + if toolchain_file: + self.logger.info('CPPython: Using toolchain file: %s', toolchain_file) + else: + self.logger.warning('CPPython: No toolchain file generated') + + return toolchain_file + + +def prepare_build(source_dir: Path) -> Path | None: + """Convenience function to prepare the build environment. + + Args: + source_dir: The source directory containing pyproject.toml + + Returns: + Path to the generated toolchain file, or None if not available + """ + preparation = BuildPreparation(source_dir) + return preparation.prepare() diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index c705d95..ecd4ffa 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -87,7 +87,7 @@ def _install_dependencies(self, *, update: bool = False, groups: list[str] | Non raise ProviderInstallationError('conan', f'Failed to prepare {operation} environment: {e}', e) from e try: - build_types = ['Release', 'Debug'] + build_types = self.data.build_types for build_type in build_types: logger.info('Installing dependencies for build type: %s', build_type) self._run_conan_install(conanfile_path, update, build_type, logger) @@ -161,6 +161,13 @@ def _run_conan_install(self, conanfile_path: Path, update: bool, build_type: str # Build conan install command arguments command_args = ['install', str(conanfile_path)] + # Use build_path as the output folder directly + output_folder = self.core_data.cppython_data.build_path + command_args.extend(['--output-folder', str(output_folder)]) + + # Override cmake_layout's default 'build' subfolder to normalize path structure + command_args.extend(['-c', 'tools.cmake.cmake_layout:build_folder=.']) + # Add build missing flag command_args.extend(['--build', 'missing']) @@ -276,6 +283,8 @@ def _create_cmake_sync_data(self) -> CMakeSyncData: Returns: CMakeSyncData configured for Conan integration """ + # With tools.cmake.cmake_layout:build_folder=. and --output-folder=build_path, + # generators are placed directly in build_path/generators/ conan_toolchain_path = self.core_data.cppython_data.build_path / 'generators' / 'conan_toolchain.cmake' return CMakeSyncData( diff --git a/cppython/plugins/conan/resolution.py b/cppython/plugins/conan/resolution.py index 3c98b37..85cf6e2 100644 --- a/cppython/plugins/conan/resolution.py +++ b/cppython/plugins/conan/resolution.py @@ -123,4 +123,5 @@ def resolve_conan_data(data: dict[str, Any], core_data: CorePluginData) -> Conan remotes=parsed_data.remotes, skip_upload=parsed_data.skip_upload, profile_dir=profile_dir, + build_types=parsed_data.build_types, ) diff --git a/cppython/plugins/conan/schema.py b/cppython/plugins/conan/schema.py index 67e65c8..d2a6dd5 100644 --- a/cppython/plugins/conan/schema.py +++ b/cppython/plugins/conan/schema.py @@ -295,6 +295,7 @@ class ConanData(CPPythonModel): remotes: list[str] skip_upload: bool profile_dir: Path + build_types: list[str] class ConanfileGenerationData(CPPythonModel): @@ -328,3 +329,13 @@ class ConanConfiguration(CPPythonModel): "If a relative path is provided, it will be resolved relative to the tool's working directory." ), ] = 'profiles' + build_types: Annotated[ + list[str], + Field( + alias='build-types', + description='List of CMake build types to install dependencies for. ' + 'For multi-config generators (Visual Studio), use both Release and Debug. ' + 'For single-config generators or build backends like scikit-build-core, ' + 'use only the build type you need (e.g., ["Release"]).', + ), + ] = ['Release', 'Debug'] diff --git a/cppython/plugins/pdm/plugin.py b/cppython/plugins/pdm/plugin.py index 64482f9..47607fd 100644 --- a/cppython/plugins/pdm/plugin.py +++ b/cppython/plugins/pdm/plugin.py @@ -59,7 +59,7 @@ def on_post_install(self, project: Project, dry_run: bool, **_kwargs: Any) -> No self.logger.info("CPPython: Entered 'on_post_install'") - if (pdm_pyproject := project.pyproject.read()) is None: + if (pdm_pyproject := project.pyproject.open_for_read()) is None: self.logger.info('CPPython: Project data was not available') return diff --git a/docs/build-backend/configuration.md b/docs/build-backend/configuration.md new file mode 100644 index 0000000..69824bf --- /dev/null +++ b/docs/build-backend/configuration.md @@ -0,0 +1,174 @@ +# Configuration Reference + +Complete configuration options for the `cppython.build` backend. + +## Build System + +```toml +[build-system] +requires = ["cppython[conan, cmake]"] +build-backend = "cppython.build" +``` + +### Extras + +| Extra | Description | +|-------|-------------| +| `conan` | Conan package manager support | +| `cmake` | CMake generator support | +| `git` | Git SCM for dynamic versioning | + +## CPPython Options + +These options are configured under `[tool.cppython]`. + +### install-path + +Path where provider tools and cached dependencies are stored. + +```toml +[tool.cppython] +install-path = "~/.cppython" # Default +``` + +- **Type**: Path +- **Default**: `~/.cppython` +- Relative paths are resolved from the project root +- Use absolute path for build isolation compatibility + +### tool-path + +Local directory for CPPython-generated files (presets, etc.). + +```toml +[tool.cppython] +tool-path = "tool" # Default +``` + +- **Type**: Path +- **Default**: `tool` + +### build-path + +Directory for build artifacts. + +```toml +[tool.cppython] +build-path = "build" # Default +``` + +- **Type**: Path +- **Default**: `build` + +### dependencies + +List of C++ dependencies in PEP 508-style format. + +```toml +[tool.cppython] +dependencies = [ + "fmt>=11.0.0", + "nanobind>=2.4.0", + "boost>=1.84.0", +] +``` + +- **Type**: List of strings +- Syntax follows provider conventions (Conan uses `name>=version`) + +### dependency-groups + +Named groups of dependencies for optional features. + +```toml +[tool.cppython.dependency-groups] +test = ["gtest>=1.14.0"] +dev = ["benchmark>=1.8.0"] +``` + +- **Type**: Dictionary of string lists + +## Provider Configuration + +### Conan + +```toml +[tool.cppython.providers.conan] +# Conan-specific options +``` + +See [Conan Plugin Configuration](../plugins/conan/configuration.md) for details. + +## Generator Configuration + +### CMake + +```toml +[tool.cppython.generators.cmake] +preset_file = "CMakePresets.json" # Default +configuration_name = "default" # Default +``` + +## scikit-build-core Passthrough + +All `[tool.scikit-build]` options are passed directly to scikit-build-core: + +```toml +[tool.scikit-build] +cmake.build-type = "Release" +cmake.args = ["-DSOME_OPTION=ON"] +wheel.packages = ["src/my_package"] +logging.level = "WARNING" +``` + +CPPython only adds: + +- `cmake.define.CMAKE_TOOLCHAIN_FILE` (from provider) + +All other settings are untouched. + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `CMAKE_TOOLCHAIN_FILE` | Overrides CPPython's injected toolchain (user takes precedence) | +| `CONAN_HOME` | Conan cache location | + +## Example: Full Configuration + +```toml +[build-system] +requires = ["cppython[conan, cmake, git]"] +build-backend = "cppython.build" + +[project] +name = "my_extension" +version = "1.0.0" +requires-python = ">=3.10" + +[tool.scikit-build] +cmake.build-type = "Release" +cmake.args = ["-DBUILD_TESTING=OFF"] +wheel.packages = ["src/my_extension"] +wheel.install-dir = "my_extension" + +[tool.cppython] +install-path = "~/.cppython" +tool-path = "tool" +build-path = "build" + +dependencies = [ + "fmt>=11.0.0", + "nanobind>=2.4.0", + "spdlog>=1.14.0", +] + +[tool.cppython.dependency-groups] +test = ["gtest>=1.14.0", "benchmark>=1.8.0"] + +[tool.cppython.generators.cmake] +preset_file = "CMakePresets.json" +configuration_name = "default" + +[tool.cppython.providers.conan] +``` diff --git a/docs/build-backend/index.md b/docs/build-backend/index.md new file mode 100644 index 0000000..80e20ee --- /dev/null +++ b/docs/build-backend/index.md @@ -0,0 +1,245 @@ +# Build Backend + +CPPython provides a PEP 517 build backend that wraps [scikit-build-core](https://scikit-build-core.readthedocs.io/), enabling seamless building of Python extension modules with C++ dependencies managed by CPPython. + +## Overview + +The `cppython.build` backend automatically: + +1. Runs the CPPython provider workflow (Conan/vcpkg) to install C++ dependencies +2. Extracts the generated toolchain file +3. Injects `CMAKE_TOOLCHAIN_FILE` into scikit-build-core +4. Delegates the actual wheel building to scikit-build-core + +This allows you to define C++ dependencies in `[tool.cppython]` and have them automatically available when building Python extensions. + +## Quick Start + +Set `cppython.build` as your build backend in `pyproject.toml`: + +```toml +[build-system] +requires = ["cppython[conan, cmake]"] +build-backend = "cppython.build" + +[project] +name = "my_extension" +version = "1.0.0" + +[tool.scikit-build] +cmake.build-type = "Release" + +[tool.cppython] +dependencies = ["fmt>=11.0.0", "nanobind>=2.4.0"] + +[tool.cppython.generators.cmake] + +[tool.cppython.providers.conan] +``` + +Then build with: + +```bash +pip wheel . +``` + +## Configuration + +### Build System Requirements + +The `build-system.requires` should include CPPython with the appropriate extras: + +```toml +[build-system] +requires = ["cppython[conan, cmake]"] +build-backend = "cppython.build" +``` + +Available extras: + +- `conan` - Conan package manager support +- `cmake` - CMake build system +- `git` - Git SCM support for version detection + +### CPPython Configuration + +Configure C++ dependencies under `[tool.cppython]`: + +```toml +[tool.cppython] +install-path = "install" # Where provider tools are cached + +dependencies = [ + "fmt>=11.0.0", + "nanobind>=2.4.0", +] + +[tool.cppython.generators.cmake] +# CMake generator options + +[tool.cppython.providers.conan] +# Conan provider options +``` + +### scikit-build-core Configuration + +All standard `[tool.scikit-build]` options are supported and passed through: + +```toml +[tool.scikit-build] +cmake.build-type = "Release" +wheel.packages = ["src/my_package"] +``` + +CPPython only injects `CMAKE_TOOLCHAIN_FILE` - all other scikit-build-core settings remain under your control. + +## How It Works + +### Build Workflow + +``` +pip wheel . / pdm build + │ + ▼ +┌─────────────────────────────────────┐ +│ cppython.build │ +├─────────────────────────────────────┤ +│ 1. Load pyproject.toml │ +│ 2. Initialize CPPython Project │ +│ 3. Run provider.install() │ +│ └─► Conan/vcpkg installs deps │ +│ 4. Extract toolchain file path │ +│ 5. Inject CMAKE_TOOLCHAIN_FILE │ +└──────────────┬──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ scikit_build_core.build │ +├─────────────────────────────────────┤ +│ 1. Configure CMake with toolchain │ +│ 2. Build extension module │ +│ 3. Package into wheel │ +└─────────────────────────────────────┘ +``` + +### Toolchain Injection + +The provider (e.g., Conan) generates a toolchain file containing paths to all installed dependencies. CPPython extracts this path and passes it to scikit-build-core via: + +``` +cmake.define.CMAKE_TOOLCHAIN_FILE=/path/to/conan_toolchain.cmake +``` + +This allows CMake's `find_package()` to locate all CPPython-managed dependencies. + +## Example Project + +A complete example is available at `examples/conan_cmake/extension/`. + +### Project Structure + +``` +my_extension/ +├── CMakeLists.txt +├── pyproject.toml +└── src/ + └── my_extension/ + ├── __init__.py + └── _core.cpp +``` + +### CMakeLists.txt + +```cmake +cmake_minimum_required(VERSION 3.15...3.30) +project(my_extension LANGUAGES CXX) + +find_package(Python REQUIRED COMPONENTS Interpreter Development.Module) +find_package(nanobind REQUIRED) +find_package(fmt REQUIRED) + +nanobind_add_module(_core src/my_extension/_core.cpp) +target_link_libraries(_core PRIVATE fmt::fmt) +target_compile_features(_core PRIVATE cxx_std_17) + +install(TARGETS _core DESTINATION my_extension) +``` + +### pyproject.toml + +```toml +[build-system] +requires = ["cppython[conan, cmake]"] +build-backend = "cppython.build" + +[project] +name = "my_extension" +version = "1.0.0" + +[tool.scikit-build] +cmake.build-type = "Release" +wheel.packages = ["src/my_extension"] + +[tool.cppython] +dependencies = ["fmt>=11.0.0", "nanobind>=2.4.0"] + +[tool.cppython.generators.cmake] + +[tool.cppython.providers.conan] +``` + +### C++ Source + +```cpp +#include +#include + +namespace nb = nanobind; + +std::string greet(const std::string& name) { + return fmt::format("Hello, {}!", name); +} + +NB_MODULE(_core, m) { + m.def("greet", &greet, nb::arg("name")); +} +``` + +## Comparison with Alternatives + +| Approach | C++ Deps | Python Bindings | Build Backend | +|----------|----------|-----------------|---------------| +| **CPPython + scikit-build-core** | Conan/vcpkg | Any (nanobind, pybind11) | `cppython.build` | +| scikit-build-core alone | Manual/system | Any | `scikit_build_core.build` | +| meson-python | Manual/system | Any | `mesonpy` | + +CPPython's advantage is automated C++ dependency management - you declare dependencies in `pyproject.toml` and they're installed automatically during the build. + +## Troubleshooting + +### Dependencies not found by CMake + +Ensure your `CMakeLists.txt` uses `find_package()` for each dependency: + +```cmake +find_package(fmt REQUIRED) +target_link_libraries(my_target PRIVATE fmt::fmt) +``` + +### Build isolation issues + +If dependencies aren't being found in isolated builds, ensure `install-path` in `[tool.cppython]` uses an absolute path for caching: + +```toml +[tool.cppython] +install-path = "~/.cppython" # Persists across builds +``` + +### Viewing build logs + +Set scikit-build-core to verbose mode: + +```toml +[tool.scikit-build] +logging.level = "DEBUG" +``` diff --git a/docs/build-backend/integration.md b/docs/build-backend/integration.md new file mode 100644 index 0000000..a97b185 --- /dev/null +++ b/docs/build-backend/integration.md @@ -0,0 +1,220 @@ +# Integration Guide + +How to integrate the `cppython.build` backend into your Python extension project. + +## Migrating from scikit-build-core + +If you have an existing scikit-build-core project, migration is straightforward. + +### Before (scikit-build-core only) + +```toml +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[tool.scikit-build] +cmake.build-type = "Release" +``` + +With manual C++ dependency management (system packages, git submodules, etc.). + +### After (CPPython + scikit-build-core) + +```toml +[build-system] +requires = ["cppython[conan, cmake]"] +build-backend = "cppython.build" + +[tool.scikit-build] +cmake.build-type = "Release" + +[tool.cppython] +dependencies = ["fmt>=11.0.0", "nanobind>=2.4.0"] + +[tool.cppython.generators.cmake] + +[tool.cppython.providers.conan] +``` + +### CMakeLists.txt Changes + +Remove manual dependency fetching: + +```cmake +# Before: FetchContent, git submodules, find_package with hints +FetchContent_Declare(fmt GIT_REPOSITORY ...) +FetchContent_MakeAvailable(fmt) + +# After: Just find_package (Conan toolchain provides paths) +find_package(fmt REQUIRED) +``` + +## Using with PDM + +CPPython integrates with PDM for development workflow. + +### Development Setup + +```toml +[tool.pdm] +distribution = true + +[build-system] +requires = ["cppython[conan, cmake]"] +build-backend = "cppython.build" +``` + +### Commands + +```bash +# Install Python dependencies + build extension +pdm install + +# Build wheel +pdm build + +# Development with editable install +pdm install --dev +``` + +## Build Isolation + +### Default Behavior + +`pip wheel .` and `pdm build` use isolated build environments. CPPython handles this by: + +1. Installing C++ dependencies to `install-path` (outside isolation) +2. Generating toolchain in the build directory +3. Passing absolute paths to scikit-build-core + +### Caching Dependencies + +For faster builds, use a persistent `install-path`: + +```toml +[tool.cppython] +install-path = "~/.cppython" # Shared across projects +``` + +### Disabling Isolation (Development) + +For faster iteration during development: + +```bash +pip wheel . --no-build-isolation +``` + +This uses your current environment's CPPython installation. + +## CI/CD Integration + +### GitHub Actions + +```yaml +name: Build + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install build dependencies + run: pip install build + + - name: Build wheel + run: python -m build + + - name: Upload wheel + uses: actions/upload-artifact@v4 + with: + name: wheel + path: dist/*.whl +``` + +### Caching Conan Packages + +```yaml + - name: Cache Conan packages + uses: actions/cache@v4 + with: + path: ~/.conan2 + key: conan-${{ runner.os }}-${{ hashFiles('pyproject.toml') }} +``` + +## Multi-Platform Builds + +### cibuildwheel + +CPPython works with cibuildwheel for building wheels across platforms: + +```toml +# pyproject.toml +[tool.cibuildwheel] +build-verbosity = 1 + +[tool.cibuildwheel.linux] +before-all = "pip install conan && conan profile detect" + +[tool.cibuildwheel.macos] +before-all = "pip install conan && conan profile detect" + +[tool.cibuildwheel.windows] +before-all = "pip install conan && conan profile detect" +``` + +## Editable Installs + +scikit-build-core's editable mode works with CPPython: + +```bash +pip install -e . --no-build-isolation +``` + +For automatic rebuilds on import: + +```toml +[tool.scikit-build] +editable.rebuild = true +editable.verbose = true +``` + +## Combining with Non-Python Projects + +If your repository contains both a Python extension and a standalone C++ project: + +``` +my_project/ +├── CMakeLists.txt # Standalone C++ build +├── CMakePresets.json # CPPython manages presets +├── pyproject.toml # Python extension config +├── src/ +│ ├── lib/ # C++ library +│ └── python/ # Python bindings +└── tool/ # CPPython generated files +``` + +### Dual Workflow + +**Python extension** (uses `cppython.build`): + +```bash +pip wheel . +``` + +**Standalone C++ build** (uses CMakePresets): + +```bash +cmake --preset=default +cmake --build build +``` + +Both workflows share the same Conan-managed dependencies through CPPython's CMake preset integration. diff --git a/examples/conan_cmake/extension/CMakeLists.txt b/examples/conan_cmake/extension/CMakeLists.txt new file mode 100644 index 0000000..bd58ddb --- /dev/null +++ b/examples/conan_cmake/extension/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 4.0) +project(example_extension LANGUAGES CXX) + +# Find Python and nanobind (nanobind will be found via Conan toolchain) +find_package(Python REQUIRED COMPONENTS Interpreter Development.Module) +find_package(nanobind REQUIRED) +find_package(fmt REQUIRED) + +# Create the Python extension module using nanobind +nanobind_add_module(_core src/example_extension/_core.cpp) + +# Link against fmt library +target_link_libraries(_core PRIVATE fmt::fmt) + +# Set C++ standard (nanobind requires C++17+) +target_compile_features(_core PRIVATE cxx_std_17) + +# Install the module +install(TARGETS _core DESTINATION example_extension) diff --git a/examples/conan_cmake/extension/CMakePresets.json b/examples/conan_cmake/extension/CMakePresets.json new file mode 100644 index 0000000..8cbd5d7 --- /dev/null +++ b/examples/conan_cmake/extension/CMakePresets.json @@ -0,0 +1,10 @@ +{ + "version": 9, + "configurePresets": [ + { + "name": "default", + "hidden": true, + "description": "Base preset for all configurations" + } + ] +} \ No newline at end of file diff --git a/examples/conan_cmake/extension/README.md b/examples/conan_cmake/extension/README.md new file mode 100644 index 0000000..6e6e5ad --- /dev/null +++ b/examples/conan_cmake/extension/README.md @@ -0,0 +1,18 @@ +# Example Python Extension with CPPython + +A Python extension module built with CPPython's `cppython.build` backend, using Conan-managed C++ dependencies (nanobind, fmt) and scikit-build-core. + +## Building + +```bash +pip wheel . +``` + +## Usage + +```python +import example_extension + +print(example_extension.format_greeting("World")) +# Output: Hello, World! This message was formatted by fmt. +``` diff --git a/examples/conan_cmake/extension/pyproject.toml b/examples/conan_cmake/extension/pyproject.toml new file mode 100644 index 0000000..db65b91 --- /dev/null +++ b/examples/conan_cmake/extension/pyproject.toml @@ -0,0 +1,31 @@ +[project] +name = "example_extension" +version = "1.0.0" +description = "A Python extension module using CPPython with Conan-managed dependencies" +license = { text = "MIT" } +authors = [{ name = "Synodic Software", email = "contact@synodic.software" }] +requires-python = ">=3.10" + +[build-system] +requires = ["cppython[conan, cmake, git]"] +build-backend = "cppython.build" + +[tool.scikit-build] +# scikit-build-core configuration +cmake.build-type = "Release" +wheel.packages = ["src/example_extension"] + +[tool.cppython] +# CPPython will install these C++ dependencies via Conan +# and inject the toolchain file into scikit-build-core +install-path = "install" + +dependencies = ["fmt>=11.0.2", "nanobind>=2.4.0"] + +[tool.cppython.generators.cmake] +# Use the CMakePresets.json in this directory + +[tool.cppython.providers.conan] +# Conan provider configuration +# Only install Release build type for scikit-build-core (single-config builds) +build-types = ["Release"] diff --git a/examples/conan_cmake/extension/src/example_extension/__init__.py b/examples/conan_cmake/extension/src/example_extension/__init__.py new file mode 100644 index 0000000..9c0834c --- /dev/null +++ b/examples/conan_cmake/extension/src/example_extension/__init__.py @@ -0,0 +1,5 @@ +"""Example Python extension module with C++ backend.""" + +from example_extension._core import add_numbers, format_greeting + +__all__ = ['format_greeting', 'add_numbers'] diff --git a/examples/conan_cmake/extension/src/example_extension/_core.cpp b/examples/conan_cmake/extension/src/example_extension/_core.cpp new file mode 100644 index 0000000..1a8b211 --- /dev/null +++ b/examples/conan_cmake/extension/src/example_extension/_core.cpp @@ -0,0 +1,46 @@ +/** + * Example Python extension module using nanobind and fmt. + * + * This demonstrates how CPPython manages C++ dependencies (fmt, nanobind) + * via Conan, then scikit-build-core builds the Python extension. + */ + +#include +#include +#include + +namespace nb = nanobind; + +/** + * Format a greeting message using the fmt library. + * + * @param name The name to greet + * @return A formatted greeting string + */ +std::string format_greeting(const std::string &name) +{ + return fmt::format("Hello, {}! This message was formatted by fmt.", name); +} + +/** + * Add two numbers together. + * + * @param a First number + * @param b Second number + * @return Sum of a and b + */ +int add_numbers(int a, int b) +{ + return a + b; +} + +NB_MODULE(_core, m) +{ + m.doc() = "Example extension module built with CPPython + scikit-build-core"; + + m.def("format_greeting", &format_greeting, nb::arg("name"), + "Format a greeting message using the fmt library"); + + m.def("add_numbers", &add_numbers, nb::arg("a"), nb::arg("b"), + "Add two numbers together"); +} diff --git a/examples/conan_cmake/library/pyproject.toml b/examples/conan_cmake/library/pyproject.toml index cdeca84..3b5a30f 100644 --- a/examples/conan_cmake/library/pyproject.toml +++ b/examples/conan_cmake/library/pyproject.toml @@ -17,8 +17,6 @@ install-path = "install" dependencies = ["fmt>=12.1.0"] -[tool.cppython.generators.cmake] -cmake_binary = "C:/Program Files/CMake/bin/cmake.exe" [tool.cppython.providers.conan] diff --git a/examples/conan_cmake/simple/pyproject.toml b/examples/conan_cmake/simple/pyproject.toml index ee8e31e..908aefd 100644 --- a/examples/conan_cmake/simple/pyproject.toml +++ b/examples/conan_cmake/simple/pyproject.toml @@ -17,9 +17,6 @@ install-path = "install" dependencies = ["fmt>=12.1.0"] -[tool.cppython.generators.cmake] -cmake_binary = "C:/Program Files/CMake/bin/cmake.exe" - [tool.cppython.providers.conan] [tool.pdm] diff --git a/pyproject.toml b/pyproject.toml index 9dcf4d2..5f71305 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,22 +21,11 @@ dependencies = [ ] [project.optional-dependencies] -pytest = [ - "pytest>=9.0.1", - "pytest-mock>=3.15.1", -] -git = [ - "dulwich>=0.24.10", -] -pdm = [ - "pdm>=2.26.2", -] -cmake = [ - "cmake>=4.2.0", -] -conan = [ - "conan>=2.23.0", -] +pytest = ["pytest>=9.0.1", "pytest-mock>=3.15.1"] +git = ["dulwich>=0.24.10"] +pdm = ["pdm>=2.26.2"] +cmake = ["cmake>=4.2.0"] +conan = ["conan>=2.23.0"] [project.urls] homepage = "https://github.com/Synodic-Software/CPPython" @@ -59,18 +48,9 @@ cppython = "cppython.plugins.pdm.plugin:CPPythonPlugin" cppython = "cppython.test.pytest.fixtures" [dependency-groups] -lint = [ - "ruff>=0.14.7", - "pyrefly>=0.43.1", -] -test = [ - "pytest>=9.0.1", - "pytest-cov>=7.0.0", - "pytest-mock>=3.15.1", -] -docs = [ - "zensical>=0.0.10", -] +lint = ["ruff>=0.14.7", "pyrefly>=0.43.1"] +test = ["pytest>=9.0.1", "pytest-cov>=7.0.0", "pytest-mock>=3.15.1"] +docs = ["zensical>=0.0.10"] [project.scripts] cppython = "cppython.console.entry:app" diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index f7a431a..f90b6f7 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -5,12 +5,15 @@ """ import subprocess +import sys import tomllib +import zipfile from pathlib import Path from tomllib import loads from typer.testing import CliRunner +from cppython.build import build_wheel from cppython.console.schema import ConsoleInterface from cppython.core.schema import ProjectConfiguration from cppython.project import Project @@ -136,3 +139,41 @@ def test_library(example_runner: CliRunner) -> None: # Package the library to local cache publish_project = TestConanCMake._create_project(skip_upload=True) publish_project.publish() + + @staticmethod + def test_extension(example_runner: CliRunner) -> None: + """Test Python extension module built with cppython.build backend and scikit-build-core""" + # This test uses the cppython.build backend which wraps scikit-build-core + # The build backend automatically runs CPPython's provider workflow + + # Create dist directory for the wheel + dist_path = Path('dist') + dist_path.mkdir(exist_ok=True) + + # Build the wheel using the cppython.build backend directly + wheel_name = build_wheel(str(dist_path)) + + # Verify wheel was created + wheel_path = dist_path / wheel_name + assert wheel_path.exists(), f'Wheel not created at {wheel_path}' + + # Extract and test the extension + install_path = Path('install_target') + install_path.mkdir(exist_ok=True) + + with zipfile.ZipFile(wheel_path, 'r') as whl: + whl.extractall(install_path) + + # Test the installed extension by adding install_target to path + test_code = ( + f'import sys; sys.path.insert(0, {str(install_path)!r}); ' + "import example_extension; print(example_extension.format_greeting('Test'))" + ) + test_result = subprocess.run( + [sys.executable, '-c', test_code], + capture_output=True, + text=True, + check=False, + ) + assert test_result.returncode == 0, f'Extension test failed: {test_result.stderr}' + assert 'Hello, Test!' in test_result.stdout, f'Unexpected output: {test_result.stdout}' From 15409a6e84907e11617a13055c49f73cc293725c Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 2 Dec 2025 14:40:03 -0800 Subject: [PATCH 05/13] Remove Rec --- .vscode/extensions.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index aa989ee..1c7d5e9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,5 @@ { "recommendations": [ - "asciidoctor.asciidoctor-vscode", "charliermarsh.ruff", "tamasfe.even-better-toml", "meta.pyrefly" From af9edd785048ab609a8635b5b98eb0d4bf938582 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 2 Dec 2025 15:01:50 -0800 Subject: [PATCH 06/13] Skip `vcpkg` Tests --- tests/integration/plugins/vcpkg/test_provider.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/plugins/vcpkg/test_provider.py b/tests/integration/plugins/vcpkg/test_provider.py index 4d28cd7..4c12021 100644 --- a/tests/integration/plugins/vcpkg/test_provider.py +++ b/tests/integration/plugins/vcpkg/test_provider.py @@ -8,6 +8,7 @@ from cppython.test.pytest.contracts import ProviderIntegrationTestContract +@pytest.mark.skip(reason='Requires system dependencies (zip, unzip, tar) not available in all environments.') class TestCPPythonProvider(ProviderIntegrationTestContract[VcpkgProvider]): """The tests for the vcpkg provider""" From e599ad0e2a7a6402977c63d391baf9b03372aec0 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Tue, 2 Dec 2025 15:20:04 -0800 Subject: [PATCH 07/13] `nanobind` As Test Dep --- docs/build-backend/configuration.md | 2 -- docs/build-backend/index.md | 1 - examples/conan_cmake/extension/CMakeLists.txt | 2 +- examples/conan_cmake/extension/pyproject.toml | 4 ++-- pdm.lock | 11 ++++++++++- pyproject.toml | 2 +- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/build-backend/configuration.md b/docs/build-backend/configuration.md index 69824bf..d6a567d 100644 --- a/docs/build-backend/configuration.md +++ b/docs/build-backend/configuration.md @@ -68,7 +68,6 @@ List of C++ dependencies in PEP 508-style format. [tool.cppython] dependencies = [ "fmt>=11.0.0", - "nanobind>=2.4.0", "boost>=1.84.0", ] ``` @@ -159,7 +158,6 @@ build-path = "build" dependencies = [ "fmt>=11.0.0", - "nanobind>=2.4.0", "spdlog>=1.14.0", ] diff --git a/docs/build-backend/index.md b/docs/build-backend/index.md index 80e20ee..c66433b 100644 --- a/docs/build-backend/index.md +++ b/docs/build-backend/index.md @@ -71,7 +71,6 @@ install-path = "install" # Where provider tools are cached dependencies = [ "fmt>=11.0.0", - "nanobind>=2.4.0", ] [tool.cppython.generators.cmake] diff --git a/examples/conan_cmake/extension/CMakeLists.txt b/examples/conan_cmake/extension/CMakeLists.txt index bd58ddb..2f8800e 100644 --- a/examples/conan_cmake/extension/CMakeLists.txt +++ b/examples/conan_cmake/extension/CMakeLists.txt @@ -1,9 +1,9 @@ cmake_minimum_required(VERSION 4.0) project(example_extension LANGUAGES CXX) -# Find Python and nanobind (nanobind will be found via Conan toolchain) find_package(Python REQUIRED COMPONENTS Interpreter Development.Module) find_package(nanobind REQUIRED) + find_package(fmt REQUIRED) # Create the Python extension module using nanobind diff --git a/examples/conan_cmake/extension/pyproject.toml b/examples/conan_cmake/extension/pyproject.toml index db65b91..7d41ebb 100644 --- a/examples/conan_cmake/extension/pyproject.toml +++ b/examples/conan_cmake/extension/pyproject.toml @@ -7,7 +7,7 @@ authors = [{ name = "Synodic Software", email = "contact@synodic.software" }] requires-python = ">=3.10" [build-system] -requires = ["cppython[conan, cmake, git]"] +requires = ["cppython[conan, cmake, git]", "nanobind>=2.4.0"] build-backend = "cppython.build" [tool.scikit-build] @@ -20,7 +20,7 @@ wheel.packages = ["src/example_extension"] # and inject the toolchain file into scikit-build-core install-path = "install" -dependencies = ["fmt>=11.0.2", "nanobind>=2.4.0"] +dependencies = ["fmt>=11.0.2"] [tool.cppython.generators.cmake] # Use the CMakePresets.json in this directory diff --git a/pdm.lock b/pdm.lock index bff2dc4..ba4ece9 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "cmake", "conan", "docs", "git", "lint", "pdm", "pytest", "test"] strategy = [] lock_version = "4.5.0" -content_hash = "sha256:041e2527f6dfc7446b63a6e7251946f9f50f1000ae097aa97fcb59d330e0b31f" +content_hash = "sha256:1bae711fef71d97486071202b156a2e3a75f019f55372aa7800d031a809447e5" [[metadata.targets]] requires_python = ">=3.14" @@ -532,6 +532,15 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "nanobind" +version = "2.9.2" +summary = "nanobind: tiny and efficient C++/Python bindings" +files = [ + {file = "nanobind-2.9.2-py3-none-any.whl", hash = "sha256:c37957ffd5eac7eda349cff3622ecd32e5ee1244ecc912c99b5bc8188bafd16e"}, + {file = "nanobind-2.9.2.tar.gz", hash = "sha256:e7608472de99d375759814cab3e2c94aba3f9ec80e62cfef8ced495ca5c27d6e"}, +] + [[package]] name = "packaging" version = "25.0" diff --git a/pyproject.toml b/pyproject.toml index 5f71305..c9e50b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ cppython = "cppython.test.pytest.fixtures" [dependency-groups] lint = ["ruff>=0.14.7", "pyrefly>=0.43.1"] -test = ["pytest>=9.0.1", "pytest-cov>=7.0.0", "pytest-mock>=3.15.1"] +test = ["pytest>=9.0.1", "pytest-cov>=7.0.0", "pytest-mock>=3.15.1", "nanobind>=2.4.0"] docs = ["zensical>=0.0.10"] [project.scripts] From 411cdaea5d662d3cfedeb113e4b9bb3af084afb9 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 2 Dec 2025 15:41:17 -0800 Subject: [PATCH 08/13] Linux Fixes --- cppython/plugins/conan/plugin.py | 9 ++++++--- tests/integration/examples/test_conan_cmake.py | 9 ++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index ecd4ffa..051765e 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -283,9 +283,12 @@ def _create_cmake_sync_data(self) -> CMakeSyncData: Returns: CMakeSyncData configured for Conan integration """ - # With tools.cmake.cmake_layout:build_folder=. and --output-folder=build_path, - # generators are placed directly in build_path/generators/ - conan_toolchain_path = self.core_data.cppython_data.build_path / 'generators' / 'conan_toolchain.cmake' + # With cmake_layout, Conan creates a subfolder for each build type. + # Use the first build type for the toolchain path. + build_type = self.data.build_types[0] if self.data.build_types else 'Release' + conan_toolchain_path = ( + self.core_data.cppython_data.build_path / build_type / 'generators' / 'conan_toolchain.cmake' + ) return CMakeSyncData( provider_name=TypeName('conan'), diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index f90b6f7..8dbbc23 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -11,6 +11,7 @@ from pathlib import Path from tomllib import loads +import pytest from typer.testing import CliRunner from cppython.build import build_wheel @@ -20,6 +21,11 @@ pytest_plugins = ['tests.fixtures.example', 'tests.fixtures.conan', 'tests.fixtures.cmake'] +# C++20 modules require Ninja or Visual Studio generator, not Unix Makefiles +_skip_modules_test = pytest.mark.skipif( + sys.platform != 'win32', reason='C++20 modules require Ninja or Visual Studio generator, not Unix Makefiles.' +) + class TestConanCMake: """Test project variation of conan and CMake""" @@ -47,7 +53,7 @@ def _run_cmake_configure(cmake_binary: str) -> None: Args: cmake_binary: Path or command name for the CMake binary to use """ - result = subprocess.run([cmake_binary, '--preset=default'], capture_output=True, text=True, check=False) + result = subprocess.run([cmake_binary, '--preset=default-release'], capture_output=True, text=True, check=False) assert result.returncode == 0, f'CMake configuration failed: {result.stderr}' @staticmethod @@ -108,6 +114,7 @@ def test_simple(example_runner: CliRunner) -> None: publish_project.publish() @staticmethod + @_skip_modules_test def test_library(example_runner: CliRunner) -> None: """Test library creation and packaging workflow""" # Read cmake_binary from the current pyproject.toml (we're in the example directory) From 161472d036ea58b9162591ceadcb97e503a01670 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Tue, 2 Dec 2025 15:51:32 -0800 Subject: [PATCH 09/13] f --- cppython/plugins/conan/plugin.py | 18 +++++++++++++++--- tests/integration/examples/test_conan_cmake.py | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index 051765e..04b829b 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -283,13 +283,25 @@ def _create_cmake_sync_data(self) -> CMakeSyncData: Returns: CMakeSyncData configured for Conan integration """ - # With cmake_layout, Conan creates a subfolder for each build type. - # Use the first build type for the toolchain path. + # With tools.cmake.cmake_layout:build_folder=. and --output-folder=build_path, + # generators are placed in build_path/generators/ for multi-config generators (Windows) + # or build_path//generators/ for single-config generators (Linux/Mac). + # We check which path exists to handle both cases. build_type = self.data.build_types[0] if self.data.build_types else 'Release' - conan_toolchain_path = ( + + # Try multi-config path first (Windows with Visual Studio) + multiconfig_path = self.core_data.cppython_data.build_path / 'generators' / 'conan_toolchain.cmake' + # Single-config path (Linux/Mac with Make/Ninja) + singleconfig_path = ( self.core_data.cppython_data.build_path / build_type / 'generators' / 'conan_toolchain.cmake' ) + # Use whichever path exists, defaulting to multi-config for sync (before install runs) + if singleconfig_path.exists(): + conan_toolchain_path = singleconfig_path + else: + conan_toolchain_path = multiconfig_path + return CMakeSyncData( provider_name=TypeName('conan'), toolchain_file=conan_toolchain_path, diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index 8dbbc23..b53eb8d 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -53,7 +53,7 @@ def _run_cmake_configure(cmake_binary: str) -> None: Args: cmake_binary: Path or command name for the CMake binary to use """ - result = subprocess.run([cmake_binary, '--preset=default-release'], capture_output=True, text=True, check=False) + result = subprocess.run([cmake_binary, '--preset=default'], capture_output=True, text=True, check=False) assert result.returncode == 0, f'CMake configuration failed: {result.stderr}' @staticmethod From aba296cd3f62e65ef545a42ca21f5a339e5dac00 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Tue, 2 Dec 2025 16:24:55 -0800 Subject: [PATCH 10/13] Update plugin.py --- cppython/plugins/conan/plugin.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index 04b829b..d60c52c 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -165,8 +165,12 @@ def _run_conan_install(self, conanfile_path: Path, update: bool, build_type: str output_folder = self.core_data.cppython_data.build_path command_args.extend(['--output-folder', str(output_folder)]) - # Override cmake_layout's default 'build' subfolder to normalize path structure + # Normalize cmake_layout behavior across all platforms/generators: + # - build_folder=. puts build output directly in output_folder (no 'build' subfolder) + # - build_folder_vars=[] prevents build_type subfolders (Release/Debug) + # This ensures generators always end up in output_folder/generators/ consistently command_args.extend(['-c', 'tools.cmake.cmake_layout:build_folder=.']) + command_args.extend(['-c', 'tools.cmake.cmake_layout:build_folder_vars=[]']) # Add build missing flag command_args.extend(['--build', 'missing']) @@ -283,24 +287,9 @@ def _create_cmake_sync_data(self) -> CMakeSyncData: Returns: CMakeSyncData configured for Conan integration """ - # With tools.cmake.cmake_layout:build_folder=. and --output-folder=build_path, - # generators are placed in build_path/generators/ for multi-config generators (Windows) - # or build_path//generators/ for single-config generators (Linux/Mac). - # We check which path exists to handle both cases. - build_type = self.data.build_types[0] if self.data.build_types else 'Release' - - # Try multi-config path first (Windows with Visual Studio) - multiconfig_path = self.core_data.cppython_data.build_path / 'generators' / 'conan_toolchain.cmake' - # Single-config path (Linux/Mac with Make/Ninja) - singleconfig_path = ( - self.core_data.cppython_data.build_path / build_type / 'generators' / 'conan_toolchain.cmake' - ) - - # Use whichever path exists, defaulting to multi-config for sync (before install runs) - if singleconfig_path.exists(): - conan_toolchain_path = singleconfig_path - else: - conan_toolchain_path = multiconfig_path + # With cmake_layout config overrides (build_folder=. and build_folder_vars=[]), + # generators are always placed in build_path/generators/ regardless of platform/generator + conan_toolchain_path = self.core_data.cppython_data.build_path / 'generators' / 'conan_toolchain.cmake' return CMakeSyncData( provider_name=TypeName('conan'), From 0e1719a49da451d113e26e3f9d8f254f8a44e7e9 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Tue, 2 Dec 2025 19:04:24 -0800 Subject: [PATCH 11/13] Custom Layout --- cppython/plugins/conan/builder.py | 23 +++++++++++++++++++---- cppython/plugins/conan/plugin.py | 11 ++--------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/cppython/plugins/conan/builder.py b/cppython/plugins/conan/builder.py index 0176eef..db69cba 100644 --- a/cppython/plugins/conan/builder.py +++ b/cppython/plugins/conan/builder.py @@ -46,14 +46,24 @@ def _create_base_conanfile( content = f'''"""CPPython managed base ConanFile. This file is auto-generated by CPPython. Do not edit manually. -Dependencies are managed through pyproject.toml. +Dependencies and layout are managed through pyproject.toml. """ from conan import ConanFile class CPPythonBase(ConanFile): - """Base ConanFile with CPPython managed dependencies.""" + """Base ConanFile with CPPython managed dependencies and layout.""" + + def layout(self): + """CPPython managed layout for consistent paths across all platforms. + + Uses explicit folder settings instead of cmake_layout() to avoid + platform/generator-dependent behavior (e.g., build_type subfolders + on single-config generators). + """ + self.folders.build = "." + self.folders.generators = "generators" def requirements(self): """CPPython managed requirements.""" @@ -74,7 +84,7 @@ def _create_conanfile( """Creates a conanfile.py file that inherits from CPPython base.""" class_name = name.replace('-', '_').title().replace('_', '') content = f'''import os -from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout +from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain from conan.tools.files import copy from conanfile_base import CPPythonBase @@ -107,7 +117,12 @@ def build_requirements(self): # Add your custom build requirements here def layout(self): - cmake_layout(self) + """Configure build folder layout. + + CPPython managed layout is inherited from CPPythonBase. + Override if you need custom folder settings. + """ + super().layout() # Get CPPython managed layout def generate(self): deps = CMakeDeps(self) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index d60c52c..9e730d2 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -165,13 +165,6 @@ def _run_conan_install(self, conanfile_path: Path, update: bool, build_type: str output_folder = self.core_data.cppython_data.build_path command_args.extend(['--output-folder', str(output_folder)]) - # Normalize cmake_layout behavior across all platforms/generators: - # - build_folder=. puts build output directly in output_folder (no 'build' subfolder) - # - build_folder_vars=[] prevents build_type subfolders (Release/Debug) - # This ensures generators always end up in output_folder/generators/ consistently - command_args.extend(['-c', 'tools.cmake.cmake_layout:build_folder=.']) - command_args.extend(['-c', 'tools.cmake.cmake_layout:build_folder_vars=[]']) - # Add build missing flag command_args.extend(['--build', 'missing']) @@ -287,8 +280,8 @@ def _create_cmake_sync_data(self) -> CMakeSyncData: Returns: CMakeSyncData configured for Conan integration """ - # With cmake_layout config overrides (build_folder=. and build_folder_vars=[]), - # generators are always placed in build_path/generators/ regardless of platform/generator + # The generated conanfile uses explicit layout (self.folders.generators = "generators") + # Combined with --output-folder=build_path, generators are always at build_path/generators/ conan_toolchain_path = self.core_data.cppython_data.build_path / 'generators' / 'conan_toolchain.cmake' return CMakeSyncData( From 33ce0b83b69af15e37036de110266098eef29987 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 2 Dec 2025 19:10:01 -0800 Subject: [PATCH 12/13] test --- tests/integration/examples/test_conan_cmake.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index b53eb8d..0ef21d2 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -26,6 +26,10 @@ sys.platform != 'win32', reason='C++20 modules require Ninja or Visual Studio generator, not Unix Makefiles.' ) +# On Windows (multi-config generators), use 'default' preset +# On Linux/Mac (single-config generators), use 'default-release' because CMAKE_BUILD_TYPE is required +_cmake_preset = 'default' if sys.platform == 'win32' else 'default-release' + class TestConanCMake: """Test project variation of conan and CMake""" @@ -53,7 +57,7 @@ def _run_cmake_configure(cmake_binary: str) -> None: Args: cmake_binary: Path or command name for the CMake binary to use """ - result = subprocess.run([cmake_binary, '--preset=default'], capture_output=True, text=True, check=False) + result = subprocess.run([cmake_binary, f'--preset={_cmake_preset}'], capture_output=True, text=True, check=False) assert result.returncode == 0, f'CMake configuration failed: {result.stderr}' @staticmethod From 0021fd43b3d6ad56a1a0690ebbf458c1c61c47e1 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Tue, 2 Dec 2025 19:17:07 -0800 Subject: [PATCH 13/13] Update test_conan_cmake.py --- tests/integration/examples/test_conan_cmake.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index 0ef21d2..8c932c8 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -57,7 +57,9 @@ def _run_cmake_configure(cmake_binary: str) -> None: Args: cmake_binary: Path or command name for the CMake binary to use """ - result = subprocess.run([cmake_binary, f'--preset={_cmake_preset}'], capture_output=True, text=True, check=False) + result = subprocess.run( + [cmake_binary, f'--preset={_cmake_preset}'], capture_output=True, text=True, check=False + ) assert result.returncode == 0, f'CMake configuration failed: {result.stderr}' @staticmethod