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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions cppython/plugins/conan/builder.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Construction of Conan data"""

import shutil
from pathlib import Path

from pydantic import DirectoryPath

from cppython.plugins.conan.schema import ConanDependency
from cppython.plugins.conan.schema import ConanDependency, ConanfileGenerationData


class Builder:
Expand All @@ -19,8 +20,16 @@ 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."""
"""Creates a conanfile_base.py with CPPython managed dependencies.

Args:
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', [])

# Generate requirements method content
Expand All @@ -37,6 +46,16 @@ 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.
Expand All @@ -48,6 +67,7 @@ def _create_base_conanfile(

class CPPythonBase(ConanFile):
"""Base ConanFile with CPPython managed dependencies."""
{configure_content}

def requirements(self):
"""CPPython managed requirements."""
Expand Down Expand Up @@ -135,23 +155,36 @@ def export_sources(self):
def generate_conanfile(
self,
directory: DirectoryPath,
dependencies: list[ConanDependency],
dependency_groups: dict[str, list[ConanDependency]],
name: str,
version: str,
data: ConanfileGenerationData,
) -> None:
"""Generate conanfile.py and conanfile_base.py for the project.

Always generates the base conanfile with managed dependencies.
Only creates conanfile.py if it doesn't exist (never modifies existing files).

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.
"""
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, dependencies, dependency_groups)
self._create_base_conanfile(base_file, data.dependencies, data.dependency_groups, resolved_cmake)

# Only create conanfile.py if it doesn't exist
conan_file = directory / self._filename
if not conan_file.exists():
self._create_conanfile(conan_file, name, version)
self._create_conanfile(conan_file, data.name, data.version)
45 changes: 32 additions & 13 deletions cppython/plugins/conan/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import os
import shutil
from logging import Logger, getLogger
from pathlib import Path
from typing import Any
Expand All @@ -20,7 +21,7 @@
from cppython.plugins.cmake.schema import CMakeSyncData
from cppython.plugins.conan.builder import Builder
from cppython.plugins.conan.resolution import resolve_conan_data, resolve_conan_dependency
from cppython.plugins.conan.schema import ConanData
from cppython.plugins.conan.schema import ConanData, ConanfileGenerationData
from cppython.utility.exception import NotSupportedError, ProviderInstallationError
from cppython.utility.utility import TypeName

Expand All @@ -45,8 +46,7 @@ def __init__(

self._ensure_default_profiles()

# Initialize cmake_binary with system default. It may be overridden during sync.
self._cmake_binary = 'cmake'
self._cmake_binary: str | None = None

@staticmethod
def features(directory: Path) -> SupportedFeatures:
Expand Down Expand Up @@ -116,12 +116,17 @@ def _prepare_installation(self, groups: list[str] | None = None) -> Path:
for req in self.core_data.cppython_data.dependency_groups[group_name]
]

generation_data = ConanfileGenerationData(
dependencies=resolved_dependencies,
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(
self.core_data.project_data.project_root,
resolved_dependencies,
resolved_dependency_groups,
self.core_data.pep621_data.name,
self.core_data.pep621_data.version,
generation_data,
)

# Ensure build directory exists
Expand Down Expand Up @@ -169,7 +174,7 @@ def _run_conan_install(self, conanfile_path: Path, update: bool, build_type: str
command_args.extend(['-s', f'build_type={build_type}'])

# Add cmake binary configuration if specified
if self._cmake_binary and self._cmake_binary != 'cmake':
if self._cmake_binary:
# Quote the path if it contains spaces
cmake_path = f'"{self._cmake_binary}"' if ' ' in self._cmake_binary else self._cmake_binary
command_args.extend(['-c', f'tools.cmake:cmake_program={cmake_path}'])
Expand Down Expand Up @@ -234,6 +239,23 @@ def sync_data(self, consumer: SyncConsumer) -> SyncData:

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

@staticmethod
def _resolve_cmake_binary(cmake_path: Path | str | None) -> str | None:
"""Resolve the cmake binary path.

If an explicit path is provided, use it. Otherwise, try to find cmake
in the current Python environment (venv) to ensure we use the same
cmake version for all operations including dependency builds.

Args:
cmake_path: Explicit cmake path, or None to auto-detect

Returns:
Resolved cmake path as string, or None if not found
"""
resolved = cmake_path or shutil.which('cmake')
return str(Path(resolved).resolve()) if resolved else None

def _sync_with_cmake(self, consumer: SyncConsumer) -> CMakeSyncData:
"""Synchronize with CMake generator and create sync data.

Expand All @@ -245,10 +267,7 @@ def _sync_with_cmake(self, consumer: SyncConsumer) -> CMakeSyncData:
"""
# Extract cmake_binary from CMakeGenerator if available
if isinstance(consumer, CMakeGenerator) and not os.environ.get('CMAKE_BINARY'):
# Only override if not already set from environment variable
# Convert Path to string, or use 'cmake' if None
cmake_path = consumer.data.cmake_binary
self._cmake_binary = str(cmake_path) if cmake_path else 'cmake'
self._cmake_binary = self._resolve_cmake_binary(consumer.data.cmake_binary)

return self._create_cmake_sync_data()

Expand Down Expand Up @@ -296,7 +315,7 @@ def publish(self) -> None:
command_args.extend(['-c', 'tools.build:skip_test=True'])

# Add cmake binary configuration if specified
if self._cmake_binary and self._cmake_binary != 'cmake':
if self._cmake_binary:
# Quote the path if it contains spaces
cmake_path = f'"{self._cmake_binary}"' if ' ' in self._cmake_binary else self._cmake_binary
command_args.extend(['-c', f'tools.cmake:cmake_program={cmake_path}'])
Expand Down
13 changes: 13 additions & 0 deletions cppython/plugins/conan/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,19 @@ class ConanData(CPPythonModel):
profile_dir: Path


class ConanfileGenerationData(CPPythonModel):
"""Data required for generating conanfile.py and conanfile_base.py.

Groups related parameters for conanfile generation to reduce function argument count.
"""

dependencies: list[ConanDependency]
dependency_groups: dict[str, list[ConanDependency]]
name: str
version: str
cmake_binary: str | None = None


class ConanConfiguration(CPPythonModel):
"""Conan provider configuration"""

Expand Down
Loading
Loading