Skip to content

Commit 2623621

Browse files
committed
Custom cmake_binary
1 parent 45598d1 commit 2623621

File tree

11 files changed

+131
-25
lines changed

11 files changed

+131
-25
lines changed

cppython/plugins/cmake/resolution.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Builder to help resolve cmake state"""
22

3+
import os
4+
from pathlib import Path
35
from typing import Any
46

57
from cppython.core.schema import CorePluginData
@@ -24,4 +26,13 @@ def resolve_cmake_data(data: dict[str, Any], core_data: CorePluginData) -> CMake
2426
if not modified_preset_file.is_absolute():
2527
modified_preset_file = root_directory / modified_preset_file
2628

27-
return CMakeData(preset_file=modified_preset_file, configuration_name=parsed_data.configuration_name)
29+
# Resolve cmake binary: environment variable takes precedence over configuration
30+
cmake_binary: Path | None = None
31+
if env_binary := os.environ.get('CMAKE_BINARY'):
32+
cmake_binary = Path(env_binary)
33+
elif parsed_data.cmake_binary:
34+
cmake_binary = parsed_data.cmake_binary
35+
36+
return CMakeData(
37+
preset_file=modified_preset_file, configuration_name=parsed_data.configuration_name, cmake_binary=cmake_binary
38+
)

cppython/plugins/cmake/schema.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ class CMakeData(CPPythonModel):
111111

112112
preset_file: Path
113113
configuration_name: str
114+
cmake_binary: Path | None
114115

115116

116117
class CMakeConfiguration(CPPythonModel):
@@ -131,3 +132,10 @@ class CMakeConfiguration(CPPythonModel):
131132
'"default-release" will also be written'
132133
),
133134
] = 'default'
135+
cmake_binary: Annotated[
136+
Path | None,
137+
Field(
138+
description='Path to a specific CMake binary to use. If not specified, uses "cmake" from PATH. '
139+
'Can be overridden via CMAKE_BINARY environment variable.'
140+
),
141+
] = None

cppython/plugins/conan/builder.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,11 @@ def export_sources(self):
176176
file.write(result)
177177

178178
def generate_conanfile(
179-
self, directory: DirectoryPath, dependencies: list[ConanDependency], name: str, version: str
179+
self,
180+
directory: DirectoryPath,
181+
dependencies: list[ConanDependency],
182+
name: str,
183+
version: str,
180184
) -> None:
181185
"""Generate a conanfile.py file for the project."""
182186
conan_file = directory / self._filename

cppython/plugins/conan/plugin.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ def __init__(
4545

4646
self._ensure_default_profiles()
4747

48+
# Initialize cmake_binary with system default. It may be overridden during sync.
49+
self._cmake_binary = 'cmake'
50+
4851
@staticmethod
4952
def features(directory: Path) -> SupportedFeatures:
5053
"""Queries conan support
@@ -149,8 +152,11 @@ def _run_conan_install(self, conanfile_path: Path, update: bool, build_type: str
149152
if build_type:
150153
command_args.extend(['-s', f'build_type={build_type}'])
151154

152-
# Log the command being executed
153-
logger.info('Executing conan command: conan %s', ' '.join(command_args))
155+
# Add cmake binary configuration if specified
156+
if self._cmake_binary and self._cmake_binary != 'cmake':
157+
# Quote the path if it contains spaces
158+
cmake_path = f'"{self._cmake_binary}"' if ' ' in self._cmake_binary else self._cmake_binary
159+
command_args.extend(['-c', f'tools.cmake:cmake_program={cmake_path}'])
154160

155161
try:
156162
# Use reusable Conan API instance instead of subprocess
@@ -200,10 +206,28 @@ def sync_data(self, consumer: SyncConsumer) -> SyncData:
200206
"""
201207
for sync_type in consumer.sync_types():
202208
if sync_type == CMakeSyncData:
203-
return self._create_cmake_sync_data()
209+
return self._sync_with_cmake(consumer)
204210

205211
raise NotSupportedError(f'Unsupported sync types: {consumer.sync_types()}')
206212

213+
def _sync_with_cmake(self, consumer: SyncConsumer) -> CMakeSyncData:
214+
"""Synchronize with CMake generator and create sync data.
215+
216+
Args:
217+
consumer: The CMake generator consumer
218+
219+
Returns:
220+
CMakeSyncData configured for Conan integration
221+
"""
222+
# Extract cmake_binary from CMakeGenerator if available
223+
if isinstance(consumer, CMakeGenerator) and not os.environ.get('CMAKE_BINARY'):
224+
# Only override if not already set from environment variable
225+
# Convert Path to string, or use 'cmake' if None
226+
cmake_path = consumer.data.cmake_binary
227+
self._cmake_binary = str(cmake_path) if cmake_path else 'cmake'
228+
229+
return self._create_cmake_sync_data()
230+
207231
def _create_cmake_sync_data(self) -> CMakeSyncData:
208232
"""Creates CMake synchronization data with Conan toolchain configuration.
209233
@@ -243,8 +267,11 @@ def publish(self) -> None:
243267
# Add build mode (build everything for publishing)
244268
command_args.extend(['--build', 'missing'])
245269

246-
# Log the command being executed
247-
logger.info('Executing conan create command: conan %s', ' '.join(command_args))
270+
# Add cmake binary configuration if specified
271+
if self._cmake_binary and self._cmake_binary != 'cmake':
272+
# Quote the path if it contains spaces
273+
cmake_path = f'"{self._cmake_binary}"' if ' ' in self._cmake_binary else self._cmake_binary
274+
command_args.extend(['-c', f'tools.cmake:cmake_program={cmake_path}'])
248275

249276
# Run conan create using reusable Conan API instance
250277
# Change to project directory since Conan API might not handle cwd like subprocess

cppython/project.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,11 @@ def install(self) -> None:
6868
self.logger.info('Installing project')
6969
self.logger.info('Installing %s provider', self._data.plugins.provider.name())
7070

71+
# Sync before install to allow provider to access generator's resolved configuration
72+
self._data.sync()
73+
7174
# Let provider handle its own exceptions for better error context
7275
self._data.plugins.provider.install()
73-
self._data.sync()
7476

7577
def update(self) -> None:
7678
"""Updates project dependencies
@@ -88,9 +90,11 @@ def update(self) -> None:
8890
self.logger.info('Updating project')
8991
self.logger.info('Updating %s provider', self._data.plugins.provider.name())
9092

93+
# Sync before update to allow provider to access generator's resolved configuration
94+
self._data.sync()
95+
9196
# Let provider handle its own exceptions for better error context
9297
self._data.plugins.provider.update()
93-
self._data.sync()
9498

9599
def publish(self) -> None:
96100
"""Publishes the project

examples/conan_cmake/library/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ install-path = "install"
1818
dependencies = ["fmt>=11.2.0"]
1919

2020
[tool.cppython.generators.cmake]
21+
cmake_binary = "C:/Program Files/CMake/bin/cmake.exe"
2122

2223
[tool.cppython.providers.conan]
2324

examples/conan_cmake/simple/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ install-path = "install"
1818
dependencies = ["fmt>=11.2.0"]
1919

2020
[tool.cppython.generators.cmake]
21+
cmake_binary = "C:/Program Files/CMake/bin/cmake.exe"
2122

2223
[tool.cppython.providers.conan]
2324

examples/vcpkg_cmake/simple/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ install-path = "install"
1818
dependencies = ["fmt>=11.2.0"]
1919

2020
[tool.cppython.generators.cmake]
21+
cmake_binary = "C:/Program Files/CMake/bin/cmake.exe"
2122

2223
[tool.cppython.providers.vcpkg]
2324

tests/integration/examples/test_conan_cmake.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import subprocess
8+
import tomllib
89
from pathlib import Path
910
from tomllib import loads
1011

@@ -14,7 +15,7 @@
1415
from cppython.core.schema import ProjectConfiguration
1516
from cppython.project import Project
1617

17-
pytest_plugins = ['tests.fixtures.example', 'tests.fixtures.conan']
18+
pytest_plugins = ['tests.fixtures.example', 'tests.fixtures.conan', 'tests.fixtures.cmake']
1819

1920

2021
class TestConanCMake:
@@ -37,15 +38,23 @@ def _create_project(skip_upload: bool = True) -> Project:
3738
return Project(config, interface, pyproject_data)
3839

3940
@staticmethod
40-
def _run_cmake_configure() -> None:
41-
"""Run CMake configuration and assert success."""
42-
result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False)
41+
def _run_cmake_configure(cmake_binary: str) -> None:
42+
"""Run CMake configuration and assert success.
43+
44+
Args:
45+
cmake_binary: Path or command name for the CMake binary to use
46+
"""
47+
result = subprocess.run([cmake_binary, '--preset=default'], capture_output=True, text=True, check=False)
4348
assert result.returncode == 0, f'CMake configuration failed: {result.stderr}'
4449

4550
@staticmethod
46-
def _run_cmake_build() -> None:
47-
"""Run CMake build and assert success."""
48-
result = subprocess.run(['cmake', '--build', 'build'], capture_output=True, text=True, check=False)
51+
def _run_cmake_build(cmake_binary: str) -> None:
52+
"""Run CMake build and assert success.
53+
54+
Args:
55+
cmake_binary: Path or command name for the CMake binary to use
56+
"""
57+
result = subprocess.run([cmake_binary, '--build', 'build'], capture_output=True, text=True, check=False)
4958
assert result.returncode == 0, f'CMake build failed: {result.stderr}'
5059

5160
@staticmethod
@@ -70,12 +79,25 @@ def _ensure_conan_config(pyproject_data: dict) -> None:
7079
@staticmethod
7180
def test_simple(example_runner: CliRunner) -> None:
7281
"""Simple project"""
82+
# Read cmake_binary from the current pyproject.toml (we're in the example directory)
83+
pyproject_path = Path.cwd() / 'pyproject.toml'
84+
with pyproject_path.open('rb') as file:
85+
pyproject_data = tomllib.load(file)
86+
87+
cmake_binary = (
88+
pyproject_data.get('tool', {})
89+
.get('cppython', {})
90+
.get('generators', {})
91+
.get('cmake', {})
92+
.get('cmake_binary', 'cmake')
93+
)
94+
7395
# Create project and install dependencies
7496
project = TestConanCMake._create_project(skip_upload=False)
7597
project.install()
7698

7799
# Configure and verify build
78-
TestConanCMake._run_cmake_configure()
100+
TestConanCMake._run_cmake_configure(cmake_binary)
79101
TestConanCMake._verify_build_artifacts()
80102

81103
# Test publishing with skip_upload enabled
@@ -85,13 +107,26 @@ def test_simple(example_runner: CliRunner) -> None:
85107
@staticmethod
86108
def test_library(example_runner: CliRunner) -> None:
87109
"""Test library creation and packaging workflow"""
110+
# Read cmake_binary from the current pyproject.toml (we're in the example directory)
111+
pyproject_path = Path.cwd() / 'pyproject.toml'
112+
with pyproject_path.open('rb') as file:
113+
pyproject_data = tomllib.load(file)
114+
115+
cmake_binary = (
116+
pyproject_data.get('tool', {})
117+
.get('cppython', {})
118+
.get('generators', {})
119+
.get('cmake', {})
120+
.get('cmake_binary', 'cmake')
121+
)
122+
88123
# Create project and install dependencies
89124
project = TestConanCMake._create_project(skip_upload=False)
90125
project.install()
91126

92127
# Configure, build, and verify
93-
TestConanCMake._run_cmake_configure()
94-
TestConanCMake._run_cmake_build()
128+
TestConanCMake._run_cmake_configure(cmake_binary)
129+
TestConanCMake._run_cmake_build(cmake_binary)
95130
build_path = TestConanCMake._verify_build_artifacts()
96131

97132
# Verify library files exist (platform-specific)

tests/integration/examples/test_vcpkg_cmake.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import subprocess
8+
import tomllib
89
from pathlib import Path
910
from tomllib import loads
1011

@@ -15,7 +16,7 @@
1516
from cppython.core.schema import ProjectConfiguration
1617
from cppython.project import Project
1718

18-
pytest_plugins = ['tests.fixtures.example', 'tests.fixtures.vcpkg']
19+
pytest_plugins = ['tests.fixtures.example', 'tests.fixtures.vcpkg', 'tests.fixtures.cmake']
1920

2021

2122
@pytest.mark.skip(reason='Address file locks.')
@@ -53,12 +54,25 @@ def _ensure_vcpkg_config(pyproject_data: dict) -> None:
5354
@staticmethod
5455
def test_simple(example_runner: CliRunner) -> None:
5556
"""Simple project"""
57+
# Read cmake_binary from the current pyproject.toml (we're in the example directory)
58+
pyproject_path = Path.cwd() / 'pyproject.toml'
59+
with pyproject_path.open('rb') as file:
60+
pyproject_data = tomllib.load(file)
61+
62+
cmake_binary = (
63+
pyproject_data.get('tool', {})
64+
.get('cppython', {})
65+
.get('generators', {})
66+
.get('cmake', {})
67+
.get('cmake_binary', 'cmake')
68+
)
69+
5670
# Create project and install dependencies
5771
project = TestVcpkgCMake._create_project(skip_upload=False)
5872
project.install()
5973

6074
# Run the CMake configuration command
61-
result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False)
75+
result = subprocess.run([cmake_binary, '--preset=default'], capture_output=True, text=True, check=False)
6276

6377
assert result.returncode == 0, f'Cmake failed: {result.stderr}'
6478

0 commit comments

Comments
 (0)