Skip to content

Commit 84e2d02

Browse files
authored
Pass CMake Bin to Conan (#131)
1 parent 1b1dcd7 commit 84e2d02

File tree

4 files changed

+151
-33
lines changed

4 files changed

+151
-33
lines changed

cppython/plugins/conan/builder.py

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Construction of Conan data"""
22

3+
import shutil
34
from pathlib import Path
45

56
from pydantic import DirectoryPath
67

7-
from cppython.plugins.conan.schema import ConanDependency
8+
from cppython.plugins.conan.schema import ConanDependency, ConanfileGenerationData
89

910

1011
class Builder:
@@ -19,8 +20,16 @@ def _create_base_conanfile(
1920
base_file: Path,
2021
dependencies: list[ConanDependency],
2122
dependency_groups: dict[str, list[ConanDependency]],
23+
cmake_binary: Path | None = None,
2224
) -> None:
23-
"""Creates a conanfile_base.py with CPPython managed dependencies."""
25+
"""Creates a conanfile_base.py with CPPython managed dependencies.
26+
27+
Args:
28+
base_file: Path to write the base conanfile
29+
dependencies: List of main dependencies
30+
dependency_groups: Dictionary of dependency groups (e.g., 'test')
31+
cmake_binary: Optional path to CMake binary to use
32+
"""
2433
test_dependencies = dependency_groups.get('test', [])
2534

2635
# Generate requirements method content
@@ -37,6 +46,16 @@ def _create_base_conanfile(
3746
'\n'.join(test_requires_lines) if test_requires_lines else ' pass # No test requirements'
3847
)
3948

49+
# Generate configure method content for cmake_program if specified
50+
if cmake_binary:
51+
# Use forward slashes for cross-platform compatibility in Conan
52+
cmake_path_str = str(cmake_binary.resolve()).replace('\\', '/')
53+
configure_content = f''' def configure(self):
54+
"""CPPython managed configuration."""
55+
self.conf.define("tools.cmake:cmake_program", "{cmake_path_str}")'''
56+
else:
57+
configure_content = ''
58+
4059
content = f'''"""CPPython managed base ConanFile.
4160
4261
This file is auto-generated by CPPython. Do not edit manually.
@@ -48,6 +67,7 @@ def _create_base_conanfile(
4867
4968
class CPPythonBase(ConanFile):
5069
"""Base ConanFile with CPPython managed dependencies."""
70+
{configure_content}
5171
5272
def requirements(self):
5373
"""CPPython managed requirements."""
@@ -135,23 +155,36 @@ def export_sources(self):
135155
def generate_conanfile(
136156
self,
137157
directory: DirectoryPath,
138-
dependencies: list[ConanDependency],
139-
dependency_groups: dict[str, list[ConanDependency]],
140-
name: str,
141-
version: str,
158+
data: ConanfileGenerationData,
142159
) -> None:
143160
"""Generate conanfile.py and conanfile_base.py for the project.
144161
145162
Always generates the base conanfile with managed dependencies.
146163
Only creates conanfile.py if it doesn't exist (never modifies existing files).
164+
165+
Args:
166+
directory: The project directory
167+
data: Generation data containing dependencies, project info, and cmake binary path.
168+
If cmake_binary is not provided, attempts to find cmake in the current
169+
Python environment.
147170
"""
148171
directory.mkdir(parents=True, exist_ok=True)
149172

173+
# Resolve cmake binary path
174+
resolved_cmake: Path | None = None
175+
if data.cmake_binary and data.cmake_binary != 'cmake':
176+
resolved_cmake = Path(data.cmake_binary).resolve()
177+
else:
178+
# Try to find cmake in the current Python environment (venv)
179+
cmake_path = shutil.which('cmake')
180+
if cmake_path:
181+
resolved_cmake = Path(cmake_path).resolve()
182+
150183
# Always regenerate the base conanfile with managed dependencies
151184
base_file = directory / 'conanfile_base.py'
152-
self._create_base_conanfile(base_file, dependencies, dependency_groups)
185+
self._create_base_conanfile(base_file, data.dependencies, data.dependency_groups, resolved_cmake)
153186

154187
# Only create conanfile.py if it doesn't exist
155188
conan_file = directory / self._filename
156189
if not conan_file.exists():
157-
self._create_conanfile(conan_file, name, version)
190+
self._create_conanfile(conan_file, data.name, data.version)

cppython/plugins/conan/plugin.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from cppython.plugins.cmake.schema import CMakeSyncData
2121
from cppython.plugins.conan.builder import Builder
2222
from cppython.plugins.conan.resolution import resolve_conan_data, resolve_conan_dependency
23-
from cppython.plugins.conan.schema import ConanData
23+
from cppython.plugins.conan.schema import ConanData, ConanfileGenerationData
2424
from cppython.utility.exception import NotSupportedError, ProviderInstallationError
2525
from cppython.utility.utility import TypeName
2626

@@ -116,12 +116,17 @@ def _prepare_installation(self, groups: list[str] | None = None) -> Path:
116116
for req in self.core_data.cppython_data.dependency_groups[group_name]
117117
]
118118

119+
generation_data = ConanfileGenerationData(
120+
dependencies=resolved_dependencies,
121+
dependency_groups=resolved_dependency_groups,
122+
name=self.core_data.pep621_data.name,
123+
version=self.core_data.pep621_data.version,
124+
cmake_binary=self._cmake_binary,
125+
)
126+
119127
self.builder.generate_conanfile(
120128
self.core_data.project_data.project_root,
121-
resolved_dependencies,
122-
resolved_dependency_groups,
123-
self.core_data.pep621_data.name,
124-
self.core_data.pep621_data.version,
129+
generation_data,
125130
)
126131

127132
# Ensure build directory exists

cppython/plugins/conan/schema.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,19 @@ class ConanData(CPPythonModel):
297297
profile_dir: Path
298298

299299

300+
class ConanfileGenerationData(CPPythonModel):
301+
"""Data required for generating conanfile.py and conanfile_base.py.
302+
303+
Groups related parameters for conanfile generation to reduce function argument count.
304+
"""
305+
306+
dependencies: list[ConanDependency]
307+
dependency_groups: dict[str, list[ConanDependency]]
308+
name: str
309+
version: str
310+
cmake_binary: str | None = None
311+
312+
300313
class ConanConfiguration(CPPythonModel):
301314
"""Conan provider configuration"""
302315

tests/unit/plugins/conan/test_builder.py

Lines changed: 87 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pytest
77

88
from cppython.plugins.conan.builder import Builder
9-
from cppython.plugins.conan.schema import ConanDependency, ConanVersion
9+
from cppython.plugins.conan.schema import ConanDependency, ConanfileGenerationData, ConanVersion
1010

1111

1212
class TestBuilder:
@@ -44,13 +44,16 @@ def test_creates_both_files(self, builder: Builder, tmp_path: Path) -> None:
4444
]
4545
dependency_groups = {}
4646

47-
builder.generate_conanfile(
48-
directory=tmp_path,
47+
data = ConanfileGenerationData(
4948
dependencies=dependencies,
5049
dependency_groups=dependency_groups,
5150
name='test-project',
5251
version='1.0.0',
5352
)
53+
builder.generate_conanfile(
54+
directory=tmp_path,
55+
data=data,
56+
)
5457

5558
base_file = tmp_path / 'conanfile_base.py'
5659
conan_file = tmp_path / 'conanfile.py'
@@ -60,37 +63,43 @@ def test_creates_both_files(self, builder: Builder, tmp_path: Path) -> None:
6063

6164
def test_regenerates_base_file(self, builder: Builder, tmp_path: Path) -> None:
6265
"""Test base file is always regenerated with new dependencies."""
63-
dependencies_v1 = [
66+
initial_dependencies = [
6467
ConanDependency(name='boost', version=ConanVersion.from_string('1.80.0')),
6568
]
6669

67-
builder.generate_conanfile(
68-
directory=tmp_path,
69-
dependencies=dependencies_v1,
70+
initial_data = ConanfileGenerationData(
71+
dependencies=initial_dependencies,
7072
dependency_groups={},
7173
name='test-project',
7274
version='1.0.0',
7375
)
76+
builder.generate_conanfile(
77+
directory=tmp_path,
78+
data=initial_data,
79+
)
7480

7581
base_file = tmp_path / 'conanfile_base.py'
76-
content_v1 = base_file.read_text(encoding='utf-8')
77-
assert 'boost/1.80.0' in content_v1
82+
initial_content = base_file.read_text(encoding='utf-8')
83+
assert 'boost/1.80.0' in initial_content
7884

79-
dependencies_v2 = [
85+
updated_dependencies = [
8086
ConanDependency(name='zlib', version=ConanVersion.from_string('1.2.13')),
8187
]
8288

83-
builder.generate_conanfile(
84-
directory=tmp_path,
85-
dependencies=dependencies_v2,
89+
updated_data = ConanfileGenerationData(
90+
dependencies=updated_dependencies,
8691
dependency_groups={},
8792
name='test-project',
8893
version='1.0.0',
8994
)
95+
builder.generate_conanfile(
96+
directory=tmp_path,
97+
data=updated_data,
98+
)
9099

91-
content_v2 = base_file.read_text(encoding='utf-8')
92-
assert 'zlib/1.2.13' in content_v2
93-
assert 'boost/1.80.0' not in content_v2
100+
updated_content = base_file.read_text(encoding='utf-8')
101+
assert 'zlib/1.2.13' in updated_content
102+
assert 'boost/1.80.0' not in updated_content
94103

95104
def test_preserves_user_file(self, builder: Builder, tmp_path: Path) -> None:
96105
"""Test user conanfile is never modified once created."""
@@ -112,13 +121,16 @@ def requirements(self):
112121
ConanDependency(name='boost', version=ConanVersion.from_string('1.80.0')),
113122
]
114123

115-
builder.generate_conanfile(
116-
directory=tmp_path,
124+
data = ConanfileGenerationData(
117125
dependencies=dependencies,
118126
dependency_groups={},
119127
name='new-name',
120128
version='2.0.0',
121129
)
130+
builder.generate_conanfile(
131+
directory=tmp_path,
132+
data=data,
133+
)
122134

123135
final_content = conan_file.read_text()
124136
assert final_content == custom_content
@@ -137,13 +149,16 @@ def test_inheritance_chain(self, builder: Builder, tmp_path: Path) -> None:
137149
]
138150
}
139151

140-
builder.generate_conanfile(
141-
directory=tmp_path,
152+
data = ConanfileGenerationData(
142153
dependencies=dependencies,
143154
dependency_groups=dependency_groups,
144155
name='test-project',
145156
version='1.0.0',
146157
)
158+
builder.generate_conanfile(
159+
directory=tmp_path,
160+
data=data,
161+
)
147162

148163
base_content = (tmp_path / 'conanfile_base.py').read_text(encoding='utf-8')
149164
user_content = (tmp_path / 'conanfile.py').read_text(encoding='utf-8')
@@ -156,3 +171,55 @@ def test_inheritance_chain(self, builder: Builder, tmp_path: Path) -> None:
156171
assert 'class TestProjectPackage(CPPythonBase):' in user_content
157172
assert 'super().requirements()' in user_content
158173
assert 'super().build_requirements()' in user_content
174+
175+
def test_cmake_binary_configure(self, builder: Builder, tmp_path: Path) -> None:
176+
"""Test that cmake_binary generates configure() with forward slashes."""
177+
base_file = tmp_path / 'conanfile_base.py'
178+
cmake_path = Path('C:/Program Files/CMake/bin/cmake.exe')
179+
180+
builder._create_base_conanfile(base_file, [], {}, cmake_binary=cmake_path)
181+
182+
content = base_file.read_text(encoding='utf-8')
183+
assert 'def configure(self):' in content
184+
assert 'self.conf.define("tools.cmake:cmake_program"' in content
185+
assert 'C:/Program Files/CMake/bin/cmake.exe' in content
186+
assert '\\' not in content.split('tools.cmake:cmake_program')[1].split('"')[1]
187+
188+
def test_no_cmake_binary(self, builder: Builder, tmp_path: Path) -> None:
189+
"""Test that no cmake_binary means no configure() method."""
190+
base_file = tmp_path / 'conanfile_base.py'
191+
192+
builder._create_base_conanfile(base_file, [], {}, cmake_binary=None)
193+
194+
content = base_file.read_text(encoding='utf-8')
195+
assert 'def configure(self):' not in content
196+
197+
@pytest.mark.parametrize(
198+
('venv_cmake', 'expect_configure'),
199+
[
200+
('/path/to/venv/bin/cmake', True),
201+
(None, False),
202+
],
203+
)
204+
def test_cmake_binary_venv_fallback(
205+
self,
206+
builder: Builder,
207+
tmp_path: Path,
208+
monkeypatch: pytest.MonkeyPatch,
209+
venv_cmake: str | None,
210+
expect_configure: bool,
211+
) -> None:
212+
"""Test venv cmake fallback when cmake_binary is default."""
213+
monkeypatch.setattr('cppython.plugins.conan.builder.shutil.which', lambda _: venv_cmake)
214+
215+
data = ConanfileGenerationData(
216+
dependencies=[],
217+
dependency_groups={},
218+
name='test-project',
219+
version='1.0.0',
220+
cmake_binary='cmake',
221+
)
222+
builder.generate_conanfile(directory=tmp_path, data=data)
223+
224+
content = (tmp_path / 'conanfile_base.py').read_text(encoding='utf-8')
225+
assert ('def configure(self):' in content) == expect_configure

0 commit comments

Comments
 (0)