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
55 changes: 50 additions & 5 deletions cppython/plugins/cmake/resolution.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,62 @@
"""Builder to help resolve cmake state"""

import logging
import os
import shutil
from pathlib import Path
from typing import Any

from cppython.core.schema import CorePluginData
from cppython.plugins.cmake.schema import CMakeConfiguration, CMakeData


def _resolve_cmake_binary(configured_path: Path | None) -> Path | None:
"""Resolve the cmake binary path with validation.

Resolution order:
1. CMAKE_BINARY environment variable (highest priority)
2. Configured path from cmake_binary setting
3. cmake from PATH (fallback)

If a path is specified (via env or config) but doesn't exist,
a warning is logged and we fall back to PATH lookup.

Args:
configured_path: The cmake_binary path from configuration, if any

Returns:
Resolved cmake path, or None if not found anywhere
"""
logger = logging.getLogger('cppython.cmake')

# Environment variable takes precedence
if env_binary := os.environ.get('CMAKE_BINARY'):
env_path = Path(env_binary)
if env_path.exists():
return env_path
logger.warning(
'CMAKE_BINARY environment variable points to non-existent path: %s. '
'Falling back to PATH lookup.',
env_binary,
)

# Try configured path
if configured_path:
if configured_path.exists():
return configured_path
logger.warning(
'Configured cmake_binary path does not exist: %s. '
'Falling back to PATH lookup.',
configured_path,
)

# Fall back to PATH lookup
if cmake_in_path := shutil.which('cmake'):
return Path(cmake_in_path)

return None


def resolve_cmake_data(data: dict[str, Any], core_data: CorePluginData) -> CMakeData:
"""Resolves the input data table from defaults to requirements

Expand All @@ -27,11 +76,7 @@ def resolve_cmake_data(data: dict[str, Any], core_data: CorePluginData) -> CMake
modified_preset_file = root_directory / modified_preset_file

# Resolve cmake binary: environment variable takes precedence over configuration
cmake_binary: Path | None = None
if env_binary := os.environ.get('CMAKE_BINARY'):
cmake_binary = Path(env_binary)
elif parsed_data.cmake_binary:
cmake_binary = parsed_data.cmake_binary
cmake_binary = _resolve_cmake_binary(parsed_data.cmake_binary)

return CMakeData(
preset_file=modified_preset_file, configuration_name=parsed_data.configuration_name, cmake_binary=cmake_binary
Expand Down
96 changes: 50 additions & 46 deletions cppython/plugins/conan/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"""

import os
import shutil
from logging import Logger, getLogger
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -242,23 +241,6 @@ 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 @@ -269,8 +251,9 @@ def _sync_with_cmake(self, consumer: SyncConsumer) -> CMakeSyncData:
CMakeSyncData configured for Conan integration
"""
# Extract cmake_binary from CMakeGenerator if available
if isinstance(consumer, CMakeGenerator) and not os.environ.get('CMAKE_BINARY'):
self._cmake_binary = self._resolve_cmake_binary(consumer.data.cmake_binary)
# The cmake_binary is already validated and resolved during CMake data resolution
if isinstance(consumer, CMakeGenerator) and consumer.data.cmake_binary:
self._cmake_binary = str(consumer.data.cmake_binary.resolve())

return self._create_cmake_sync_data()

Expand Down Expand Up @@ -300,7 +283,11 @@ async def download_tooling(cls, directory: Path) -> None:
pass

def publish(self) -> None:
"""Publishes the package using conan create workflow."""
"""Publishes the package using conan create workflow.

Creates packages for all configured build types (e.g., Release, Debug)
to support both single-config and multi-config generators.
"""
project_root = self.core_data.project_data.project_root
conanfile_path = project_root / 'conanfile.py'
logger = getLogger('cppython.conan')
Expand All @@ -309,32 +296,13 @@ def publish(self) -> None:
raise FileNotFoundError(f'conanfile.py not found at {conanfile_path}')

try:
# Build conan create command arguments
command_args = ['create', str(conanfile_path)]

# Add build mode (build everything for publishing)
command_args.extend(['--build', 'missing'])

# Skip test dependencies during publishing
command_args.extend(['-c', 'tools.graph:skip_test=True'])
command_args.extend(['-c', 'tools.build:skip_test=True'])

# Add cmake binary configuration if specified
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}'])

# Run conan create using reusable Conan API instance
# Change to project directory since Conan API might not handle cwd like subprocess
original_cwd = os.getcwd()
try:
os.chdir(str(project_root))
self._conan_api.command.run(command_args)
finally:
os.chdir(original_cwd)
# Create packages for each configured build type
build_types = self.data.build_types
for build_type in build_types:
logger.info('Creating package for build type: %s', build_type)
self._run_conan_create(conanfile_path, build_type, logger)

# Upload if not skipped
# Upload once after all configurations are built
if not self.data.skip_upload:
self._upload_package(logger)

Expand All @@ -343,6 +311,42 @@ def publish(self) -> None:
logger.error('Conan create failed: %s', error_msg, exc_info=True)
raise ProviderInstallationError('conan', error_msg, e) from e

def _run_conan_create(self, conanfile_path: Path, build_type: str, logger: Logger) -> None:
"""Run conan create command for a specific build type.

Args:
conanfile_path: Path to the conanfile.py
build_type: Build type (Release, Debug, etc.)
logger: Logger instance
"""
# Build conan create command arguments
command_args = ['create', str(conanfile_path)]

# Add build mode (build everything for publishing)
command_args.extend(['--build', 'missing'])

# Skip test dependencies during publishing
command_args.extend(['-c', 'tools.graph:skip_test=True'])
command_args.extend(['-c', 'tools.build:skip_test=True'])

# Add build type setting
command_args.extend(['-s', f'build_type={build_type}'])

# Add cmake binary configuration if specified
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}'])

# Run conan create using reusable Conan API instance
# Change to project directory since Conan API might not handle cwd like subprocess
original_cwd = os.getcwd()
try:
os.chdir(str(self.core_data.project_data.project_root))
self._conan_api.command.run(command_args)
finally:
os.chdir(original_cwd)

def _upload_package(self, logger) -> None:
"""Upload the package to configured remotes using Conan API."""
# If no remotes configured, upload to all remotes
Expand Down
Loading