diff --git a/cppython/plugins/cmake/resolution.py b/cppython/plugins/cmake/resolution.py index f9ee0b5..c55b381 100644 --- a/cppython/plugins/cmake/resolution.py +++ b/cppython/plugins/cmake/resolution.py @@ -1,6 +1,8 @@ """Builder to help resolve cmake state""" +import logging import os +import shutil from pathlib import Path from typing import Any @@ -8,6 +10,53 @@ 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 @@ -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 diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index 9e730d2..1197532 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -6,7 +6,6 @@ """ import os -import shutil from logging import Logger, getLogger from pathlib import Path from typing import Any @@ -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. @@ -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() @@ -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') @@ -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) @@ -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