diff --git a/.vscode/settings.json b/.vscode/settings.json index caac882e..30db100c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,5 +14,6 @@ "source.organizeImports": "explicit" }, "editor.defaultFormatter": "charliermarsh.ruff" - } + }, + "cmake.ignoreCMakeListsMissing": true } \ No newline at end of file diff --git a/cppython/builder.py b/cppython/builder.py index 25fd4517..fb0faf54 100644 --- a/cppython/builder.py +++ b/cppython/builder.py @@ -1,11 +1,16 @@ """Defines the data and routines for building a CPPython project type""" import logging +import os from importlib.metadata import entry_points from inspect import getmodule from logging import Logger +from pprint import pformat from typing import Any, cast +from rich.console import Console +from rich.logging import RichHandler + from cppython.core.plugin_schema.generator import Generator from cppython.core.plugin_schema.provider import Provider from cppython.core.plugin_schema.scm import SCM, SupportedSCMFeatures @@ -471,8 +476,28 @@ def __init__(self, project_configuration: ProjectConfiguration, logger: Logger) self._project_configuration = project_configuration self._logger = logger - # Add default output stream - self._logger.addHandler(logging.StreamHandler()) + # Informal standard to check for color + force_color = os.getenv('FORCE_COLOR', '1') != '0' + + console = Console( + force_terminal=force_color, + color_system='auto', + width=120, + legacy_windows=False, + no_color=False, + ) + + rich_handler = RichHandler( + console=console, + rich_tracebacks=True, + show_time=False, + show_path=False, + markup=True, + show_level=False, + enable_link_path=False, + ) + + self._logger.addHandler(rich_handler) self._logger.setLevel(Builder.levels[project_configuration.verbosity]) self._logger.info('Logging setup complete') @@ -532,4 +557,6 @@ def build( plugins = Plugins(generator=generator, provider=provider, scm=scm) + self._logger.debug('Project data:\n%s', pformat(dict(core_data))) + return Data(core_data, plugins, self._logger) diff --git a/cppython/core/schema.py b/cppython/core/schema.py index 7b43589c..3678c659 100644 --- a/cppython/core/schema.py +++ b/cppython/core/schema.py @@ -44,7 +44,7 @@ class ProjectConfiguration(CPPythonModel, extra='forbid'): bool, Field(description='Debug mode. Additional processing will happen to expose more debug information') ] = False - @field_validator('verbosity') # type: ignore + @field_validator('verbosity') @classmethod def min_max(cls, value: int) -> int: """Validator that clamps the input value @@ -121,7 +121,7 @@ class CPPythonData(CPPythonModel, extra='forbid'): provider_data: Annotated[dict[str, Any], Field(description='Resolved provider configuration data')] generator_data: Annotated[dict[str, Any], Field(description='Resolved generator configuration data')] - @field_validator('configuration_path', 'install_path', 'tool_path', 'build_path') # type: ignore + @field_validator('configuration_path', 'install_path', 'tool_path', 'build_path') @classmethod def validate_absolute_path(cls, value: Path) -> Path: """Enforce the input is an absolute path diff --git a/cppython/plugins/cmake/builder.py b/cppython/plugins/cmake/builder.py index 659a79b0..b264cd70 100644 --- a/cppython/plugins/cmake/builder.py +++ b/cppython/plugins/cmake/builder.py @@ -2,7 +2,13 @@ from pathlib import Path -from cppython.plugins.cmake.schema import CMakeData, CMakePresets, CMakeSyncData, ConfigurePreset +from cppython.plugins.cmake.schema import ( + BuildPreset, + CMakeData, + CMakePresets, + CMakeSyncData, + ConfigurePreset, +) class Builder: @@ -11,89 +17,58 @@ class Builder: def __init__(self) -> None: """Initialize the builder""" - @staticmethod - def generate_provider_preset(provider_data: CMakeSyncData) -> CMakePresets: - """Generates a provider preset from input sync data - - Args: - provider_directory: The base directory to place the preset files - provider_data: The providers synchronization data - """ - generated_configure_preset = ConfigurePreset(name=provider_data.provider_name, hidden=True) - - # Toss in that sync data from the provider - generated_configure_preset.cacheVariables = { - 'CMAKE_PROJECT_TOP_LEVEL_INCLUDES': str(provider_data.top_level_includes.as_posix()), - } - - return CMakePresets(configurePresets=[generated_configure_preset]) - - @staticmethod - def write_provider_preset(provider_directory: Path, provider_data: CMakeSyncData) -> None: - """Writes a provider preset from input sync data - - Args: - provider_directory: The base directory to place the preset files - provider_data: The providers synchronization data - """ - generated_preset = Builder.generate_provider_preset(provider_data) - - provider_preset_file = provider_directory / f'{provider_data.provider_name}.json' - - initial_preset = None - - # If the file already exists, we need to compare it - if provider_preset_file.exists(): - with open(provider_preset_file, encoding='utf-8') as file: - initial_json = file.read() - initial_preset = CMakePresets.model_validate_json(initial_json) - - if generated_preset != initial_preset: - serialized = generated_preset.model_dump_json(exclude_none=True, by_alias=False, indent=4) - with open(provider_preset_file, 'w', encoding='utf8') as file: - file.write(serialized) - @staticmethod def generate_cppython_preset( - cppython_preset_directory: Path, provider_directory: Path, provider_data: CMakeSyncData + cppython_preset_directory: Path, provider_preset_file: Path, provider_data: CMakeSyncData ) -> CMakePresets: """Generates the cppython preset which inherits from the provider presets Args: cppython_preset_directory: The tool directory - provider_directory: The base directory containing provider presets + provider_preset_file: Path to the provider's preset file provider_data: The provider's synchronization data Returns: A CMakePresets object """ - generated_configure_preset = ConfigurePreset(name='cppython', inherits=provider_data.provider_name, hidden=True) - generated_preset = CMakePresets(configurePresets=[generated_configure_preset]) + configure_presets = [] - # Get the relative path to the provider preset file - provider_preset_file = provider_directory / f'{provider_data.provider_name}.json' - relative_preset = provider_preset_file.relative_to(cppython_preset_directory, walk_up=True).as_posix() + preset_name = 'cppython' + + # Create a default preset that inherits from provider's default preset + default_configure = ConfigurePreset( + name=preset_name, + hidden=True, + description='Injected configuration preset for CPPython', + ) + + if provider_data.toolchain_file: + default_configure.toolchainFile = provider_data.toolchain_file.as_posix() + + configure_presets.append(default_configure) + + generated_preset = CMakePresets( + configurePresets=configure_presets, + ) - # Set the data - generated_preset.include = [relative_preset] return generated_preset @staticmethod def write_cppython_preset( - cppython_preset_directory: Path, provider_directory: Path, provider_data: CMakeSyncData + cppython_preset_directory: Path, provider_preset_file: Path, provider_data: CMakeSyncData ) -> Path: """Write the cppython presets which inherit from the provider presets Args: cppython_preset_directory: The tool directory - provider_directory: The base directory containing provider presets + provider_preset_file: Path to the provider's preset file provider_data: The provider's synchronization data Returns: A file path to the written data """ generated_preset = Builder.generate_cppython_preset( - cppython_preset_directory, provider_directory, provider_data + cppython_preset_directory, provider_preset_file, provider_data ) cppython_preset_file = cppython_preset_directory / 'cppython.json' @@ -114,66 +89,195 @@ def write_cppython_preset( return cppython_preset_file @staticmethod - def generate_root_preset( - preset_file: Path, cppython_preset_file: Path, cmake_data: CMakeData, build_directory: Path - ) -> CMakePresets: - """Generates the top level root preset with the include reference. + def _create_presets( + cmake_data: CMakeData, build_directory: Path + ) -> tuple[list[ConfigurePreset], list[BuildPreset]]: + """Create the default configure and build presets for the user. Args: - preset_file: Preset file to modify - cppython_preset_file: Path to the cppython preset file to include cmake_data: The CMake data to use build_directory: The build directory to use Returns: - A CMakePresets object + A tuple containing the configure preset and list of build presets """ - default_configure_preset = ConfigurePreset( - name=cmake_data.configuration_name, - inherits='cppython', - binaryDir=build_directory.as_posix(), - cacheVariables={ - 'CMAKE_BUILD_TYPE': 'Release' # Ensure compatibility for single-config and multi-config generators - }, + user_configure_presets: list[ConfigurePreset] = [] + user_build_presets: list[BuildPreset] = [] + + name = cmake_data.configuration_name + release_name = name + '-release' + debug_name = name + '-debug' + + user_configure_presets.append( + ConfigurePreset( + name=name, + description='All multi-configuration generators should inherit from this preset', + inherits='cppython', + binaryDir='${sourceDir}/' + build_directory.as_posix(), + cacheVariables={'CMAKE_CONFIGURATION_TYPES': 'Debug;Release'}, + ) ) - if preset_file.exists(): - with open(preset_file, encoding='utf-8') as file: - initial_json = file.read() - root_preset = CMakePresets.model_validate_json(initial_json) - - if root_preset.configurePresets is None: - root_preset.configurePresets = [default_configure_preset] - - # Set defaults - preset = next((p for p in root_preset.configurePresets if p.name == default_configure_preset.name), None) - if preset: - # If the name matches, we need to verify it inherits from cppython - if preset.inherits is None: - preset.inherits = 'cppython' - elif isinstance(preset.inherits, str) and preset.inherits != 'cppython': - preset.inherits = [preset.inherits, 'cppython'] - elif isinstance(preset.inherits, list) and 'cppython' not in preset.inherits: - preset.inherits.append('cppython') - else: - root_preset.configurePresets.append(default_configure_preset) + user_configure_presets.append( + ConfigurePreset( + name=release_name, + description='All single-configuration generators should inherit from this preset', + inherits=name, + cacheVariables={'CMAKE_BUILD_TYPE': 'Release'}, + ) + ) + + user_configure_presets.append( + ConfigurePreset( + name=debug_name, + description='All single-configuration generators should inherit from this preset', + inherits=name, + cacheVariables={'CMAKE_BUILD_TYPE': 'Debug'}, + ) + ) + + user_build_presets.append( + BuildPreset( + name=release_name, + description='An example build preset for release', + configurePreset=release_name, + ) + ) + + user_build_presets.append( + BuildPreset( + name=debug_name, + description='An example build preset for debug', + configurePreset=debug_name, + ) + ) + return user_configure_presets, user_build_presets + + @staticmethod + def _load_existing_preset(preset_file: Path) -> CMakePresets | None: + """Load existing preset file if it exists. + + Args: + preset_file: Path to the preset file + + Returns: + CMakePresets object if file exists, None otherwise + """ + if not preset_file.exists(): + return None + + with open(preset_file, encoding='utf-8') as file: + initial_json = file.read() + return CMakePresets.model_validate_json(initial_json) + + @staticmethod + def _update_configure_preset(existing_preset: ConfigurePreset, build_directory: Path) -> None: + """Update an existing configure preset to ensure proper inheritance and binary directory. + + Args: + existing_preset: The preset to update + build_directory: The build directory to use + """ + # Update existing preset to ensure it inherits from 'cppython' + if existing_preset.inherits is None: + existing_preset.inherits = 'cppython' # type: ignore[misc] + elif isinstance(existing_preset.inherits, str) and existing_preset.inherits != 'cppython': + existing_preset.inherits = ['cppython', existing_preset.inherits] # type: ignore[misc] + elif isinstance(existing_preset.inherits, list) and 'cppython' not in existing_preset.inherits: + existing_preset.inherits.insert(0, 'cppython') + + # Update binary directory if not set + if not existing_preset.binaryDir: + existing_preset.binaryDir = '${sourceDir}/' + build_directory.as_posix() # type: ignore[misc] + + @staticmethod + def _modify_presets( + root_preset: CMakePresets, + user_configure_presets: list[ConfigurePreset], + user_build_presets: list[BuildPreset], + build_directory: Path, + ) -> None: + """Handle presets in the root preset. + + Args: + root_preset: The root preset to modify + user_configure_presets: The user's configure presets + user_build_presets: The user's build presets + build_directory: The build directory to use + """ + if root_preset.configurePresets is None: + root_preset.configurePresets = user_configure_presets.copy() # type: ignore[misc] else: - # If the file doesn't exist, we need to default it for the user - root_preset = CMakePresets(configurePresets=[default_configure_preset]) + # Update or add the user's configure preset + for user_configure_preset in user_configure_presets: + existing_preset = next( + (p for p in root_preset.configurePresets if p.name == user_configure_preset.name), None + ) + if existing_preset: + Builder._update_configure_preset(existing_preset, build_directory) + else: + root_preset.configurePresets.append(user_configure_preset) + + if root_preset.buildPresets is None: + root_preset.buildPresets = user_build_presets.copy() # type: ignore[misc] + else: + # Add build presets if they don't exist + for build_preset in user_build_presets: + existing = next((p for p in root_preset.buildPresets if p.name == build_preset.name), None) + if not existing: + root_preset.buildPresets.append(build_preset) + + @staticmethod + def _modify_includes(root_preset: CMakePresets, preset_file: Path, cppython_preset_file: Path) -> None: + """Handle include paths in the root preset. + Args: + root_preset: The root preset to modify + preset_file: Path to the preset file + cppython_preset_file: Path to the cppython preset file to include + """ # Get the relative path to the cppython preset file preset_directory = preset_file.parent.absolute() relative_preset = cppython_preset_file.relative_to(preset_directory, walk_up=True).as_posix() - # If the include key doesn't exist, we know we will write to disk afterwards + # Handle includes if not root_preset.include: - root_preset.include = [] + root_preset.include = [] # type: ignore[misc] - # Only the included preset file if it doesn't exist. Implied by the above check if str(relative_preset) not in root_preset.include: root_preset.include.append(str(relative_preset)) + @staticmethod + def generate_root_preset( + preset_file: Path, cppython_preset_file: Path, cmake_data: CMakeData, build_directory: Path + ) -> CMakePresets: + """Generates the top level root preset with the include reference. + + Args: + preset_file: Preset file to modify + cppython_preset_file: Path to the cppython preset file to include + cmake_data: The CMake data to use + build_directory: The build directory to use + + Returns: + A CMakePresets object + """ + # Create user presets + user_configure_presets, user_build_presets = Builder._create_presets(cmake_data, build_directory) + + # Load existing preset or create new one + root_preset = Builder._load_existing_preset(preset_file) + if root_preset is None: + root_preset = CMakePresets( + configurePresets=user_configure_presets, + buildPresets=user_build_presets, + ) + else: + Builder._modify_presets(root_preset, user_configure_presets, user_build_presets, build_directory) + + Builder._modify_includes(root_preset, preset_file, cppython_preset_file) + return root_preset @staticmethod @@ -200,6 +304,9 @@ def write_root_presets( initial_json = file.read() initial_root_preset = CMakePresets.model_validate_json(initial_json) + # Ensure that the build_directory is relative to the preset_file, allowing upward traversal + build_directory = build_directory.relative_to(preset_file.parent, walk_up=True) + root_preset = Builder.generate_root_preset(preset_file, cppython_preset_file, cmake_data, build_directory) # Only write the file if the data has changed diff --git a/cppython/plugins/cmake/plugin.py b/cppython/plugins/cmake/plugin.py index 052ef7a4..e008a70a 100644 --- a/cppython/plugins/cmake/plugin.py +++ b/cppython/plugins/cmake/plugin.py @@ -63,12 +63,11 @@ def sync(self, sync_data: SyncData) -> None: match sync_data: case CMakeSyncData(): self._cppython_preset_directory.mkdir(parents=True, exist_ok=True) - self._provider_directory.mkdir(parents=True, exist_ok=True) - self.builder.write_provider_preset(self._provider_directory, sync_data) + cppython_preset_file = self._cppython_preset_directory / 'CPPython.json' cppython_preset_file = self.builder.write_cppython_preset( - self._cppython_preset_directory, self._provider_directory, sync_data + self._cppython_preset_directory, cppython_preset_file, sync_data ) self.builder.write_root_presets( diff --git a/cppython/plugins/cmake/schema.py b/cppython/plugins/cmake/schema.py index eac1ed6d..6571f5b8 100644 --- a/cppython/plugins/cmake/schema.py +++ b/cppython/plugins/cmake/schema.py @@ -10,7 +10,6 @@ from typing import Annotated from pydantic import Field -from pydantic.types import FilePath from cppython.core.schema import CPPythonModel, SyncData @@ -47,6 +46,8 @@ class ConfigurePreset(CPPythonModel, extra='allow'): """Partial Configure Preset specification to allow cache variable injection""" name: str + description: Annotated[str | None, Field(description='A human-readable description of the preset.')] = None + hidden: Annotated[bool | None, Field(description='If true, the preset is hidden and cannot be used directly.')] = ( None ) @@ -58,26 +59,51 @@ class ConfigurePreset(CPPythonModel, extra='allow'): str | None, Field(description='The path to the output binary directory.'), ] = None + toolchainFile: Annotated[ + str | Path | None, + Field(description='Path to the toolchain file.'), + ] = None cacheVariables: dict[str, None | bool | str | CacheVariable] | None = None -class CMakePresets(CPPythonModel, extra='allow'): - """The schema for the CMakePresets and CMakeUserPresets files. +class BuildPreset(CPPythonModel, extra='allow'): + """Partial Build Preset specification for CMake build presets""" - The only information needed is the configure preset list for cache variable injection - """ + name: str + description: Annotated[str | None, Field(description='A human-readable description of the preset.')] = None + + hidden: Annotated[bool | None, Field(description='If true, the preset is hidden and cannot be used directly.')] = ( + None + ) + + inherits: Annotated[ + str | list[str] | None, Field(description='The inherits field allows inheriting from other presets.') + ] = None + configurePreset: Annotated[ + str | None, + Field(description='The name of a configure preset to associate with this build preset.'), + ] = None + configuration: Annotated[ + str | None, + Field(description='Build configuration. Equivalent to --config on the command line.'), + ] = None + + +class CMakePresets(CPPythonModel, extra='allow'): + """The schema for the CMakePresets and CMakeUserPresets files.""" version: Annotated[int, Field(description='The version of the JSON schema.')] = 9 include: Annotated[ list[str] | None, Field(description='The include field allows inheriting from another preset.') ] = None configurePresets: Annotated[list[ConfigurePreset] | None, Field(description='The list of configure presets')] = None + buildPresets: Annotated[list[BuildPreset] | None, Field(description='The list of build presets')] = None class CMakeSyncData(SyncData): """The CMake sync data""" - top_level_includes: FilePath + toolchain_file: Path | None = None class CMakeData(CPPythonModel): @@ -98,5 +124,10 @@ class CMakeConfiguration(CPPythonModel): ), ] = Path('CMakePresets.json') configuration_name: Annotated[ - str, Field(description='The CMake configuration preset to look for and override inside the given `preset_file`') + str, + Field( + description='The CMake configuration preset to look for and override inside the given `preset_file`. ' + 'Additional configurations will be added using this option as the base. For example, given "default", ' + '"default-release" will also be written' + ), ] = 'default' diff --git a/cppython/plugins/conan/builder.py b/cppython/plugins/conan/builder.py index 2be317d6..9ae94e83 100644 --- a/cppython/plugins/conan/builder.py +++ b/cppython/plugins/conan/builder.py @@ -121,30 +121,53 @@ def __init__(self) -> None: self._filename = 'conanfile.py' @staticmethod - def _create_conanfile(conan_file: Path, dependencies: list[ConanDependency]) -> None: + def _create_conanfile(conan_file: Path, dependencies: list[ConanDependency], name: str, version: str) -> None: """Creates a conanfile.py file with the necessary content.""" template_string = """ from conan import ConanFile - from conan.tools.cmake import CMake, cmake_layout + from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout + from conan.tools.files import copy - class MyProject(ConanFile): - name = "myproject" - version = "1.0" + class AutoPackage(ConanFile): + name = "${name}" + version = "${version}" settings = "os", "compiler", "build_type", "arch" requires = ${dependencies} - generators = "CMakeDeps" def layout(self): cmake_layout(self) + def generate(self): + deps = CMakeDeps(self) + deps.generate() + tc = CMakeToolchain(self) + tc.user_presets_path = None + tc.generate() + def build(self): cmake = CMake(self) cmake.configure() - cmake.build()""" + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.libs = ["${name}"] + + def export_sources(self): + copy(self, "CMakeLists.txt", src=self.recipe_folder, dst=self.export_sources_folder) + copy(self, "include/*", src=self.recipe_folder, dst=self.export_sources_folder) + copy(self, "src/*", src=self.recipe_folder, dst=self.export_sources_folder) + copy(self, "cmake/*", src=self.recipe_folder, dst=self.export_sources_folder) + """ template = Template(dedent(template_string)) values = { + 'name': name, + 'version': version, 'dependencies': [dependency.requires() for dependency in dependencies], } @@ -153,7 +176,9 @@ def build(self): with open(conan_file, 'w', encoding='utf-8') as file: file.write(result) - def generate_conanfile(self, directory: DirectoryPath, dependencies: list[ConanDependency]) -> None: + def generate_conanfile( + self, directory: DirectoryPath, dependencies: list[ConanDependency], name: str, version: str + ) -> None: """Generate a conanfile.py file for the project.""" conan_file = directory / self._filename @@ -167,4 +192,4 @@ def generate_conanfile(self, directory: DirectoryPath, dependencies: list[ConanD conan_file.write_text(modified.code, encoding='utf-8') else: directory.mkdir(parents=True, exist_ok=True) - self._create_conanfile(conan_file, dependencies) + self._create_conanfile(conan_file, dependencies, name, version) diff --git a/cppython/plugins/conan/plugin.py b/cppython/plugins/conan/plugin.py index 0e4ebd82..42abcddd 100644 --- a/cppython/plugins/conan/plugin.py +++ b/cppython/plugins/conan/plugin.py @@ -5,12 +5,13 @@ installation, and synchronization with other tools. """ +import os +from logging import Logger, getLogger from pathlib import Path from typing import Any -import requests from conan.api.conan_api import ConanAPI -from conan.api.model import ListPattern +from conan.cli.cli import Cli from cppython.core.plugin_schema.generator import SyncConsumer from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData, SupportedProviderFeatures @@ -20,15 +21,13 @@ 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.utility.exception import NotSupportedError, ProviderConfigurationError, ProviderInstallationError +from cppython.utility.exception import NotSupportedError, ProviderInstallationError from cppython.utility.utility import TypeName class ConanProvider(Provider): """Conan Provider""" - _provider_url = 'https://raw.githubusercontent.com/conan-io/cmake-conan/refs/heads/develop2/conan_provider.cmake' - def __init__( self, group_data: ProviderPluginGroupData, core_data: CorePluginData, configuration_data: dict[str, Any] ) -> None: @@ -38,15 +37,13 @@ def __init__( self.data: ConanData = resolve_conan_data(configuration_data, core_data) self.builder = Builder() + # Initialize ConanAPI once and reuse it + self._conan_api = ConanAPI() + # Initialize CLI for command API to work properly + self._cli = Cli(self._conan_api) + self._cli.add_commands() - @staticmethod - def _download_file(url: str, file: Path) -> None: - """Replaces the given file with the contents of the url""" - file.parent.mkdir(parents=True, exist_ok=True) - - with open(file, 'wb') as out_file: - content = requests.get(url, stream=True).content - out_file.write(content) + self._ensure_default_profiles() @staticmethod def features(directory: Path) -> SupportedFeatures: @@ -70,127 +67,104 @@ def information() -> Information: return Information() def _install_dependencies(self, *, update: bool = False) -> None: - """Install/update dependencies using Conan API. + """Install/update dependencies using Conan CLI. Args: update: If True, check remotes for newer versions/revisions and install those. If False, use cached versions when available. """ operation = 'update' if update else 'install' + logger = getLogger('cppython.conan') try: # Setup environment and generate conanfile - conan_api, conanfile_path = self._prepare_installation() + conanfile_path = self._prepare_installation() except Exception as e: raise ProviderInstallationError('conan', f'Failed to prepare {operation} environment: {e}', e) from e try: - # Load dependency graph - deps_graph = self._load_dependency_graph(conan_api, conanfile_path, update) - except Exception as e: - raise ProviderInstallationError('conan', f'Failed to load dependency graph: {e}', e) from e - - try: - # Install dependencies - self._install_binaries(conan_api, deps_graph, update) + build_types = ['Release', 'Debug'] + for build_type in build_types: + logger.info('Installing dependencies for build type: %s', build_type) + self._run_conan_install(conanfile_path, update, build_type, logger) except Exception as e: - raise ProviderInstallationError('conan', f'Failed to install binary dependencies: {e}', e) from e + raise ProviderInstallationError('conan', f'Failed to install dependencies: {e}', e) from e - try: - # Generate consumer files - self._generate_consumer_files(conan_api, deps_graph) - except Exception as e: - raise ProviderInstallationError('conan', f'Failed to generate consumer files: {e}', e) from e - - def _prepare_installation(self) -> tuple[ConanAPI, Path]: + def _prepare_installation(self) -> Path: """Prepare the installation environment and generate conanfile. Returns: - Tuple of (ConanAPI instance, conanfile path) + Path to conanfile.py """ # Resolve dependencies and generate conanfile.py resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies] - self.builder.generate_conanfile(self.core_data.project_data.project_root, resolved_dependencies) + self.builder.generate_conanfile( + self.core_data.project_data.project_root, + resolved_dependencies, + self.core_data.pep621_data.name, + self.core_data.pep621_data.version, + ) # Ensure build directory exists self.core_data.cppython_data.build_path.mkdir(parents=True, exist_ok=True) - # Setup paths and API - conan_api = ConanAPI() + # Setup paths project_root = self.core_data.project_data.project_root conanfile_path = project_root / 'conanfile.py' if not conanfile_path.exists(): raise FileNotFoundError('Generated conanfile.py not found') - return conan_api, conanfile_path + return conanfile_path + + def _ensure_default_profiles(self) -> None: + """Ensure default Conan profiles exist, creating them if necessary.""" + try: + self._conan_api.profiles.get_default_host() + self._conan_api.profiles.get_default_build() + except Exception: + # If profiles don't exist, create them using profile detect + self._conan_api.command.run(['profile', 'detect']) - def _load_dependency_graph(self, conan_api: ConanAPI, conanfile_path: Path, update: bool): - """Load and build the dependency graph. + def _run_conan_install(self, conanfile_path: Path, update: bool, build_type: str, logger: Logger) -> None: + """Run conan install command using Conan API with optional build type. Args: - conan_api: The Conan API instance conanfile_path: Path to the conanfile.py update: Whether to check for updates - - Returns: - The loaded dependency graph + build_type: Build type (Release, Debug, etc.) or None for default + logger: Logger instance """ - all_remotes = conan_api.remotes.list() - profile_host, profile_build = self.data.host_profile, self.data.build_profile - - return conan_api.graph.load_graph_consumer( - path=str(conanfile_path), - name=None, - version=None, - user=None, - channel=None, - lockfile=None, - remotes=all_remotes, - update=update or None, - check_updates=update, - is_build_require=False, - profile_host=profile_host, - profile_build=profile_build, - ) + # Build conan install command arguments + command_args = ['install', str(conanfile_path)] - def _install_binaries(self, conan_api: ConanAPI, deps_graph, update: bool) -> None: - """Analyze and install binary dependencies. - - Args: - conan_api: The Conan API instance - deps_graph: The dependency graph - update: Whether to check for updates - """ - all_remotes = conan_api.remotes.list() - - # Analyze binaries to determine what needs to be built/downloaded - conan_api.graph.analyze_binaries( - graph=deps_graph, - build_mode=['missing'], - remotes=all_remotes, - update=update or None, - lockfile=None, - ) + # Add build missing flag + command_args.extend(['--build', 'missing']) - # Install all dependencies - conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) + # Add update flag if needed + if update: + command_args.append('--update') - def _generate_consumer_files(self, conan_api: ConanAPI, deps_graph) -> None: - """Generate consumer files (CMake toolchain, deps, etc.). + # Add build type setting if specified + if build_type: + command_args.extend(['-s', f'build_type={build_type}']) - Args: - conan_api: The Conan API instance - deps_graph: The dependency graph - """ - project_root = self.core_data.project_data.project_root + # Log the command being executed + logger.info('Executing conan command: conan %s', ' '.join(command_args)) - conan_api.install.install_consumer( - deps_graph=deps_graph, - generators=['CMakeToolchain', 'CMakeDeps'], - source_folder=str(project_root), - output_folder=str(self.core_data.cppython_data.build_path), - ) + try: + # Use reusable Conan API instance instead of subprocess + # 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) + except Exception as e: + error_msg = str(e) + logger.error('Conan install failed: %s', error_msg, exc_info=True) + raise ProviderInstallationError('conan', error_msg, e) from e def install(self) -> None: """Installs the provider""" @@ -226,117 +200,98 @@ def sync_data(self, consumer: SyncConsumer) -> SyncData: """ for sync_type in consumer.sync_types(): if sync_type == CMakeSyncData: - return CMakeSyncData( - provider_name=TypeName('conan'), - top_level_includes=self.core_data.cppython_data.install_path / 'conan_provider.cmake', - ) + return self._create_cmake_sync_data() raise NotSupportedError(f'Unsupported sync types: {consumer.sync_types()}') + def _create_cmake_sync_data(self) -> CMakeSyncData: + """Creates CMake synchronization data with Conan toolchain configuration. + + Returns: + CMakeSyncData configured for Conan integration + """ + conan_toolchain_path = self.core_data.cppython_data.build_path / 'generators' / 'conan_toolchain.cmake' + + return CMakeSyncData( + provider_name=TypeName('conan'), + toolchain_file=conan_toolchain_path, + ) + @classmethod async def download_tooling(cls, directory: Path) -> None: - """Downloads the conan provider file""" - cls._download_file(cls._provider_url, directory / 'conan_provider.cmake') + """Download external tooling required by the Conan provider. + + Since we're using CMakeToolchain generator instead of cmake-conan provider, + no external tooling needs to be downloaded. + """ + # No external tooling required when using CMakeToolchain + pass def publish(self) -> None: """Publishes the package using conan create workflow.""" project_root = self.core_data.project_data.project_root conanfile_path = project_root / 'conanfile.py' + logger = getLogger('cppython.conan') if not conanfile_path.exists(): raise FileNotFoundError(f'conanfile.py not found at {conanfile_path}') - conan_api = ConanAPI() - - all_remotes = conan_api.remotes.list() - - # Configure remotes for upload - configured_remotes = self._get_configured_remotes(all_remotes) - - # Export the recipe to cache - ref, _ = conan_api.export.export( - path=str(conanfile_path), - name=None, - version=None, - user=None, - channel=None, - lockfile=None, - remotes=all_remotes, - ) - - # Build dependency graph and install - profile_host, profile_build = self.data.host_profile, self.data.build_profile - deps_graph = conan_api.graph.load_graph_consumer( - path=str(conanfile_path), - name=None, - version=None, - user=None, - channel=None, - lockfile=None, - remotes=all_remotes, # Use all remotes for dependency resolution - update=None, - check_updates=False, - is_build_require=False, - profile_host=profile_host, - profile_build=profile_build, - ) + try: + # Build conan create command arguments + command_args = ['create', str(conanfile_path)] - # Analyze and build binaries - conan_api.graph.analyze_binaries( - graph=deps_graph, - build_mode=['*'], - remotes=all_remotes, # Use all remotes for dependency resolution - update=None, - lockfile=None, - ) + # Add build mode (build everything for publishing) + command_args.extend(['--build', 'missing']) - conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes) + # Log the command being executed + logger.info('Executing conan create command: conan %s', ' '.join(command_args)) - if not self.data.skip_upload: - self._upload_package(conan_api, ref, configured_remotes) + # 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) - def _get_configured_remotes(self, all_remotes): - """Get and validate configured remotes for upload. + # Upload if not skipped + if not self.data.skip_upload: + self._upload_package(logger) - Note: This only affects upload behavior. For dependency resolution, - we always use all available system remotes regardless of this config. - """ - # If skip_upload is True, don't upload anywhere - if self.data.skip_upload: - return [] + except Exception as e: + error_msg = str(e) + logger.error('Conan create failed: %s', error_msg, exc_info=True) + raise ProviderInstallationError('conan', error_msg, e) from e - # If no remotes specified, upload to all available remotes + def _upload_package(self, logger) -> None: + """Upload the package to configured remotes using Conan API.""" + # If no remotes configured, upload to all remotes if not self.data.remotes: - return all_remotes - - # Otherwise, upload only to specified remotes - configured_remotes = [remote for remote in all_remotes if remote.name in self.data.remotes] - - if not configured_remotes: - available_remotes = [remote.name for remote in all_remotes] - raise ProviderConfigurationError( - 'conan', - f'No configured remotes found. Available: {available_remotes}, Configured: {self.data.remotes}', - 'remotes', - ) - - return configured_remotes - - def _upload_package(self, conan_api, ref, configured_remotes): - """Upload the package to configured remotes.""" - ref_pattern = ListPattern(f'{ref.name}/*', package_id='*', only_recipe=False) - package_list = conan_api.list.select(ref_pattern) - - if not package_list.recipes: - raise ProviderInstallationError('conan', 'No packages found to upload') - - remote = configured_remotes[0] - conan_api.upload.upload_full( - package_list=package_list, - remote=remote, - enabled_remotes=configured_remotes, - check_integrity=False, - force=False, - metadata=None, - dry_run=False, - ) + # Upload to all available remotes + command_args = ['upload', '*', '--all', '--confirm'] + else: + # Upload only to specified remotes + for remote in self.data.remotes: + command_args = ['upload', '*', '--remote', remote, '--all', '--confirm'] + + # Log the command being executed + logger.info('Executing conan upload command: conan %s', ' '.join(command_args)) + + try: + self._conan_api.command.run(command_args) + except Exception as e: + error_msg = str(e) + logger.error('Conan upload failed for remote %s: %s', remote, error_msg, exc_info=True) + raise ProviderInstallationError('conan', f'Upload to {remote} failed: {error_msg}', e) from e + return + + # Log the command for uploading to all remotes + logger.info('Executing conan upload command: conan %s', ' '.join(command_args)) + + try: + self._conan_api.command.run(command_args) + except Exception as e: + error_msg = str(e) + logger.error('Conan upload failed: %s', error_msg, exc_info=True) + raise ProviderInstallationError('conan', error_msg, e) from e diff --git a/cppython/plugins/conan/resolution.py b/cppython/plugins/conan/resolution.py index 4f5f8685..3c98b37f 100644 --- a/cppython/plugins/conan/resolution.py +++ b/cppython/plugins/conan/resolution.py @@ -1,12 +1,8 @@ """Provides functionality to resolve Conan-specific data for the CPPython project.""" -import importlib -import logging from pathlib import Path from typing import Any -from conan.api.conan_api import ConanAPI -from conan.internal.model.profile import Profile from packaging.requirements import Requirement from cppython.core.exception import ConfigException @@ -18,193 +14,6 @@ ConanVersion, ConanVersionRange, ) -from cppython.utility.exception import ProviderConfigurationError - - -def _detect_cmake_program() -> str | None: - """Detect CMake program path from the cmake module if available. - - Returns: - Path to cmake executable, or None if not found - """ - try: - # Try to import cmake module and get its executable path - # Note: cmake is an optional dependency, so we import it conditionally - cmake = importlib.import_module('cmake') - - cmake_bin_dir = Path(cmake.CMAKE_BIN_DIR) - - # Try common cmake executable names (pathlib handles platform differences) - for cmake_name in ['cmake.exe', 'cmake']: - cmake_exe = cmake_bin_dir / cmake_name - if cmake_exe.exists(): - return str(cmake_exe) - - return None - except ImportError: - # cmake module not available - return None - except (AttributeError, Exception): - # If cmake module doesn't have expected attributes - return None - - -def _profile_post_process( - profiles: list[Profile], conan_api: ConanAPI, cache_settings: Any, cmake_program: str | None = None -) -> None: - """Apply profile plugin and settings processing to a list of profiles. - - Args: - profiles: List of profiles to process - conan_api: The Conan API instance - cache_settings: The settings configuration - cmake_program: Optional path to cmake program to configure in profiles - """ - logger = logging.getLogger('cppython.conan') - - # Get global configuration - global_conf = conan_api.config.global_conf - - # Apply profile plugin processing - try: - profile_plugin = conan_api.profiles._load_profile_plugin() - if profile_plugin is not None: - for profile in profiles: - try: - profile_plugin(profile) - except Exception as plugin_error: - logger.warning('Profile plugin failed for profile: %s', str(plugin_error)) - except (AttributeError, Exception): - logger.debug('Profile plugin not available or failed to load') - - # Apply the full profile processing pipeline for each profile - for profile in profiles: - # Set cmake program configuration if provided - if cmake_program is not None: - try: - # Set the tools.cmake:cmake_program configuration in the profile - profile.conf.update('tools.cmake:cmake_program', cmake_program) - logger.debug('Set tools.cmake:cmake_program=%s in profile', cmake_program) - except (AttributeError, Exception) as cmake_error: - logger.debug('Failed to set cmake program configuration: %s', str(cmake_error)) - - # Process settings to initialize processed_settings - try: - profile.process_settings(cache_settings) - except (AttributeError, Exception) as settings_error: - logger.debug('Settings processing failed for profile: %s', str(settings_error)) - - # Validate configuration - try: - profile.conf.validate() - except (AttributeError, Exception) as conf_error: - logger.debug('Configuration validation failed for profile: %s', str(conf_error)) - - # Apply global configuration to the profile - try: - if global_conf is not None: - profile.conf.rebase_conf_definition(global_conf) - except (AttributeError, Exception) as rebase_error: - logger.debug('Configuration rebase failed for profile: %s', str(rebase_error)) - - -def _apply_cmake_config_to_profile(profile: Profile, cmake_program: str | None, profile_type: str) -> None: - """Apply cmake program configuration to a profile. - - Args: - profile: The profile to configure - cmake_program: Path to cmake program to configure - profile_type: Type of profile (for logging) - """ - if cmake_program is not None: - logger = logging.getLogger('cppython.conan') - try: - profile.conf.update('tools.cmake:cmake_program', cmake_program) - logger.debug('Set tools.cmake:cmake_program=%s in %s profile', cmake_program, profile_type) - except (AttributeError, Exception) as cmake_error: - logger.debug('Failed to set cmake program in %s profile: %s', profile_type, str(cmake_error)) - - -def _resolve_profiles( - host_profile_name: str | None, build_profile_name: str | None, conan_api: ConanAPI, cmake_program: str | None = None -) -> tuple[Profile, Profile]: - """Resolve host and build profiles, with fallback to auto-detection. - - Args: - host_profile_name: The host profile name to resolve, or None for auto-detection - build_profile_name: The build profile name to resolve, or None for auto-detection - conan_api: The Conan API instance - cmake_program: Optional path to cmake program to configure in profiles - - Returns: - A tuple of (host_profile, build_profile) - """ - logger = logging.getLogger('cppython.conan') - - def _resolve_profile(profile_name: str | None, is_host: bool) -> Profile: - """Helper to resolve a single profile.""" - profile_type = 'host' if is_host else 'build' - - if profile_name is not None and profile_name != 'default': - # Explicitly specified profile name (not the default) - fail if not found - try: - logger.debug('Loading %s profile: %s', profile_type, profile_name) - profile = conan_api.profiles.get_profile([profile_name]) - logger.debug('Successfully loaded %s profile: %s', profile_type, profile_name) - _apply_cmake_config_to_profile(profile, cmake_program, profile_type) - return profile - except Exception as e: - logger.error('Failed to load %s profile %s: %s', profile_type, profile_name, str(e)) - raise ProviderConfigurationError( - 'conan', - f'Failed to load {profile_type} profile {profile_name}: {str(e)}', - f'{profile_type}_profile', - ) from e - elif profile_name == 'default': - # Try to load default profile, but fall back to auto-detection if it fails - try: - logger.debug('Loading %s profile: %s', profile_type, profile_name) - profile = conan_api.profiles.get_profile([profile_name]) - logger.debug('Successfully loaded %s profile: %s', profile_type, profile_name) - _apply_cmake_config_to_profile(profile, cmake_program, profile_type) - return profile - except Exception as e: - logger.debug( - 'Failed to load %s profile %s: %s. Falling back to auto-detection.', - profile_type, - profile_name, - str(e), - ) - # Fall back to auto-detection - - try: - if is_host: - default_profile_path = conan_api.profiles.get_default_host() - else: - default_profile_path = conan_api.profiles.get_default_build() - - profile = conan_api.profiles.get_profile([default_profile_path]) - logger.debug('Using default %s profile', profile_type) - _apply_cmake_config_to_profile(profile, cmake_program, profile_type) - return profile - except Exception as e: - logger.warning('Default %s profile not available, using auto-detection: %s', profile_type, str(e)) - - # Create auto-detected profile - profile = conan_api.profiles.detect() - cache_settings = conan_api.config.settings_yml - - # Apply profile plugin processing - _profile_post_process([profile], conan_api, cache_settings, cmake_program) - - logger.debug('Auto-detected %s profile with plugin processing applied', profile_type) - return profile - - # Resolve both profiles - host_profile = _resolve_profile(host_profile_name, is_host=True) - build_profile = _resolve_profile(build_profile_name, is_host=False) - - return host_profile, build_profile def _handle_single_specifier(name: str, specifier) -> ConanDependency: @@ -257,7 +66,7 @@ def resolve_conan_dependency(requirement: Requirement) -> ConanDependency: return _handle_single_specifier(requirement.name, next(iter(specifiers))) # Handle multiple specifiers - convert to Conan range syntax - range_parts = [] + range_parts: list[str] = [] # Define order for operators to ensure consistent output operator_order = ['>=', '>', '<=', '<', '!='] @@ -305,20 +114,13 @@ def resolve_conan_data(data: dict[str, Any], core_data: CorePluginData) -> Conan """ parsed_data = ConanConfiguration(**data) - # Initialize Conan API for profile resolution - conan_api = ConanAPI() + profile_dir = Path(parsed_data.profile_dir) - # Try to detect cmake program path from current virtual environment - cmake_program = _detect_cmake_program() - - # Resolve profiles - host_profile, build_profile = _resolve_profiles( - parsed_data.host_profile, parsed_data.build_profile, conan_api, cmake_program - ) + if not profile_dir.is_absolute(): + profile_dir = core_data.cppython_data.tool_path / profile_dir return ConanData( remotes=parsed_data.remotes, skip_upload=parsed_data.skip_upload, - host_profile=host_profile, - build_profile=build_profile, + profile_dir=profile_dir, ) diff --git a/cppython/plugins/conan/schema.py b/cppython/plugins/conan/schema.py index 19d2e73e..43b136e3 100644 --- a/cppython/plugins/conan/schema.py +++ b/cppython/plugins/conan/schema.py @@ -6,9 +6,9 @@ """ import re +from pathlib import Path from typing import Annotated -from conan.internal.model.profile import Profile from pydantic import Field, field_validator from cppython.core.schema import CPPythonModel @@ -294,8 +294,7 @@ class ConanData(CPPythonModel): remotes: list[str] skip_upload: bool - host_profile: Profile - build_profile: Profile + profile_dir: Path class ConanConfiguration(CPPythonModel): @@ -307,19 +306,13 @@ class ConanConfiguration(CPPythonModel): ] = ['conancenter'] skip_upload: Annotated[ bool, - Field(description='If true, skip uploading packages during publish (local-only mode).'), + Field(description='If true, skip uploading packages to a remote during publishing.'), ] = False - host_profile: Annotated[ - str | None, + profile_dir: Annotated[ + str, Field( - description='Conan host profile defining the target platform where the built software will run. ' - 'Used for cross-compilation scenarios.' + description='Directory containing Conan profiles. Profiles will be looked up relative to this directory. ' + 'If profiles do not exist in this directory, Conan will fall back to default profiles.' + "If a relative path is provided, it will be resolved relative to the tool's working directory." ), - ] = 'default' - build_profile: Annotated[ - str | None, - Field( - description='Conan build profile defining the platform where the compilation process executes. ' - 'Typically matches the development machine.' - ), - ] = 'default' + ] = 'profiles' diff --git a/cppython/plugins/vcpkg/plugin.py b/cppython/plugins/vcpkg/plugin.py index 06522209..2fb02368 100644 --- a/cppython/plugins/vcpkg/plugin.py +++ b/cppython/plugins/vcpkg/plugin.py @@ -32,6 +32,39 @@ def __init__( self.core_data: CorePluginData = core_data self.data: VcpkgData = resolve_vcpkg_data(configuration_data, core_data) + @staticmethod + def _handle_subprocess_error( + logger_instance, operation: str, error: subprocess.CalledProcessError, exception_class: type + ) -> None: + """Handles subprocess errors with comprehensive error message formatting. + + Args: + logger_instance: The logger instance to use for error logging + operation: Description of the operation that failed (e.g., 'install', 'clone') + error: The CalledProcessError exception + exception_class: The exception class to raise + + Raises: + The specified exception_class with the formatted error message + """ + # Capture both stdout and stderr for better error reporting + stdout_msg = error.stdout.strip() if error.stdout else '' + stderr_msg = error.stderr.strip() if error.stderr else '' + + # Combine both outputs for comprehensive error message + error_parts = [] + if stderr_msg: + error_parts.append(f'stderr: {stderr_msg}') + if stdout_msg: + error_parts.append(f'stdout: {stdout_msg}') + + if not error_parts: + error_parts.append(f'Command failed with exit code {error.returncode}') + + error_msg = ' | '.join(error_parts) + logger_instance.error('Unable to %s: %s', operation, error_msg, exc_info=True) + raise exception_class('vcpkg', operation, error_msg, error) from error + @staticmethod def features(directory: Path) -> SupportedFeatures: """Queries vcpkg support @@ -82,6 +115,7 @@ def _update_provider(cls, path: Path) -> None: shell=True, check=True, capture_output=True, + text=True, ) elif system_name == 'posix': subprocess.run( @@ -90,11 +124,10 @@ def _update_provider(cls, path: Path) -> None: shell=True, check=True, capture_output=True, + text=True, ) except subprocess.CalledProcessError as e: - error_msg = e.stderr.decode() if e.stderr else str(e) - logger.error('Unable to bootstrap the vcpkg repository: %s', error_msg, exc_info=True) - raise ProviderToolingError('vcpkg', 'bootstrap', error_msg, e) from e + cls._handle_subprocess_error(logger, 'bootstrap the vcpkg repository', e, ProviderToolingError) def sync_data(self, consumer: SyncConsumer) -> SyncData: """Gathers a data object for the given generator @@ -110,13 +143,24 @@ def sync_data(self, consumer: SyncConsumer) -> SyncData: """ for sync_type in consumer.sync_types(): if sync_type == CMakeSyncData: - return CMakeSyncData( - provider_name=TypeName('vcpkg'), - top_level_includes=self.core_data.cppython_data.install_path / 'scripts/buildsystems/vcpkg.cmake', - ) + return self._create_cmake_sync_data() raise NotSupportedError('OOF') + def _create_cmake_sync_data(self) -> CMakeSyncData: + """Creates CMake synchronization data with vcpkg configuration. + + Returns: + CMakeSyncData configured for vcpkg integration + """ + # Create CMakeSyncData with vcpkg configuration + vcpkg_cmake_path = self.core_data.cppython_data.install_path / 'scripts/buildsystems/vcpkg.cmake' + + return CMakeSyncData( + provider_name=TypeName('vcpkg'), + toolchain_file=vcpkg_cmake_path, + ) + @classmethod def tooling_downloaded(cls, path: Path) -> bool: """Returns whether the provider tooling needs to be downloaded @@ -158,17 +202,17 @@ async def download_tooling(cls, directory: Path) -> None: cwd=directory, check=True, capture_output=True, + text=True, ) subprocess.run( ['git', 'pull'], cwd=directory, check=True, capture_output=True, + text=True, ) except subprocess.CalledProcessError as e: - error_msg = e.stderr.decode() if e.stderr else str(e) - logger.error('Unable to update the vcpkg repository: %s', error_msg, exc_info=True) - raise ProviderToolingError('vcpkg', 'update', error_msg, e) from e + cls._handle_subprocess_error(logger, 'update the vcpkg repository', e, ProviderToolingError) else: try: logger.debug("Cloning the vcpkg repository to '%s'", directory.absolute()) @@ -179,12 +223,11 @@ async def download_tooling(cls, directory: Path) -> None: cwd=directory, check=True, capture_output=True, + text=True, ) except subprocess.CalledProcessError as e: - error_msg = e.stderr.decode() if e.stderr else str(e) - logger.error('Unable to clone the vcpkg repository: %s', error_msg, exc_info=True) - raise ProviderToolingError('vcpkg', 'clone', error_msg, e) from e + cls._handle_subprocess_error(logger, 'clone the vcpkg repository', e, ProviderToolingError) cls._update_provider(directory) @@ -209,11 +252,10 @@ def install(self) -> None: cwd=str(build_path), check=True, capture_output=True, + text=True, ) except subprocess.CalledProcessError as e: - error_msg = e.stderr.decode() if e.stderr else str(e) - logger.error('Unable to install project dependencies: %s', error_msg, exc_info=True) - raise ProviderInstallationError('vcpkg', error_msg, e) from e + self._handle_subprocess_error(logger, 'install project dependencies', e, ProviderInstallationError) def update(self) -> None: """Called when dependencies need to be updated and written to the lock file.""" @@ -237,11 +279,10 @@ def update(self) -> None: cwd=str(build_path), check=True, capture_output=True, + text=True, ) except subprocess.CalledProcessError as e: - error_msg = e.stderr.decode() if e.stderr else str(e) - logger.error('Unable to update project dependencies: %s', error_msg, exc_info=True) - raise ProviderInstallationError('vcpkg', error_msg, e) from e + self._handle_subprocess_error(logger, 'update project dependencies', e, ProviderInstallationError) def publish(self) -> None: """Called when the project needs to be published. diff --git a/cppython/project.py b/cppython/project.py index 0ac3c9ea..1cab7efe 100644 --- a/cppython/project.py +++ b/cppython/project.py @@ -98,5 +98,14 @@ def publish(self) -> None: Raises: Exception: Provider-specific exception """ + if not self._enabled: + self.logger.info('Skipping publish because the project is not enabled') + return + + self.logger.info('Publishing project') + + # Ensure sync is performed before publishing to generate necessary files + self._data.sync() + # Let provider handle its own exceptions for better error context self._data.plugins.provider.publish() diff --git a/docs/antora.yml b/docs/antora.yml index 3334c3c9..7487323a 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -2,5 +2,6 @@ name: cppython version: 0.1.0 title: CPPython nav: - - modules/ROOT/nav.adoc - - modules/tests/nav.adoc + - modules/ROOT/nav.adoc + - modules/plugins/nav.adoc + - modules/tests/nav.adoc diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index c3caa444..c89ed3d2 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -1 +1,16 @@ -= Documentation for CPPython \ No newline at end of file += CPPython Documentation + +CPPython is a C++ project management tool that integrates with various build systems, package managers, and development tools through a flexible plugin architecture. + +== Getting Started + +- xref:configuration.adoc[Configuration Guide] - Learn how to configure CPPython for your project +- xref:plugins:index.adoc[Plugin System] - Understand the plugin architecture and available plugins + +== Plugin Documentation + +=== Build System Integration + +- xref:plugins:cmake.adoc[CMake Generator Plugin] - Complete integration with CMake build system + * xref:plugins:cmake-presets.adoc[CMake Presets System] - Advanced preset management + * xref:plugins:cmake-configuration.adoc[Configuration Options] - Detailed configuration reference \ No newline at end of file diff --git a/docs/modules/plugins/nav.adoc b/docs/modules/plugins/nav.adoc new file mode 100644 index 00000000..6e40db10 --- /dev/null +++ b/docs/modules/plugins/nav.adoc @@ -0,0 +1,5 @@ +.Plugins +* xref:index.adoc[Plugin System Overview] +* xref:cmake.adoc[CMake Generator Plugin] +** xref:cmake-presets.adoc[CMake Presets System] +** xref:cmake-configuration.adoc[Configuration Options] diff --git a/docs/modules/plugins/pages/cmake-configuration.adoc b/docs/modules/plugins/pages/cmake-configuration.adoc new file mode 100644 index 00000000..c238e1fa --- /dev/null +++ b/docs/modules/plugins/pages/cmake-configuration.adoc @@ -0,0 +1,14 @@ += CMake Configuration Options + +This page provides a comprehensive reference for configuring the CMake Generator Plugin in CPPython. + +== Configuration Location + +CMake plugin configuration is specified in your project's `pyproject.toml` file under the `[tool.cppython.generators.cmake]` section: + +[source,toml] +---- +[tool.cppython.generators.cmake] +preset_file = "CMakePresets.json" +configuration_name = "default" +---- \ No newline at end of file diff --git a/docs/modules/plugins/pages/cmake-presets.adoc b/docs/modules/plugins/pages/cmake-presets.adoc new file mode 100644 index 00000000..f3dbc37f --- /dev/null +++ b/docs/modules/plugins/pages/cmake-presets.adoc @@ -0,0 +1,72 @@ += CMake Presets System + +The CMake Presets System in CPPython provides a sophisticated three-tier architecture for managing CMake configurations across different build scenarios, dependency providers, and user customizations. + +== Architecture Overview + +The preset system is designed around three distinct layers: + +[source,text] +---- +User CMakePresets.json + ↓ (includes) +CPPython Presets (.cppython/cppython.json) + ↓ (includes) +Provider Presets (.cppython/providers/*.json) +---- + +Each layer serves a specific purpose and maintains clear separation of concerns. + +== Provider Preset Layer + +The provider layer contains presets generated by dependency management tools like Conan or vcpkg. These presets include tool-specific configurations and are completely managed by CPPython. + +=== Structure + +Each provider generates **4 configure presets**: + +`{provider}-base`:: +Hidden preset containing common configuration shared by all other presets. Includes toolchain files and top-level includes. + +`{provider}-default`:: +Hidden preset inheriting from base. No specific build type set, making it suitable for multi-config generators. + +`{provider}-release`:: +Hidden preset inheriting from base with `CMAKE_BUILD_TYPE=Release` for single-config generators. + +`{provider}-debug`:: +Hidden preset inheriting from base with `CMAKE_BUILD_TYPE=Debug` for single-config generators. + + +== CPPython Preset Layer + +The CPPython layer standardizes provider configurations and creates both configure and build presets. This layer serves as the primary interface between provider-specific settings and user configurations. + +=== Configure Presets + +The CPPython layer generates **3 configure presets**: + +`default`:: +Public preset inheriting from `{provider}-default`. Designed for multi-config generators. + +`release`:: +Public preset inheriting from `{provider}-release`. Optimized for single-config Release builds. + +`debug`:: +Public preset inheriting from `{provider}-debug`. Optimized for single-config Debug builds. + +=== Build Presets + +The CPPython layer generates **4 build presets** to handle both single-config and multi-config generators: + +`multi-release`:: +Uses the `default` configure preset with `configuration: "Release"`. For multi-config generators. + +`multi-debug`:: +Uses the `default` configure preset with `configuration: "Debug"`. For multi-config generators. + +`release`:: +Uses the `release` configure preset with `configuration: "Release"`. For single-config generators. + +`debug`:: +Uses the `debug` configure preset with `configuration: "Debug"`. For single-config generators. \ No newline at end of file diff --git a/docs/modules/plugins/pages/cmake.adoc b/docs/modules/plugins/pages/cmake.adoc new file mode 100644 index 00000000..121aa134 --- /dev/null +++ b/docs/modules/plugins/pages/cmake.adoc @@ -0,0 +1,7 @@ += CMake Generator Plugin + +**TODO** + +== Overview + +**TODO** \ No newline at end of file diff --git a/docs/modules/plugins/pages/index.adoc b/docs/modules/plugins/pages/index.adoc new file mode 100644 index 00000000..0a765367 --- /dev/null +++ b/docs/modules/plugins/pages/index.adoc @@ -0,0 +1,47 @@ += Plugin System Overview + +CPPython uses a plugin-based architecture to support different build systems, package managers, and source control systems. The plugin system is designed to be extensible and modular, allowing for easy integration of new tools and workflows. + +== Plugin Types + +CPPython supports three main types of plugins: + +=== Generator Plugins + +Generator plugins integrate with build systems like CMake, providing functionality to generate build files and manage build configurations. + +Currently supported generators: +- xref:cmake.adoc[CMake Generator Plugin] + +=== Provider Plugins + +Provider plugins integrate with package managers and dependency providers, handling dependency resolution and environment setup. + +Currently supported providers: +- Conan +- vcpkg + +=== Source Control Manager (SCM) Plugins + +SCM plugins integrate with version control systems to manage project metadata and versioning. + +Currently supported SCM systems: +- Git + +== Plugin Architecture + +The plugin system follows a consistent pattern where each plugin type implements specific interfaces: + +- **Generator Interface**: Handles build system integration +- **Provider Interface**: Manages dependency resolution +- **SCM Interface**: Provides version control integration + +Each plugin can define: +- Configuration schemas using Pydantic models +- Synchronization data structures +- Build and resolution logic +- File generation and management + +== Configuration + +Plugins are configured through the `pyproject.toml` file in the `[tool.cppython]` section. Each plugin type has its own configuration section with specific options and defaults. \ No newline at end of file diff --git a/docs/modules/tests/nav.adoc b/docs/modules/tests/nav.adoc index 24495cf9..a8cd6854 100644 --- a/docs/modules/tests/nav.adoc +++ b/docs/modules/tests/nav.adoc @@ -1,3 +1,3 @@ -.CPPython -* xref:index.adoc[] -* xref:fixtures.adoc[] \ No newline at end of file +.Testing +* xref:index.adoc[Testing Overview] +* xref:fixtures.adoc[Test Fixtures] \ No newline at end of file diff --git a/examples/conan_cmake/library/CMakeLists.txt b/examples/conan_cmake/library/CMakeLists.txt new file mode 100644 index 00000000..eb23c20c --- /dev/null +++ b/examples/conan_cmake/library/CMakeLists.txt @@ -0,0 +1,59 @@ +cmake_minimum_required(VERSION 3.30) + +project(mathutils + VERSION 1.0.0 + DESCRIPTION "A modern math utilities library" + LANGUAGES CXX +) + +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +# Dependencies +find_package(fmt REQUIRED) + +# Library target +add_library(mathutils src/mathutils.cpp) +add_library(mathutils::mathutils ALIAS mathutils) + +target_compile_features(mathutils PUBLIC cxx_std_17) +target_include_directories(mathutils PUBLIC + $ + $ +) +target_link_libraries(mathutils PUBLIC fmt::fmt) + +# Installation +install(TARGETS mathutils + EXPORT mathutilsTargets + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +install(DIRECTORY include/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.h*" +) + +install(EXPORT mathutilsTargets + FILE mathutilsTargets.cmake + NAMESPACE mathutils:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mathutils +) + +# Package configuration +write_basic_package_version_file( + mathutilsConfigVersion.cmake + COMPATIBILITY SameMajorVersion +) + +configure_package_config_file( + cmake/mathutilsConfig.cmake.in + mathutilsConfig.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mathutils +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/mathutilsConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/mathutilsConfigVersion.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mathutils +) \ No newline at end of file diff --git a/examples/conan_cmake/library/cmake/mathutilsConfig.cmake.in b/examples/conan_cmake/library/cmake/mathutilsConfig.cmake.in new file mode 100644 index 00000000..cfe24c19 --- /dev/null +++ b/examples/conan_cmake/library/cmake/mathutilsConfig.cmake.in @@ -0,0 +1,8 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(fmt REQUIRED) + +include("${CMAKE_CURRENT_LIST_DIR}/mathutilsTargets.cmake") + +check_required_components(mathutils) diff --git a/examples/conan_cmake/library/include/mathutils/mathutils.h b/examples/conan_cmake/library/include/mathutils/mathutils.h new file mode 100644 index 00000000..63ef0162 --- /dev/null +++ b/examples/conan_cmake/library/include/mathutils/mathutils.h @@ -0,0 +1,29 @@ +#pragma once + +namespace mathutils +{ + /** + * Add two numbers and return the result with formatted output + * @param a First number + * @param b Second number + * @return Sum of a and b + */ + double add(double a, double b); + + /** + * Multiply two numbers and return the result with formatted output + * @param a First number + * @param b Second number + * @return Product of a and b + */ + double multiply(double a, double b); + + /** + * Print a formatted calculation result + * @param operation The operation performed + * @param a First operand + * @param b Second operand + * @param result The result of the operation + */ + void print_result(const char *operation, double a, double b, double result); +} diff --git a/examples/conan_cmake/library/pyproject.toml b/examples/conan_cmake/library/pyproject.toml new file mode 100644 index 00000000..db4ea395 --- /dev/null +++ b/examples/conan_cmake/library/pyproject.toml @@ -0,0 +1,25 @@ +[project] +description = "A simple C++ library example using conan with CPPython" +name = "mathutils" +version = "1.0.0" + +license = { text = "MIT" } + +authors = [{ name = "Synodic Software", email = "contact@synodic.software" }] + +requires-python = ">=3.13" + +dependencies = ["cppython[conan, cmake, git]>=0.9.0"] + + +[tool.cppython] +install-path = "install" + +dependencies = ["fmt>=11.2.0"] + +[tool.cppython.generators.cmake] + +[tool.cppython.providers.conan] + +[tool.pdm] +distribution = false diff --git a/examples/conan_cmake/library/src/mathutils.cpp b/examples/conan_cmake/library/src/mathutils.cpp new file mode 100644 index 00000000..c8017244 --- /dev/null +++ b/examples/conan_cmake/library/src/mathutils.cpp @@ -0,0 +1,27 @@ +#include "mathutils/mathutils.h" +#include +#include + +namespace mathutils +{ + double add(double a, double b) + { + double result = a + b; + print_result("addition", a, b, result); + return result; + } + + double multiply(double a, double b) + { + double result = a * b; + print_result("multiplication", a, b, result); + return result; + } + + void print_result(const char *operation, double a, double b, double result) + { + fmt::print(fg(fmt::terminal_color::green), + "MathUtils {}: {} + {} = {}\n", + operation, a, b, result); + } +} diff --git a/examples/conan_cmake/library/test_package/CMakeLists.txt b/examples/conan_cmake/library/test_package/CMakeLists.txt new file mode 100644 index 00000000..455b7196 --- /dev/null +++ b/examples/conan_cmake/library/test_package/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.24) + +project(MathUtilsConsumer LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(mathutils REQUIRED) + +add_executable(consumer main.cpp) +target_link_libraries(consumer PRIVATE mathutils::mathutils) diff --git a/examples/conan_cmake/library/test_package/conanfile.py b/examples/conan_cmake/library/test_package/conanfile.py new file mode 100644 index 00000000..57e15ffe --- /dev/null +++ b/examples/conan_cmake/library/test_package/conanfile.py @@ -0,0 +1,40 @@ +"""Test package for mathutils library.""" + +import os + +from conan import ConanFile +from conan.tools.build import can_run +from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout + + +class MathUtilsTestConan(ConanFile): + """Test package for mathutils library.""" + + settings = 'os', 'compiler', 'build_type', 'arch' + + def requirements(self): + """Add the tested package as a requirement.""" + self.requires(self.tested_reference_str) + + def layout(self): + """Set the CMake layout.""" + cmake_layout(self) + + def generate(self): + """Generate CMake dependencies and toolchain.""" + deps = CMakeDeps(self) + deps.generate() + tc = CMakeToolchain(self) + tc.generate() + + def build(self): + """Build the test package.""" + cmake = CMake(self) + cmake.configure() + cmake.build() + + def test(self): + """Run the test.""" + if can_run(self): + cmd = os.path.join(self.cpp.build.bindir, 'consumer') + self.run(cmd, env='conanrun') diff --git a/examples/conan_cmake/library/test_package/main.cpp b/examples/conan_cmake/library/test_package/main.cpp new file mode 100644 index 00000000..2ba064a4 --- /dev/null +++ b/examples/conan_cmake/library/test_package/main.cpp @@ -0,0 +1,14 @@ +#include +#include + +int main() +{ + // Test the mathutils library + std::cout << "Testing MathUtils library..." << std::endl; + + double result1 = mathutils::add(5.0, 3.0); + double result2 = mathutils::multiply(4.0, 2.5); + + std::cout << "MathUtils tests completed successfully!" << std::endl; + return 0; +} diff --git a/examples/conan_cmake/simple/CMakeLists.txt b/examples/conan_cmake/simple/CMakeLists.txt index 7cf1eccb..e728aa4d 100644 --- a/examples/conan_cmake/simple/CMakeLists.txt +++ b/examples/conan_cmake/simple/CMakeLists.txt @@ -6,5 +6,5 @@ set(CMAKE_CXX_STANDARD 14) find_package(fmt REQUIRED) -add_executable(main main.cpp) +add_executable(main src/main.cpp) target_link_libraries(main PRIVATE fmt::fmt) \ No newline at end of file diff --git a/examples/conan_cmake/simple/main.cpp b/examples/conan_cmake/simple/src/main.cpp similarity index 100% rename from examples/conan_cmake/simple/main.cpp rename to examples/conan_cmake/simple/src/main.cpp diff --git a/examples/vcpkg_cmake/simple/CMakeLists.txt b/examples/vcpkg_cmake/simple/CMakeLists.txt index 5526812f..39fa2d17 100644 --- a/examples/vcpkg_cmake/simple/CMakeLists.txt +++ b/examples/vcpkg_cmake/simple/CMakeLists.txt @@ -1,9 +1,10 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.24) -project(HelloWorld) +project(FormatOutput LANGUAGES CXX C) -find_package(fmt CONFIG REQUIRED) +set(CMAKE_CXX_STANDARD 14) -add_executable(HelloWorld helloworld.cpp) +find_package(fmt REQUIRED) -target_link_libraries(HelloWorld PRIVATE fmt::fmt) \ No newline at end of file +add_executable(main src/main.cpp) +target_link_libraries(main PRIVATE fmt::fmt) \ No newline at end of file diff --git a/examples/vcpkg_cmake/simple/helloworld.cpp b/examples/vcpkg_cmake/simple/helloworld.cpp deleted file mode 100644 index 82c4e7a0..00000000 --- a/examples/vcpkg_cmake/simple/helloworld.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include - -int main() -{ - fmt::print("Hello World!\n"); - return 0; -} \ No newline at end of file diff --git a/examples/vcpkg_cmake/simple/pdm.lock b/examples/vcpkg_cmake/simple/pdm.lock new file mode 100644 index 00000000..2f4a85d0 --- /dev/null +++ b/examples/vcpkg_cmake/simple/pdm.lock @@ -0,0 +1,393 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:b2622b10976126fce19eb70004ec43a9b5bb0319222fb13824ff58cfcd81c256" + +[[metadata.targets]] +requires_python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +requires_python = ">=3.8" +summary = "Reusable constraint types to use with typing.Annotated" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.9\"", +] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +requires_python = ">=3.7" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default"] +files = [ + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +requires_python = ">=3.7" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["default"] +files = [ + {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, +] + +[[package]] +name = "click" +version = "8.2.1" +requires_python = ">=3.10" +summary = "Composable command line interface toolkit" +groups = ["default"] +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[[package]] +name = "cmake" +version = "4.1.0" +requires_python = ">=3.8" +summary = "CMake is an open-source, cross-platform family of tools designed to build, test and package software" +groups = ["default"] +files = [ + {file = "cmake-4.1.0-py3-none-macosx_10_10_universal2.whl", hash = "sha256:69df62445b22d78c2002c22edeb0e85590ae788e477d222fb2ae82c871c33090"}, + {file = "cmake-4.1.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4e3a30a4f72a8a6d8d593dc289e791f1d84352c1f629543ac8e22c62dbadb20a"}, + {file = "cmake-4.1.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0e2fea746d746f52aa52b8498777ff665a0627d9b136bec4ae0465c38b75e799"}, + {file = "cmake-4.1.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5a28a87601fa5e775017bf4f5836e8e75091d08f3e5aac411256754ba54fe5c4"}, + {file = "cmake-4.1.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2a8790473afbb895b8e684e479f26773e4fc5c86845e3438e8488d38de9db807"}, + {file = "cmake-4.1.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dab375932f5962e078da8cf76ca228c21bf4bea9ddeb1308e2b35797fa30f784"}, + {file = "cmake-4.1.0-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:f2eaa6f0a25e31fe09fb0b7f40fbf208eea5f1313093ff441ecfff7dc1b80adf"}, + {file = "cmake-4.1.0-py3-none-manylinux_2_35_riscv64.whl", hash = "sha256:3ee38de00cad0501c7dd2b94591522381e3ef9c8468094f037a17ed9e478ef13"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2d9f14b7d58e447865c111b3b90945b150724876866f5801c80970151718f710"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:574448a03acdf34c55a7c66485e7a8260709e8386e9145708e18e2abe5fc337b"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8c2538fb557b9edd74d48c189fcde42a55ad7e2c39e04254f8c5d248ca1af4c"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:7c7999c5a1d5a3a66adacc61056765557ed253dc7b8e9deab5cae546f4f9361c"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:e77ac2554a7b8a94745add465413e3266b714766e9a5d22ac8e5b36a900a1136"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:d54e68d5439193265fd7211671420601f6a672b8ca220f19e6c72238b41a84c2"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c6bd346fe4d9c205310ef9a6e09ced7e610915fa982d7b649f9b12caa6fa0605"}, + {file = "cmake-4.1.0-py3-none-win32.whl", hash = "sha256:7219b7e85ed03a98af89371b9dee762e236ad94e8a09ce141070e6ac6415756f"}, + {file = "cmake-4.1.0-py3-none-win_amd64.whl", hash = "sha256:76e8e7d80a1a9bb5c7ec13ec8da961a8c5a997247f86a08b29f0c2946290c461"}, + {file = "cmake-4.1.0-py3-none-win_arm64.whl", hash = "sha256:8d39bbfee7c181e992875cd390fc6d51a317c9374656b332021a67bb40c0b07f"}, + {file = "cmake-4.1.0.tar.gz", hash = "sha256:bacdd21aebdf9a42e5631cfb365beb8221783fcd27c4e04f7db8b79c43fb12df"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["default"] +marker = "platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cppython" +version = "0.9.6" +requires_python = ">=3.13" +summary = "A Python management solution for C++ dependencies" +groups = ["default"] +dependencies = [ + "packaging>=25.0", + "pydantic>=2.11.7", + "requests>=2.32.4", + "typer>=0.16.0", + "types-requests>=2.32.4.20250611", +] +files = [ + {file = "cppython-0.9.6-py3-none-any.whl", hash = "sha256:9e95545a5bb38b02f86991a4d3873d61764c044adcdeca05d8d9a2a55a885a1f"}, + {file = "cppython-0.9.6.tar.gz", hash = "sha256:313729995ba1952d8a8e7fca23f3e5be6031a5bfefede747627407ca68d6ea6c"}, +] + +[[package]] +name = "cppython" +version = "0.9.6" +extras = ["cmake", "git", "vcpkg"] +requires_python = ">=3.13" +summary = "A Python management solution for C++ dependencies" +groups = ["default"] +dependencies = [ + "cmake>=4.0.3", + "cppython==0.9.6", + "dulwich>=0.23.2", +] +files = [ + {file = "cppython-0.9.6-py3-none-any.whl", hash = "sha256:9e95545a5bb38b02f86991a4d3873d61764c044adcdeca05d8d9a2a55a885a1f"}, + {file = "cppython-0.9.6.tar.gz", hash = "sha256:313729995ba1952d8a8e7fca23f3e5be6031a5bfefede747627407ca68d6ea6c"}, +] + +[[package]] +name = "dulwich" +version = "0.24.1" +requires_python = ">=3.9" +summary = "Python Git Library" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.0; python_version < \"3.11\"", + "urllib3>=1.25", +] +files = [ + {file = "dulwich-0.24.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a11ec69fc6604228804ddfc32c85b22bc627eca4cf4ff3f27dbe822e6f29477"}, + {file = "dulwich-0.24.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a9800df7238b586b4c38c00432776781bc889cf02d756dcfb8dc0ecb8fc47a33"}, + {file = "dulwich-0.24.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3baab4a01aff890e2e6551ccbd33eb2a44173c897f0f027ad3aeab0fb057ec44"}, + {file = "dulwich-0.24.1-cp313-cp313-win32.whl", hash = "sha256:b39689aa4d143ba1fb0a687a4eb93d2e630d2c8f940aaa6c6911e9c8dca16e6a"}, + {file = "dulwich-0.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:8fca9b863b939b52c5f759d292499f0d21a7bf7f8cbb9fdeb8cdd9511c5bc973"}, + {file = "dulwich-0.24.1-py3-none-any.whl", hash = "sha256:57cc0dc5a21059698ffa4ed9a7272f1040ec48535193df84b0ee6b16bf615676"}, + {file = "dulwich-0.24.1.tar.gz", hash = "sha256:e19fd864f10f02bb834bb86167d92dcca1c228451b04458761fc13dabd447758"}, +] + +[[package]] +name = "idna" +version = "3.10" +requires_python = ">=3.6" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +requires_python = ">=3.10" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["default"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "packaging" +version = "25.0" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["default"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +requires_python = ">=3.9" +summary = "Data validation using Python type hints" +groups = ["default"] +dependencies = [ + "annotated-types>=0.6.0", + "pydantic-core==2.33.2", + "typing-extensions>=4.12.2", + "typing-inspection>=0.4.0", +] +files = [ + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +requires_python = ">=3.9" +summary = "Core functionality for Pydantic validation and serialization" +groups = ["default"] +dependencies = [ + "typing-extensions!=4.7.0,>=4.6.0", +] +files = [ + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +requires_python = ">=3.8" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["default"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[[package]] +name = "requests" +version = "2.32.4" +requires_python = ">=3.8" +summary = "Python HTTP for Humans." +groups = ["default"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, +] + +[[package]] +name = "rich" +version = "14.1.0" +requires_python = ">=3.8.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +groups = ["default"] +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", +] +files = [ + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +requires_python = ">=3.7" +summary = "Tool to Detect Surrounding Shell" +groups = ["default"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "typer" +version = "0.16.0" +requires_python = ">=3.7" +summary = "Typer, build great CLIs. Easy to code. Based on Python type hints." +groups = ["default"] +dependencies = [ + "click>=8.0.0", + "rich>=10.11.0", + "shellingham>=1.3.0", + "typing-extensions>=3.7.4.3", +] +files = [ + {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, + {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250809" +requires_python = ">=3.9" +summary = "Typing stubs for requests" +groups = ["default"] +dependencies = [ + "urllib3>=2", +] +files = [ + {file = "types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163"}, + {file = "types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3"}, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +requires_python = ">=3.9" +summary = "Backported and Experimental Type Hints for Python 3.9+" +groups = ["default"] +files = [ + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +requires_python = ">=3.9" +summary = "Runtime typing introspection tools" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.12.0", +] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +requires_python = ">=3.9" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["default"] +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] diff --git a/examples/vcpkg_cmake/simple/pyproject.toml b/examples/vcpkg_cmake/simple/pyproject.toml index be94ba0a..78c8428f 100644 --- a/examples/vcpkg_cmake/simple/pyproject.toml +++ b/examples/vcpkg_cmake/simple/pyproject.toml @@ -15,7 +15,7 @@ dependencies = ["cppython[vcpkg, cmake, git]>=0.9.0"] [tool.cppython] install-path = "install" -dependencies = ["fmt>=11.0.2"] +dependencies = ["fmt>=11.2.0"] [tool.cppython.generators.cmake] diff --git a/examples/vcpkg_cmake/simple/src/main.cpp b/examples/vcpkg_cmake/simple/src/main.cpp new file mode 100644 index 00000000..4de35678 --- /dev/null +++ b/examples/vcpkg_cmake/simple/src/main.cpp @@ -0,0 +1,7 @@ +#include "fmt/color.h" + +int main() +{ + fmt::print(fg(fmt::terminal_color::cyan), "Hello fmt {}!\n", FMT_VERSION); + return 0; +} \ No newline at end of file diff --git a/pdm.lock b/pdm.lock index aff0232d..6bf58ffc 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "cmake", "conan", "git", "lint", "pdm", "pytest", "release", "test"] strategy = [] lock_version = "4.5.0" -content_hash = "sha256:bbd888727dfc7101af521ae5aadf9644ae7879f21fb1d1b3a5f804257e8cdd4c" +content_hash = "sha256:492bd92e62664c52af0dd980a4fb4adca223d46f544bcc4774cada07a524efeb" [[metadata.targets]] requires_python = ">=3.13" @@ -86,29 +86,29 @@ files = [ [[package]] name = "cmake" -version = "4.0.3" -requires_python = ">=3.7" -summary = "" -files = [ - {file = "cmake-4.0.3-py3-none-macosx_10_10_universal2.whl", hash = "sha256:f2adfb459747025f40f9d3bdd1f3a485d43e866c0c4eb66373d1fcd666b13e4a"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:04c40c92fdcaa96c66a5731b5b3fbbdf87da99cc68fdd30ff30b90c34d222986"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d41b83d061bcc375a7a5f2942ba523a7563368d296d91260f9d8a53a10f5e5e5"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:434f84fdf1e21578974876b8414dc47afeaea62027d9adc37a943a6bb08eb053"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beec48371a4b906fe398758ded5df57fc16e9bb14fd34244d9d66ee35862fb9f"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47dc28bee6cfb4de00c7cf7e87d565b5c86eb4088da81b60a49e214fcdd4ffda"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10fdc972b3211915b65cc89e8cd24e1a26c9bd684ee71c3f369fb488f2c4388"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d840e780c48c5df1330879d50615176896e8e6eee554507d21ce8e2f1a5f0ff8"}, - {file = "cmake-4.0.3-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:6ef63bbabcbe3b89c1d80547913b6caceaad57987a27e7afc79ebc88ecd829e4"}, - {file = "cmake-4.0.3-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:67103f2bcce8f57b8705ba8e353f18fdc3684a346eee97dc5f94d11575a424c6"}, - {file = "cmake-4.0.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:880a1e1ae26d440d7e4f604fecbf839728ca7b096c870f2e7359855cc4828532"}, - {file = "cmake-4.0.3-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:c403b660bbff1fd4d7f1c5d9e015ea27566e49ca9461e260c9758f2fd4e5e813"}, - {file = "cmake-4.0.3-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:2a66ecdd4c3238484cb0c377d689c086a9b8b533e25329f73d21bd1c38f1ae86"}, - {file = "cmake-4.0.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:004e58b1a1a384c2ca799c9c41ac4ed86ac3b80129462992c43c1121f8729ffd"}, - {file = "cmake-4.0.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:133dbc33f995cb97a4456d83d67fa0a7a798f53f979454359140588baa928f43"}, - {file = "cmake-4.0.3-py3-none-win32.whl", hash = "sha256:3e07bdd14e69ea67d1e67a4f5225ac2fd91ee9e349c440143cdddd7368be1f46"}, - {file = "cmake-4.0.3-py3-none-win_amd64.whl", hash = "sha256:9a349ff2b4a7c63c896061676bc0f4e6994f373d54314d79ba3608ee7fa75442"}, - {file = "cmake-4.0.3-py3-none-win_arm64.whl", hash = "sha256:94a52e67b264a51089907c9e74ca5a9e2f3e65c57c457e0f40f02629a0de74d8"}, - {file = "cmake-4.0.3.tar.gz", hash = "sha256:215732f09ea8a7088fe1ab46bbd61669437217278d709fd3851bf8211e8c59e3"}, +version = "4.1.0" +requires_python = ">=3.8" +summary = "CMake is an open-source, cross-platform family of tools designed to build, test and package software" +files = [ + {file = "cmake-4.1.0-py3-none-macosx_10_10_universal2.whl", hash = "sha256:69df62445b22d78c2002c22edeb0e85590ae788e477d222fb2ae82c871c33090"}, + {file = "cmake-4.1.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4e3a30a4f72a8a6d8d593dc289e791f1d84352c1f629543ac8e22c62dbadb20a"}, + {file = "cmake-4.1.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0e2fea746d746f52aa52b8498777ff665a0627d9b136bec4ae0465c38b75e799"}, + {file = "cmake-4.1.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5a28a87601fa5e775017bf4f5836e8e75091d08f3e5aac411256754ba54fe5c4"}, + {file = "cmake-4.1.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2a8790473afbb895b8e684e479f26773e4fc5c86845e3438e8488d38de9db807"}, + {file = "cmake-4.1.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dab375932f5962e078da8cf76ca228c21bf4bea9ddeb1308e2b35797fa30f784"}, + {file = "cmake-4.1.0-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:f2eaa6f0a25e31fe09fb0b7f40fbf208eea5f1313093ff441ecfff7dc1b80adf"}, + {file = "cmake-4.1.0-py3-none-manylinux_2_35_riscv64.whl", hash = "sha256:3ee38de00cad0501c7dd2b94591522381e3ef9c8468094f037a17ed9e478ef13"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2d9f14b7d58e447865c111b3b90945b150724876866f5801c80970151718f710"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:574448a03acdf34c55a7c66485e7a8260709e8386e9145708e18e2abe5fc337b"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8c2538fb557b9edd74d48c189fcde42a55ad7e2c39e04254f8c5d248ca1af4c"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:7c7999c5a1d5a3a66adacc61056765557ed253dc7b8e9deab5cae546f4f9361c"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:e77ac2554a7b8a94745add465413e3266b714766e9a5d22ac8e5b36a900a1136"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:d54e68d5439193265fd7211671420601f6a672b8ca220f19e6c72238b41a84c2"}, + {file = "cmake-4.1.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c6bd346fe4d9c205310ef9a6e09ced7e610915fa982d7b649f9b12caa6fa0605"}, + {file = "cmake-4.1.0-py3-none-win32.whl", hash = "sha256:7219b7e85ed03a98af89371b9dee762e236ad94e8a09ce141070e6ac6415756f"}, + {file = "cmake-4.1.0-py3-none-win_amd64.whl", hash = "sha256:76e8e7d80a1a9bb5c7ec13ec8da961a8c5a997247f86a08b29f0c2946290c461"}, + {file = "cmake-4.1.0-py3-none-win_arm64.whl", hash = "sha256:8d39bbfee7c181e992875cd390fc6d51a317c9374656b332021a67bb40c0b07f"}, + {file = "cmake-4.1.0.tar.gz", hash = "sha256:bacdd21aebdf9a42e5631cfb365beb8221783fcd27c4e04f7db8b79c43fb12df"}, ] [[package]] @@ -122,22 +122,22 @@ files = [ [[package]] name = "conan" -version = "2.18.1" +version = "2.19.1" requires_python = ">=3.6" -summary = "" +summary = "Conan C/C++ package manager" dependencies = [ - "colorama", - "distro; platform_system == \"FreeBSD\" or sys_platform == \"linux\"", - "fasteners", - "jinja2", - "patch-ng", - "python-dateutil", - "pyyaml", - "requests", - "urllib3", + "Jinja2<4.0.0,>=3.0", + "PyYAML<7.0,>=6.0", + "colorama<0.5.0,>=0.4.3", + "distro<=1.8.0,>=1.4.0; platform_system == \"Linux\" or platform_system == \"FreeBSD\"", + "fasteners>=0.15", + "patch-ng<1.19,>=1.18.0", + "python-dateutil<3,>=2.8.0", + "requests<3.0.0,>=2.25", + "urllib3<2.1,>=1.26.6", ] files = [ - {file = "conan-2.18.1.tar.gz", hash = "sha256:5d8e9fac7614de9297933f65de8f17db14851a871cebc962f4856b7c294f43c5"}, + {file = "conan-2.19.1.tar.gz", hash = "sha256:bf334867b81bcb73e5be31afe26a0f207017719298ad1f0f64762867caa9a971"}, ] [[package]] @@ -203,20 +203,21 @@ files = [ [[package]] name = "dulwich" -version = "0.23.2" +version = "0.24.1" requires_python = ">=3.9" -summary = "" +summary = "Python Git Library" dependencies = [ - "urllib3", + "typing-extensions>=4.0; python_version < \"3.11\"", + "urllib3>=1.25", ] files = [ - {file = "dulwich-0.23.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e44dec7e36bc035da0ec3df6c1564810699e319ba41b71d17750dd7452e1b2fc"}, - {file = "dulwich-0.23.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:398ba1c0e1581071cdcb38a681e0ff1e046aa8f31bad3bc368266f499c4ddf9e"}, - {file = "dulwich-0.23.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:432c6eeac5edf97ff7090fbac7cda708167ee90e5afa78652d252e87e397f425"}, - {file = "dulwich-0.23.2-cp313-cp313-win32.whl", hash = "sha256:8555980e8509d7f76e80de58d1eb7bd2c1c317942b7a3c9c113d81dfc287f4c0"}, - {file = "dulwich-0.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:2b042dca31de4d4a0e88e4dbe20afe804a640c8882eec0de5093bffb34b75370"}, - {file = "dulwich-0.23.2-py3-none-any.whl", hash = "sha256:0b0439d309cf808f7955f74776981d9ac9dc1ec715aa39798de9b22bb95ac163"}, - {file = "dulwich-0.23.2.tar.gz", hash = "sha256:a152ebb0e95bc0f23768be563f80ff1e719bf5c4f5c2696be4fa8ab625a39879"}, + {file = "dulwich-0.24.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a11ec69fc6604228804ddfc32c85b22bc627eca4cf4ff3f27dbe822e6f29477"}, + {file = "dulwich-0.24.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a9800df7238b586b4c38c00432776781bc889cf02d756dcfb8dc0ecb8fc47a33"}, + {file = "dulwich-0.24.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3baab4a01aff890e2e6551ccbd33eb2a44173c897f0f027ad3aeab0fb057ec44"}, + {file = "dulwich-0.24.1-cp313-cp313-win32.whl", hash = "sha256:b39689aa4d143ba1fb0a687a4eb93d2e630d2c8f940aaa6c6911e9c8dca16e6a"}, + {file = "dulwich-0.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:8fca9b863b939b52c5f759d292499f0d21a7bf7f8cbb9fdeb8cdd9511c5bc973"}, + {file = "dulwich-0.24.1-py3-none-any.whl", hash = "sha256:57cc0dc5a21059698ffa4ed9a7272f1040ec48535193df84b0ee6b16bf615676"}, + {file = "dulwich-0.24.1.tar.gz", hash = "sha256:e19fd864f10f02bb834bb86167d92dcca1c228451b04458761fc13dabd447758"}, ] [[package]] @@ -474,36 +475,38 @@ files = [ [[package]] name = "pdm" -version = "2.25.4" +version = "2.25.6" requires_python = ">=3.9" -summary = "" +summary = "A modern Python package and dependency manager supporting the latest PEP standards" dependencies = [ "blinker", - "certifi", - "dep-logic", - "filelock", - "findpython", - "hishel", - "httpcore", - "httpx[socks]", - "id", - "installer", - "packaging", - "pbs-installer", + "certifi>=2024.8.30", + "dep-logic>=0.5", + "filelock>=3.13", + "findpython<1.0.0a0,>=0.7.0", + "hishel>=0.0.32", + "httpcore>=1.0.6", + "httpx[socks]<1,>0.20", + "id>=1.5.0", + "importlib-metadata>=3.6; python_version < \"3.10\"", + "installer<0.8,>=0.7", + "packaging>22.0", + "pbs-installer>=2025.6.6", "platformdirs", "pyproject-hooks", - "python-dotenv", - "resolvelib", - "rich", - "shellingham", - "tomlkit", - "truststore", - "unearth", - "virtualenv", + "python-dotenv>=0.15", + "resolvelib>=1.1", + "rich>=12.3.0", + "shellingham>=1.3.2", + "tomli>=1.1.0; python_version < \"3.11\"", + "tomlkit<1,>=0.11.1", + "truststore>=0.10.4; python_version >= \"3.10\"", + "unearth>=0.17.5", + "virtualenv>=20", ] files = [ - {file = "pdm-2.25.4-py3-none-any.whl", hash = "sha256:3efab7367cb5d9d6e4ef9db6130e4f5620c247343c8e95e18bd0d76b201ff7da"}, - {file = "pdm-2.25.4.tar.gz", hash = "sha256:bd655d789429928d6e27ff6693c19c82bc81aa75ba51d7b1c6102d039c8f211c"}, + {file = "pdm-2.25.6-py3-none-any.whl", hash = "sha256:5f18326edb40cb3d179f96583be1253ee31cf9160cc7ca4299839eaebd079f2a"}, + {file = "pdm-2.25.6.tar.gz", hash = "sha256:46693c26dde87bdeffecf18eb852ea55434c9b6b2aec42edef237f4ac595763c"}, ] [[package]] @@ -588,19 +591,19 @@ files = [ [[package]] name = "pyrefly" -version = "0.24.2" +version = "0.28.1" requires_python = ">=3.8" summary = "A fast Python type checker written in Rust" files = [ - {file = "pyrefly-0.24.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7e6bd1b88ec53b3f1ce2ece844016d7e7f0848a77022857a7fa6674a49abcc13"}, - {file = "pyrefly-0.24.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:83aa9013f2299dfc8ce11adec30a63be71528484c45e603375efe7496cb0538e"}, - {file = "pyrefly-0.24.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bf1689032b78f8f653244cd323ee1e06a0efb6192c4d7a415d1e85aedd37905"}, - {file = "pyrefly-0.24.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8404b804a5a1bc4a54cc8e58bceacdf49d7221531843c068547241d8f476af24"}, - {file = "pyrefly-0.24.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d09f166a46e43655ea812611887ca16a0c54386296f4c9333f3f5fc7236709"}, - {file = "pyrefly-0.24.2-py3-none-win32.whl", hash = "sha256:6c602df48dcfa3240f9076c7d1e9cf9dc2d94c90ee5b4c6745f3734125a2cf3a"}, - {file = "pyrefly-0.24.2-py3-none-win_amd64.whl", hash = "sha256:9ed4690716eb47077082d4e99624e0a1165b9ac93300c8d823f42cae12ec1ef4"}, - {file = "pyrefly-0.24.2-py3-none-win_arm64.whl", hash = "sha256:96ba49c02f374d716b8674409aa653093dad5263cf4e429a1d5ec603064db715"}, - {file = "pyrefly-0.24.2.tar.gz", hash = "sha256:671b9933c2a3f646983de68bc0422736f7ce364c4f645f742559423b0b9b5150"}, + {file = "pyrefly-0.28.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a4abd5218f43c25c00571fc498f85892b434d2361882a9e38ca7bb0ccb949bff"}, + {file = "pyrefly-0.28.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b0ef6859ceca146f41be152e3bc783248cac7425f904a67bb9d3b130210e03c"}, + {file = "pyrefly-0.28.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36ac5fa3dcf83d51f9a7524a391f3614e8227fa4d22f4c053437f91e763d1fa6"}, + {file = "pyrefly-0.28.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:428e8a174891a14d9e7364e20073162d66c7a0e2575dc5433e2f8228a0fe94ca"}, + {file = "pyrefly-0.28.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a5f66c43fc2e4676307b539d1ed085744a5625c579365cfc889cb9faf9ef8d0"}, + {file = "pyrefly-0.28.1-py3-none-win32.whl", hash = "sha256:ccf2e7d1253de03940953aeb8746c189435899620d113cb05114e8d2175892e4"}, + {file = "pyrefly-0.28.1-py3-none-win_amd64.whl", hash = "sha256:cb973dc1fc3c128f9d674f943eca9eea6d4ed272a329836efc9d6e5c16ebe12a"}, + {file = "pyrefly-0.28.1-py3-none-win_arm64.whl", hash = "sha256:b3aa87f12555dda76b60aa101466ad5fde54a53f20c5112b02ea2eaaf0d6bfe9"}, + {file = "pyrefly-0.28.1.tar.gz", hash = "sha256:9ebc67e4a2e3d33c78f1962e7b2a16cd9b4415ce22fcf7a290b741ed9f3b7535"}, ] [[package]] @@ -750,28 +753,29 @@ files = [ [[package]] name = "ruff" -version = "0.12.4" +version = "0.12.9" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." files = [ - {file = "ruff-0.12.4-py3-none-linux_armv6l.whl", hash = "sha256:cb0d261dac457ab939aeb247e804125a5d521b21adf27e721895b0d3f83a0d0a"}, - {file = "ruff-0.12.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:55c0f4ca9769408d9b9bac530c30d3e66490bd2beb2d3dae3e4128a1f05c7442"}, - {file = "ruff-0.12.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a8224cc3722c9ad9044da7f89c4c1ec452aef2cfe3904365025dd2f51daeae0e"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9949d01d64fa3672449a51ddb5d7548b33e130240ad418884ee6efa7a229586"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:be0593c69df9ad1465e8a2d10e3defd111fdb62dcd5be23ae2c06da77e8fcffb"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7dea966bcb55d4ecc4cc3270bccb6f87a337326c9dcd3c07d5b97000dbff41c"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afcfa3ab5ab5dd0e1c39bf286d829e042a15e966b3726eea79528e2e24d8371a"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c057ce464b1413c926cdb203a0f858cd52f3e73dcb3270a3318d1630f6395bb3"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64b90d1122dc2713330350626b10d60818930819623abbb56535c6466cce045"}, - {file = "ruff-0.12.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abc48f3d9667fdc74022380b5c745873499ff827393a636f7a59da1515e7c57"}, - {file = "ruff-0.12.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2b2449dc0c138d877d629bea151bee8c0ae3b8e9c43f5fcaafcd0c0d0726b184"}, - {file = "ruff-0.12.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:56e45bb11f625db55f9b70477062e6a1a04d53628eda7784dce6e0f55fd549eb"}, - {file = "ruff-0.12.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:478fccdb82ca148a98a9ff43658944f7ab5ec41c3c49d77cd99d44da019371a1"}, - {file = "ruff-0.12.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0fc426bec2e4e5f4c4f182b9d2ce6a75c85ba9bcdbe5c6f2a74fcb8df437df4b"}, - {file = "ruff-0.12.4-py3-none-win32.whl", hash = "sha256:4de27977827893cdfb1211d42d84bc180fceb7b72471104671c59be37041cf93"}, - {file = "ruff-0.12.4-py3-none-win_amd64.whl", hash = "sha256:fe0b9e9eb23736b453143d72d2ceca5db323963330d5b7859d60d101147d461a"}, - {file = "ruff-0.12.4-py3-none-win_arm64.whl", hash = "sha256:0618ec4442a83ab545e5b71202a5c0ed7791e8471435b94e655b570a5031a98e"}, - {file = "ruff-0.12.4.tar.gz", hash = "sha256:13efa16df6c6eeb7d0f091abae50f58e9522f3843edb40d56ad52a5a4a4b6873"}, + {file = "ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e"}, + {file = "ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f"}, + {file = "ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb"}, + {file = "ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af"}, + {file = "ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc"}, + {file = "ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66"}, + {file = "ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7"}, + {file = "ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93"}, + {file = "ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908"}, + {file = "ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089"}, + {file = "ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a"}, ] [[package]] @@ -821,11 +825,12 @@ files = [ [[package]] name = "truststore" -version = "0.10.1" -summary = "" +version = "0.10.4" +requires_python = ">=3.10" +summary = "Verify certificates using native system trust stores" files = [ - {file = "truststore-0.10.1-py3-none-any.whl", hash = "sha256:b64e6025a409a43ebdd2807b0c41c8bff49ea7ae6550b5087ac6df6619352d4c"}, - {file = "truststore-0.10.1.tar.gz", hash = "sha256:eda021616b59021812e800fa0a071e51b266721bef3ce092db8a699e21c63539"}, + {file = "truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981"}, + {file = "truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301"}, ] [[package]] @@ -846,15 +851,15 @@ files = [ [[package]] name = "types-requests" -version = "2.32.4.20250611" +version = "2.32.4.20250809" requires_python = ">=3.9" -summary = "" +summary = "Typing stubs for requests" dependencies = [ - "urllib3", + "urllib3>=2", ] files = [ - {file = "types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072"}, - {file = "types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826"}, + {file = "types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163"}, + {file = "types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 405cd794..5678dd20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,27 +16,15 @@ dependencies = [ "pydantic>=2.11.7", "packaging>=25.0", "requests>=2.32.4", - "types-requests>=2.32.4.20250611", + "types-requests>=2.32.4.20250809", ] [project.optional-dependencies] -pytest = [ - "pytest>=8.4.1", - "pytest-mock>=3.14.1", -] -git = [ - "dulwich>=0.23.2", -] -pdm = [ - "pdm>=2.25.4", -] -cmake = [ - "cmake>=4.0.3", -] -conan = [ - "conan>=2.18.1", - "libcst>=1.8.2", -] +pytest = ["pytest>=8.4.1", "pytest-mock>=3.14.1"] +git = ["dulwich>=0.24.1"] +pdm = ["pdm>=2.25.6"] +cmake = ["cmake>=4.1.0"] +conan = ["conan>=2.19.1", "libcst>=1.8.2"] [project.urls] homepage = "https://github.com/Synodic-Software/CPPython" @@ -59,15 +47,8 @@ cppython = "cppython.plugins.pdm.plugin:CPPythonPlugin" cppython = "cppython.test.pytest.fixtures" [dependency-groups] -lint = [ - "ruff>=0.12.4", - "pyrefly>=0.24.2", -] -test = [ - "pytest>=8.4.1", - "pytest-cov>=6.2.1", - "pytest-mock>=3.14.1", -] +lint = ["ruff>=0.12.9", "pyrefly>=0.28.1"] +test = ["pytest>=8.4.1", "pytest-cov>=6.2.1", "pytest-mock>=3.14.1"] [project.scripts] cppython = "cppython.console.entry:app" @@ -105,6 +86,9 @@ quote-style = "single" [tool.coverage.report] skip_empty = true +[tool.pyrefly] +project-excludes = ["examples"] + [tool.pdm] plugins = ["-e file:///${PROJECT_ROOT}"] diff --git a/tests/fixtures/conan.py b/tests/fixtures/conan.py index f13eb0d9..11381577 100644 --- a/tests/fixtures/conan.py +++ b/tests/fixtures/conan.py @@ -166,14 +166,12 @@ def fixture_conan_mock_dependencies() -> list[Requirement]: @pytest.fixture(name='conan_setup_mocks') def fixture_conan_setup_mocks( plugin: ConanProvider, - conan_mock_api: Mock, mocker: MockerFixture, ) -> dict[str, Mock]: """Sets up all mocks for testing install/update operations Args: plugin: The plugin instance - conan_mock_api: Mock ConanAPI instance mocker: Pytest mocker fixture Returns: @@ -185,8 +183,9 @@ def fixture_conan_setup_mocks( # Set the builder attribute on the plugin plugin.builder = mock_builder # type: ignore[attr-defined] - # Mock ConanAPI constructor - mock_conan_api_constructor = mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api) + # Mock subprocess.run to simulate successful command execution + mock_subprocess_run = mocker.patch('cppython.plugins.conan.plugin.subprocess.run') + mock_subprocess_run.return_value = mocker.Mock(returncode=0) # Mock resolve_conan_dependency def mock_resolve(requirement: Requirement) -> ConanDependency: @@ -196,8 +195,13 @@ def mock_resolve(requirement: Requirement) -> ConanDependency: 'cppython.plugins.conan.plugin.resolve_conan_dependency', side_effect=mock_resolve ) + # Mock getLogger to avoid logging setup issues + mock_logger = mocker.Mock() + mocker.patch('cppython.plugins.conan.plugin.getLogger', return_value=mock_logger) + return { 'builder': mock_builder, - 'conan_api_constructor': mock_conan_api_constructor, + 'subprocess_run': mock_subprocess_run, 'resolve_conan_dependency': mock_resolve_conan_dependency, + 'logger': mock_logger, } diff --git a/tests/fixtures/vcpkg.py b/tests/fixtures/vcpkg.py new file mode 100644 index 00000000..b59fc287 --- /dev/null +++ b/tests/fixtures/vcpkg.py @@ -0,0 +1 @@ +"""Shared fixtures for VCPkg plugin tests""" diff --git a/tests/integration/examples/test_conan_cmake.py b/tests/integration/examples/test_conan_cmake.py index f62f3421..8b5634d9 100644 --- a/tests/integration/examples/test_conan_cmake.py +++ b/tests/integration/examples/test_conan_cmake.py @@ -21,41 +21,83 @@ class TestConanCMake: """Test project variation of conan and CMake""" @staticmethod - def test_simple(example_runner: CliRunner) -> None: - """Simple project""" - # Create project configuration + def _create_project(skip_upload: bool = True) -> Project: + """Create a project instance with common configuration.""" project_root = Path.cwd() - project_configuration = ProjectConfiguration(project_root=project_root, version=None, verbosity=2, debug=True) - - # Create console interface + config = ProjectConfiguration(project_root=project_root, version=None, verbosity=2, debug=True) interface = ConsoleInterface() - # Load pyproject.toml data pyproject_path = project_root / 'pyproject.toml' pyproject_data = loads(pyproject_path.read_text(encoding='utf-8')) - # Create and use the project directly - project = Project(project_configuration, interface, pyproject_data) + if skip_upload: + TestConanCMake._ensure_conan_config(pyproject_data) + pyproject_data['tool']['cppython']['providers']['conan']['skip_upload'] = True - # Call install directly to get structured results - project.install() + return Project(config, interface, pyproject_data) - # Run the CMake configuration command + @staticmethod + def _run_cmake_configure() -> None: + """Run CMake configuration and assert success.""" result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False) + assert result.returncode == 0, f'CMake configuration failed: {result.stderr}' - assert result.returncode == 0, f'Cmake failed: {result.stderr}' + @staticmethod + def _run_cmake_build() -> None: + """Run CMake build and assert success.""" + result = subprocess.run(['cmake', '--build', 'build'], capture_output=True, text=True, check=False) + assert result.returncode == 0, f'CMake build failed: {result.stderr}' - path = Path('build').absolute() + @staticmethod + def _verify_build_artifacts() -> Path: + """Verify basic build artifacts exist and return build path.""" + build_path = Path('build').absolute() + assert (build_path / 'CMakeCache.txt').exists(), f'CMakeCache.txt not found in {build_path}' + return build_path - # Verify that the build directory contains the expected files - assert (path / 'CMakeCache.txt').exists(), f'{path / "CMakeCache.txt"} not found' + @staticmethod + def _ensure_conan_config(pyproject_data: dict) -> None: + """Helper method to ensure Conan configuration exists in pyproject data""" + if 'tool' not in pyproject_data: + pyproject_data['tool'] = {} + if 'cppython' not in pyproject_data['tool']: + pyproject_data['tool']['cppython'] = {} + if 'providers' not in pyproject_data['tool']['cppython']: + pyproject_data['tool']['cppython']['providers'] = {} + if 'conan' not in pyproject_data['tool']['cppython']['providers']: + pyproject_data['tool']['cppython']['providers']['conan'] = {} + + @staticmethod + def test_simple(example_runner: CliRunner) -> None: + """Simple project""" + # Create project and install dependencies + project = TestConanCMake._create_project(skip_upload=False) + project.install() + + # Configure and verify build + TestConanCMake._run_cmake_configure() + TestConanCMake._verify_build_artifacts() + + # Test publishing with skip_upload enabled + publish_project = TestConanCMake._create_project(skip_upload=True) + publish_project.publish() + + @staticmethod + def test_library(example_runner: CliRunner) -> None: + """Test library creation and packaging workflow""" + # Create project and install dependencies + project = TestConanCMake._create_project(skip_upload=False) + project.install() - # --- Setup for Publish with modified config --- - # Modify the in-memory representation of the pyproject data - pyproject_data['tool']['cppython']['providers']['conan']['skip_upload'] = True + # Configure, build, and verify + TestConanCMake._run_cmake_configure() + TestConanCMake._run_cmake_build() + build_path = TestConanCMake._verify_build_artifacts() - # Create a new project instance with the modified configuration for the 'publish' step - publish_project = Project(project_configuration, interface, pyproject_data) + # Verify library files exist (platform-specific) + lib_files = list(build_path.glob('**/libmathutils.*')) + list(build_path.glob('**/mathutils.lib')) + assert len(lib_files) > 0, f'No library files found in {build_path}' - # Publish the project to the local cache + # Package the library to local cache + publish_project = TestConanCMake._create_project(skip_upload=True) publish_project.publish() diff --git a/tests/integration/examples/test_vcpkg_cmake.py b/tests/integration/examples/test_vcpkg_cmake.py index f7486a3e..2488e63c 100644 --- a/tests/integration/examples/test_vcpkg_cmake.py +++ b/tests/integration/examples/test_vcpkg_cmake.py @@ -6,24 +6,56 @@ import subprocess from pathlib import Path +from tomllib import loads import pytest from typer.testing import CliRunner -pytest_plugins = ['tests.fixtures.example'] +from cppython.console.schema import ConsoleInterface +from cppython.core.schema import ProjectConfiguration +from cppython.project import Project +pytest_plugins = ['tests.fixtures.example', 'tests.fixtures.vcpkg'] + +@pytest.mark.skip(reason='Address file locks.') class TestVcpkgCMake: """Test project variation of vcpkg and CMake""" @staticmethod - @pytest.mark.skip(reason='TODO') + def _create_project(skip_upload: bool = True) -> Project: + """Create a project instance with common configuration.""" + project_root = Path.cwd() + config = ProjectConfiguration(project_root=project_root, version=None, verbosity=2, debug=True) + interface = ConsoleInterface() + + pyproject_path = project_root / 'pyproject.toml' + pyproject_data = loads(pyproject_path.read_text(encoding='utf-8')) + + if skip_upload: + TestVcpkgCMake._ensure_vcpkg_config(pyproject_data) + pyproject_data['tool']['cppython']['providers']['vcpkg']['skip_upload'] = True + + return Project(config, interface, pyproject_data) + + @staticmethod + def _ensure_vcpkg_config(pyproject_data: dict) -> None: + """Helper method to ensure Vcpkg configuration exists in pyproject data""" + if 'tool' not in pyproject_data: + pyproject_data['tool'] = {} + if 'cppython' not in pyproject_data['tool']: + pyproject_data['tool']['cppython'] = {} + if 'providers' not in pyproject_data['tool']['cppython']: + pyproject_data['tool']['cppython']['providers'] = {} + if 'vcpkg' not in pyproject_data['tool']['cppython']['providers']: + pyproject_data['tool']['cppython']['providers']['vcpkg'] = {} + + @staticmethod def test_simple(example_runner: CliRunner) -> None: """Simple project""" - # By nature of running the test, we require PDM to develop the project and so it will be installed - result = subprocess.run(['pdm', 'install'], capture_output=True, text=True, check=False) - - assert result.returncode == 0, f'PDM install failed: {result.stderr}' + # Create project and install dependencies + project = TestVcpkgCMake._create_project(skip_upload=False) + project.install() # Run the CMake configuration command result = subprocess.run(['cmake', '--preset=default'], capture_output=True, text=True, check=False) diff --git a/tests/unit/plugins/cmake/test_presets.py b/tests/unit/plugins/cmake/test_presets.py index 253592bc..5b0949f7 100644 --- a/tests/unit/plugins/cmake/test_presets.py +++ b/tests/unit/plugins/cmake/test_presets.py @@ -1,6 +1,6 @@ """Tests for CMakePresets""" -from pathlib import Path +import json from cppython.core.schema import ProjectData from cppython.plugins.cmake.builder import Builder @@ -53,43 +53,6 @@ def test_generate_root_preset_existing(project_data: ProjectData) -> None: class TestWrites: """Tests for writing the CMakePresets class""" - @staticmethod - def test_provider_write(tmp_path: Path) -> None: - """Verifies that the provider preset writing works as intended - - Args: - tmp_path: The input path the use - """ - builder = Builder() - - includes_file = tmp_path / 'includes.cmake' - with includes_file.open('w', encoding='utf-8') as file: - file.write('example contents') - - data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) - builder.write_provider_preset(tmp_path, data) - - @staticmethod - def test_cppython_write(tmp_path: Path) -> None: - """Verifies that the cppython preset writing works as intended - - Args: - tmp_path: The input path the use - """ - builder = Builder() - - provider_directory = tmp_path / 'providers' - provider_directory.mkdir(parents=True, exist_ok=True) - - includes_file = provider_directory / 'includes.cmake' - with includes_file.open('w', encoding='utf-8') as file: - file.write('example contents') - - data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) - builder.write_provider_preset(provider_directory, data) - - builder.write_cppython_preset(tmp_path, provider_directory, data) - @staticmethod def test_root_write(project_data: ProjectData) -> None: """Verifies that the root preset writing works as intended @@ -116,10 +79,15 @@ def test_root_write(project_data: ProjectData) -> None: with open(root_file, 'w', encoding='utf8') as file: file.write(serialized) - data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) - builder.write_provider_preset(provider_directory, data) + # Create a mock provider preset file + provider_preset_file = provider_directory / 'CMakePresets.json' + provider_preset_data = {'version': 3, 'configurePresets': [{'name': 'test-provider-base', 'hidden': True}]} + with provider_preset_file.open('w') as f: + json.dump(provider_preset_data, f) + + data = CMakeSyncData(provider_name=TypeName('test-provider')) - cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data) + cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_preset_file, data) build_directory = project_data.project_root / 'build' builder.write_root_presets( @@ -157,10 +125,15 @@ def test_relative_root_write(project_data: ProjectData) -> None: with open(root_file, 'w', encoding='utf8') as file: file.write(serialized) - data = CMakeSyncData(provider_name=TypeName('test-provider'), top_level_includes=includes_file) - builder.write_provider_preset(provider_directory, data) + # Create a mock provider preset file + provider_preset_file = provider_directory / 'CMakePresets.json' + provider_preset_data = {'version': 3, 'configurePresets': [{'name': 'test-provider-base', 'hidden': True}]} + with provider_preset_file.open('w') as f: + json.dump(provider_preset_data, f) + + data = CMakeSyncData(provider_name=TypeName('test-provider')) - cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_directory, data) + cppython_preset_file = builder.write_cppython_preset(cppython_preset_directory, provider_preset_file, data) build_directory = project_data.project_root / 'build' builder.write_root_presets( diff --git a/tests/unit/plugins/conan/test_install.py b/tests/unit/plugins/conan/test_install.py index d90c9824..74fd89f6 100644 --- a/tests/unit/plugins/conan/test_install.py +++ b/tests/unit/plugins/conan/test_install.py @@ -1,21 +1,11 @@ """Unit tests for the conan plugin install functionality""" -from pathlib import Path from typing import Any -from unittest.mock import Mock import pytest -from packaging.requirements import Requirement -from pytest_mock import MockerFixture from cppython.plugins.conan.plugin import ConanProvider -from cppython.plugins.conan.schema import ConanDependency from cppython.test.pytest.mixins import ProviderPluginTestMixin -from cppython.utility.exception import ProviderInstallationError - -# Constants for test assertions -EXPECTED_PROFILE_CALLS = 2 -EXPECTED_GET_PROFILE_CALLS = 2 # Use shared fixtures pytest_plugins = ['tests.fixtures.conan'] @@ -46,127 +36,3 @@ def fixture_plugin_type() -> type[ConanProvider]: The type of the Provider """ return ConanProvider - - def test_with_dependencies( - self, - plugin: ConanProvider, - conan_temp_conanfile: Path, - conan_mock_dependencies: list[Requirement], - conan_setup_mocks: dict[str, Mock], - ) -> None: - """Test install method with dependencies and existing conanfile - - Args: - plugin: The plugin instance - conan_temp_conanfile: Path to temporary conanfile.py - conan_mock_dependencies: List of mock dependencies - conan_setup_mocks: Dictionary containing all mocks - """ - # Setup dependencies - plugin.core_data.cppython_data.dependencies = conan_mock_dependencies - - # Execute - plugin.install() - - # Verify builder was called - conan_setup_mocks['builder'].generate_conanfile.assert_called_once() - assert ( - conan_setup_mocks['builder'].generate_conanfile.call_args[0][0] - == plugin.core_data.project_data.project_root - ) - assert len(conan_setup_mocks['builder'].generate_conanfile.call_args[0][1]) == EXPECTED_DEPENDENCY_COUNT - - # Verify dependency resolution was called - assert conan_setup_mocks['resolve_conan_dependency'].call_count == EXPECTED_DEPENDENCY_COUNT - - # Verify build path was created - assert plugin.core_data.cppython_data.build_path.exists() - - # Verify ConanAPI constructor was called - conan_setup_mocks['conan_api_constructor'].assert_called_once() - - def test_conan_command_failure( - self, - plugin: ConanProvider, - conan_temp_conanfile: Path, - conan_mock_dependencies: list[Requirement], - conan_mock_api: Mock, - mocker: MockerFixture, - ) -> None: - """Test install method when conan API operations fail - - Args: - plugin: The plugin instance - conan_temp_conanfile: Path to temporary conanfile.py - conan_mock_dependencies: List of mock dependencies - conan_mock_api: Mock ConanAPI instance - mocker: Pytest mocker fixture - """ - # Mock builder - mock_builder = mocker.Mock() - mock_builder.generate_conanfile = mocker.Mock() - plugin.builder = mock_builder # type: ignore[attr-defined] - - # Configure the API mock to fail on graph loading - conan_mock_api.graph.load_graph_consumer.side_effect = Exception('Conan API error: package not found') - - # Mock ConanAPI constructor to return our configured mock - mock_conan_api_constructor = mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api) - - # Mock resolve_conan_dependency - def mock_resolve(requirement: Requirement) -> ConanDependency: - return ConanDependency(name=requirement.name) - - mocker.patch('cppython.plugins.conan.plugin.resolve_conan_dependency', side_effect=mock_resolve) - - # Add a dependency - plugin.core_data.cppython_data.dependencies = [conan_mock_dependencies[0]] - - # Execute and verify exception is raised - with pytest.raises( - ProviderInstallationError, - match='Failed to load dependency graph: Conan API error: package not found', - ): - plugin.install() - - # Verify builder was still called - mock_builder.generate_conanfile.assert_called_once() - - # Verify Conan API was attempted - mock_conan_api_constructor.assert_called_once() - - def test_with_default_profiles( - self, - plugin: ConanProvider, - conan_temp_conanfile: Path, - conan_mock_dependencies: list[Requirement], - conan_setup_mocks: dict[str, Mock], - conan_mock_api: Mock, - ) -> None: - """Test install method uses pre-resolved profiles from plugin construction - - Args: - plugin: The plugin instance - conan_temp_conanfile: Path to temporary conanfile.py - conan_mock_dependencies: List of mock dependencies - conan_setup_mocks: Dictionary containing all mocks - conan_mock_api: Mock ConanAPI instance - """ - # Setup dependencies - plugin.core_data.cppython_data.dependencies = conan_mock_dependencies - - # Execute - should use the profiles resolved during plugin construction - plugin.install() - - # Verify that the API was used for installation - conan_setup_mocks['conan_api_constructor'].assert_called_once() - - # Verify the rest of the process continued with resolved profiles - conan_mock_api.graph.load_graph_consumer.assert_called_once() - conan_mock_api.install.install_binaries.assert_called_once() - conan_mock_api.install.install_consumer.assert_called_once() - - # Verify that the resolved profiles were used in the graph loading - call_args = conan_mock_api.graph.load_graph_consumer.call_args - assert call_args.kwargs['profile_host'] == plugin.data.host_profile - assert call_args.kwargs['profile_build'] == plugin.data.build_profile diff --git a/tests/unit/plugins/conan/test_publish.py b/tests/unit/plugins/conan/test_publish.py index 163eeba1..c1bd7609 100644 --- a/tests/unit/plugins/conan/test_publish.py +++ b/tests/unit/plugins/conan/test_publish.py @@ -1,21 +1,15 @@ """Unit tests for the conan plugin publish functionality""" from typing import Any -from unittest.mock import MagicMock, Mock import pytest -from pytest_mock import MockerFixture from cppython.plugins.conan.plugin import ConanProvider from cppython.test.pytest.mixins import ProviderPluginTestMixin -from cppython.utility.exception import ProviderConfigurationError, ProviderInstallationError # Use shared fixtures pytest_plugins = ['tests.fixtures.conan'] -# Constants for test assertions -EXPECTED_PROFILE_CALLS = 2 - class TestConanPublish(ProviderPluginTestMixin[ConanProvider]): """Tests for the Conan provider publish functionality""" @@ -41,248 +35,3 @@ def fixture_plugin_type() -> type[ConanProvider]: The type of the Provider """ return ConanProvider - - def test_skip_upload( - self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture - ) -> None: - """Test that publish with skip_upload=True only exports and builds locally - - Args: - plugin: The plugin instance - conan_mock_api_publish: Mock ConanAPI for publish operations - conan_temp_conanfile: Fixture to create conanfile.py - mocker: Pytest mocker fixture - """ - # Set plugin to skip upload mode - plugin.data.skip_upload = True - - # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) - - # Mock the dependencies graph - mock_graph = mocker.Mock() - conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph - - # Execute publish - plugin.publish() - - # Verify export was called - conan_mock_api_publish.export.export.assert_called_once() - - # Verify graph loading and analysis - conan_mock_api_publish.graph.load_graph_consumer.assert_called_once() - conan_mock_api_publish.graph.analyze_binaries.assert_called_once_with( - graph=mock_graph, - build_mode=['*'], - remotes=conan_mock_api_publish.remotes.list(), - update=None, - lockfile=None, - ) - - # Verify install was called - conan_mock_api_publish.install.install_binaries.assert_called_once_with( - deps_graph=mock_graph, remotes=conan_mock_api_publish.remotes.list() - ) - - # Verify upload was NOT called for local mode - conan_mock_api_publish.upload.upload_full.assert_not_called() - - def test_with_upload( - self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture - ) -> None: - """Test that publish with remotes=['conancenter'] exports, builds, and uploads - - Args: - plugin: The plugin instance - conan_mock_api_publish: Mock ConanAPI for publish operations - conan_temp_conanfile: Fixture to create conanfile.py - mocker: Pytest mocker fixture - """ - # Set plugin to upload mode - plugin.data.remotes = ['conancenter'] - - # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) - - # Mock the dependencies graph - mock_graph = mocker.Mock() - conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph - - # Execute publish - plugin.publish() - - # Verify all steps were called - conan_mock_api_publish.export.export.assert_called_once() - conan_mock_api_publish.graph.load_graph_consumer.assert_called_once() - conan_mock_api_publish.graph.analyze_binaries.assert_called_once() - conan_mock_api_publish.install.install_binaries.assert_called_once() - - # Verify upload was called - conan_mock_api_publish.list.select.assert_called_once() - conan_mock_api_publish.upload.upload_full.assert_called_once() - - def test_no_remotes_configured( - self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture - ) -> None: - """Test that publish raises error when no remotes are configured for upload - - Args: - plugin: The plugin instance - conan_mock_api_publish: Mock ConanAPI for publish operations - conan_temp_conanfile: Fixture to create conanfile.py - mocker: Pytest mocker fixture - """ - # Set plugin to upload mode - plugin.data.remotes = ['conancenter'] - - # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) - - # Mock the dependencies graph - mock_graph = mocker.Mock() - conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph - - # Mock no remotes configured - conan_mock_api_publish.remotes.list.return_value = [] - - # Execute publish and expect ProviderConfigurationError - with pytest.raises(ProviderConfigurationError, match='No configured remotes found'): - plugin.publish() - - def test_no_packages_found( - self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture - ) -> None: - """Test that publish raises error when no packages are found to upload - - Args: - plugin: The plugin instance - conan_mock_api_publish: Mock ConanAPI for publish operations - conan_temp_conanfile: Fixture to create conanfile.py - mocker: Pytest mocker fixture - """ - # Set plugin to upload mode - plugin.data.remotes = ['conancenter'] - - # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) - - # Mock the dependencies graph - mock_graph = mocker.Mock() - conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph - - # Mock empty package list - mock_select_result = mocker.Mock() - mock_select_result.recipes = [] - conan_mock_api_publish.list.select.return_value = mock_select_result - - # Execute publish and expect ProviderInstallationError - with pytest.raises(ProviderInstallationError, match='No packages found to upload'): - plugin.publish() - - def test_with_default_profiles( - self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture - ) -> None: - """Test that publish uses pre-resolved profiles from plugin construction - - Args: - plugin: The plugin instance - conan_mock_api_publish: Mock ConanAPI for publish operations - conan_temp_conanfile: Fixture to create conanfile.py - mocker: Pytest mocker fixture - """ - # Set plugin to skip upload mode - plugin.data.skip_upload = True - - # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) - - # Mock the dependencies graph - mock_graph = mocker.Mock() - conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph - - # Execute publish - plugin.publish() - - # Verify that the resolved profiles were used in the graph loading - conan_mock_api_publish.graph.load_graph_consumer.assert_called_once() - call_args = conan_mock_api_publish.graph.load_graph_consumer.call_args - assert call_args.kwargs['profile_host'] == plugin.data.host_profile - assert call_args.kwargs['profile_build'] == plugin.data.build_profile - - def test_upload_parameters( - self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture - ) -> None: - """Test that publish upload is called with correct parameters - - Args: - plugin: The plugin instance - conan_mock_api_publish: Mock ConanAPI for publish operations - conan_temp_conanfile: Fixture to create conanfile.py - mocker: Pytest mocker fixture - """ - # Set plugin to upload mode - plugin.data.remotes = ['conancenter'] - - # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) - - # Mock the dependencies graph - mock_graph = mocker.Mock() - conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph - - # Mock remotes and package list - mock_remote = MagicMock() - mock_remote.name = 'conancenter' - remotes = [mock_remote] - conan_mock_api_publish.remotes.list.return_value = remotes - - mock_package_list = MagicMock() - mock_package_list.recipes = ['test_package/1.0@user/channel'] - conan_mock_api_publish.list.select.return_value = mock_package_list - - # Execute publish - plugin.publish() - - # Verify upload_full was called with correct parameters - conan_mock_api_publish.upload.upload_full.assert_called_once_with( - package_list=mock_package_list, - remote=mock_remote, - enabled_remotes=remotes, - check_integrity=False, - force=False, - metadata=None, - dry_run=False, - ) - - def test_list_pattern_creation( - self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture - ) -> None: - """Test that publish creates correct ListPattern for package selection - - Args: - plugin: The plugin instance - conan_mock_api_publish: Mock ConanAPI for publish operations - conan_temp_conanfile: Fixture to create conanfile.py - mocker: Pytest mocker fixture - """ - # Set plugin to upload mode - plugin.data.remotes = ['conancenter'] - - # Mock the necessary imports and API creation - mocker.patch('cppython.plugins.conan.plugin.ConanAPI', return_value=conan_mock_api_publish) - mock_list_pattern = mocker.patch('cppython.plugins.conan.plugin.ListPattern') - - # Mock the dependencies graph - mock_graph = mocker.Mock() - conan_mock_api_publish.graph.load_graph_consumer.return_value = mock_graph - - # Execute publish - plugin.publish() - - # Get the ref from the export call to verify ListPattern creation - # The export call returns (ref, conanfile) - we need the ref.name - export_return = conan_mock_api_publish.export.export.return_value - ref = export_return[0] # First element of the tuple - - # Verify ListPattern was created with correct reference pattern - mock_list_pattern.assert_called_once_with(f'{ref.name}/*', package_id='*', only_recipe=False) diff --git a/tests/unit/plugins/conan/test_resolution.py b/tests/unit/plugins/conan/test_resolution.py index a9034fe1..7f5889e5 100644 --- a/tests/unit/plugins/conan/test_resolution.py +++ b/tests/unit/plugins/conan/test_resolution.py @@ -1,29 +1,19 @@ """Unit tests for Conan resolution functionality.""" -import logging -from unittest.mock import Mock, patch - import pytest -from conan.internal.model.profile import Profile from packaging.requirements import Requirement from cppython.core.exception import ConfigException -from cppython.core.schema import CorePluginData from cppython.plugins.conan.resolution import ( - _profile_post_process, - _resolve_profiles, - resolve_conan_data, resolve_conan_dependency, ) from cppython.plugins.conan.schema import ( - ConanData, ConanDependency, ConanRevision, ConanUserChannel, ConanVersion, ConanVersionRange, ) -from cppython.utility.exception import ProviderConfigurationError # Constants for test validation EXPECTED_PROFILE_CALL_COUNT = 2 @@ -136,46 +126,46 @@ def test_requires_no_version(self) -> None: def test_with_user_channel(self) -> None: """Test that ConanDependency handles user/channel correctly.""" dependency = ConanDependency( - name='mylib', + name='example', version=ConanVersion.from_string('1.0.0'), user_channel=ConanUserChannel(user='myuser', channel='stable'), ) - assert dependency.requires() == 'mylib/1.0.0@myuser/stable' + assert dependency.requires() == 'example/1.0.0@myuser/stable' def test_with_revision(self) -> None: """Test that ConanDependency handles revisions correctly.""" dependency = ConanDependency( - name='mylib', version=ConanVersion.from_string('1.0.0'), revision=ConanRevision(revision='abc123') + name='example', version=ConanVersion.from_string('1.0.0'), revision=ConanRevision(revision='abc123') ) - assert dependency.requires() == 'mylib/1.0.0#abc123' + assert dependency.requires() == 'example/1.0.0#abc123' def test_full_reference(self) -> None: """Test that ConanDependency handles full references correctly.""" dependency = ConanDependency( - name='mylib', + name='example', version=ConanVersion.from_string('1.0.0'), user_channel=ConanUserChannel(user='myuser', channel='stable'), revision=ConanRevision(revision='abc123'), ) - assert dependency.requires() == 'mylib/1.0.0@myuser/stable#abc123' + assert dependency.requires() == 'example/1.0.0@myuser/stable#abc123' def test_from_reference_simple(self) -> None: """Test parsing a simple package name.""" - dependency = ConanDependency.from_conan_reference('mylib') + dependency = ConanDependency.from_conan_reference('example') - assert dependency.name == 'mylib' + assert dependency.name == 'example' assert dependency.version is None assert dependency.user_channel is None assert dependency.revision is None def test_from_reference_with_version(self) -> None: """Test parsing a package with version.""" - dependency = ConanDependency.from_conan_reference('mylib/1.0.0') + dependency = ConanDependency.from_conan_reference('example/1.0.0') - assert dependency.name == 'mylib' + assert dependency.name == 'example' assert dependency.version is not None assert str(dependency.version) == '1.0.0' assert dependency.user_channel is None @@ -183,9 +173,9 @@ def test_from_reference_with_version(self) -> None: def test_from_reference_with_version_range(self) -> None: """Test parsing a package with version range.""" - dependency = ConanDependency.from_conan_reference('mylib/[>=1.0 <2.0]') + dependency = ConanDependency.from_conan_reference('example/[>=1.0 <2.0]') - assert dependency.name == 'mylib' + assert dependency.name == 'example' assert dependency.version is None assert dependency.version_range is not None assert dependency.version_range.expression == '>=1.0 <2.0' @@ -194,9 +184,9 @@ def test_from_reference_with_version_range(self) -> None: def test_from_reference_full(self) -> None: """Test parsing a full Conan reference.""" - dependency = ConanDependency.from_conan_reference('mylib/1.0.0@myuser/stable#abc123') + dependency = ConanDependency.from_conan_reference('example/1.0.0@myuser/stable#abc123') - assert dependency.name == 'mylib' + assert dependency.name == 'example' assert dependency.version is not None assert str(dependency.version) == '1.0.0' assert dependency.user_channel is not None @@ -206,292 +196,9 @@ def test_from_reference_full(self) -> None: assert dependency.revision.revision == 'abc123' -class TestProfileProcessing: - """Test profile processing functionality.""" - - def test_success(self) -> None: - """Test successful profile processing.""" - mock_conan_api = Mock() - mock_profile = Mock() - mock_cache_settings = Mock() - mock_plugin = Mock() - - mock_conan_api.profiles._load_profile_plugin.return_value = mock_plugin - profiles = [mock_profile] - - _profile_post_process(profiles, mock_conan_api, mock_cache_settings) - - mock_plugin.assert_called_once_with(mock_profile) - mock_profile.process_settings.assert_called_once_with(mock_cache_settings) - - def test_no_plugin(self) -> None: - """Test profile processing when no plugin is available.""" - mock_conan_api = Mock() - mock_profile = Mock() - mock_cache_settings = Mock() - - mock_conan_api.profiles._load_profile_plugin.return_value = None - profiles = [mock_profile] - - _profile_post_process(profiles, mock_conan_api, mock_cache_settings) - - mock_profile.process_settings.assert_called_once_with(mock_cache_settings) - - def test_plugin_failure(self, caplog: pytest.LogCaptureFixture) -> None: - """Test profile processing when plugin fails.""" - mock_conan_api = Mock() - mock_profile = Mock() - mock_cache_settings = Mock() - mock_plugin = Mock() - - mock_conan_api.profiles._load_profile_plugin.return_value = mock_plugin - mock_plugin.side_effect = Exception('Plugin failed') - profiles = [mock_profile] - - with caplog.at_level(logging.WARNING): - _profile_post_process(profiles, mock_conan_api, mock_cache_settings) - - assert 'Profile plugin failed for profile' in caplog.text - mock_profile.process_settings.assert_called_once_with(mock_cache_settings) - - def test_settings_failure(self, caplog: pytest.LogCaptureFixture) -> None: - """Test profile processing when settings processing fails.""" - mock_conan_api = Mock() - mock_profile = Mock() - mock_cache_settings = Mock() - - mock_conan_api.profiles._load_profile_plugin.return_value = None - mock_profile.process_settings.side_effect = Exception('Settings failed') - profiles = [mock_profile] - - with caplog.at_level(logging.DEBUG): - _profile_post_process(profiles, mock_conan_api, mock_cache_settings) - - assert 'Settings processing failed for profile' in caplog.text - - class TestResolveProfiles: """Test profile resolution functionality.""" - def test_by_name(self) -> None: - """Test resolving profiles by name.""" - mock_conan_api = Mock() - mock_host_profile = Mock() - mock_build_profile = Mock() - mock_conan_api.profiles.get_profile.side_effect = [mock_host_profile, mock_build_profile] - - host_result, build_result = _resolve_profiles( - 'host-profile', 'build-profile', mock_conan_api, cmake_program=None - ) - - assert host_result == mock_host_profile - assert build_result == mock_build_profile - assert mock_conan_api.profiles.get_profile.call_count == EXPECTED_PROFILE_CALL_COUNT - mock_conan_api.profiles.get_profile.assert_any_call(['host-profile']) - mock_conan_api.profiles.get_profile.assert_any_call(['build-profile']) - - def test_by_name_failure(self) -> None: - """Test resolving profiles by name when host profile fails.""" - mock_conan_api = Mock() - mock_conan_api.profiles.get_profile.side_effect = Exception('Profile not found') - - with pytest.raises(ProviderConfigurationError, match='Failed to load host profile'): - _resolve_profiles('missing-profile', 'other-profile', mock_conan_api, cmake_program=None) - - def test_auto_detect(self) -> None: - """Test auto-detecting profiles.""" - mock_conan_api = Mock() - mock_host_profile = Mock() - mock_build_profile = Mock() - mock_host_default_path = 'host-default' - mock_build_default_path = 'build-default' - - mock_conan_api.profiles.get_default_host.return_value = mock_host_default_path - mock_conan_api.profiles.get_default_build.return_value = mock_build_default_path - mock_conan_api.profiles.get_profile.side_effect = [mock_host_profile, mock_build_profile] - - host_result, build_result = _resolve_profiles(None, None, mock_conan_api, cmake_program=None) - - assert host_result == mock_host_profile - assert build_result == mock_build_profile - mock_conan_api.profiles.get_default_host.assert_called_once() - mock_conan_api.profiles.get_default_build.assert_called_once() - mock_conan_api.profiles.get_profile.assert_any_call([mock_host_default_path]) - mock_conan_api.profiles.get_profile.assert_any_call([mock_build_default_path]) - - @patch('cppython.plugins.conan.resolution._profile_post_process') - def test_fallback_to_detect(self, mock_post_process: Mock) -> None: - """Test falling back to profile detection when defaults fail.""" - mock_conan_api = Mock() - mock_host_profile = Mock() - mock_build_profile = Mock() - mock_cache_settings = Mock() - - # Mock the default profile methods to fail - mock_conan_api.profiles.get_default_host.side_effect = Exception('No default profile') - mock_conan_api.profiles.get_default_build.side_effect = Exception('No default profile') - mock_conan_api.profiles.get_profile.side_effect = Exception('Profile not found') - - # Mock detect to succeed - mock_conan_api.profiles.detect.side_effect = [mock_host_profile, mock_build_profile] - mock_conan_api.config.settings_yml = mock_cache_settings - - host_result, build_result = _resolve_profiles(None, None, mock_conan_api, cmake_program=None) - - assert host_result == mock_host_profile - assert build_result == mock_build_profile - assert mock_conan_api.profiles.detect.call_count == EXPECTED_PROFILE_CALL_COUNT - assert mock_post_process.call_count == EXPECTED_PROFILE_CALL_COUNT - mock_post_process.assert_any_call([mock_host_profile], mock_conan_api, mock_cache_settings, None) - mock_post_process.assert_any_call([mock_build_profile], mock_conan_api, mock_cache_settings, None) - - @patch('cppython.plugins.conan.resolution._profile_post_process') - def test_default_fallback_to_detect(self, mock_post_process: Mock) -> None: - """Test falling back to profile detection when default profile fails.""" - mock_conan_api = Mock() - mock_host_profile = Mock() - mock_build_profile = Mock() - mock_cache_settings = Mock() - - # Mock the default profile to fail (this simulates the "default" profile not existing) - mock_conan_api.profiles.get_profile.side_effect = Exception('Profile not found') - mock_conan_api.profiles.get_default_host.side_effect = Exception('No default profile') - mock_conan_api.profiles.get_default_build.side_effect = Exception('No default profile') - - # Mock detect to succeed - mock_conan_api.profiles.detect.side_effect = [mock_host_profile, mock_build_profile] - mock_conan_api.config.settings_yml = mock_cache_settings - - host_result, build_result = _resolve_profiles('default', 'default', mock_conan_api, cmake_program=None) - - assert host_result == mock_host_profile - assert build_result == mock_build_profile - assert mock_conan_api.profiles.detect.call_count == EXPECTED_PROFILE_CALL_COUNT - assert mock_post_process.call_count == EXPECTED_PROFILE_CALL_COUNT - mock_post_process.assert_any_call([mock_host_profile], mock_conan_api, mock_cache_settings, None) - mock_post_process.assert_any_call([mock_build_profile], mock_conan_api, mock_cache_settings, None) - class TestResolveConanData: """Test Conan data resolution.""" - - @patch('cppython.plugins.conan.resolution.ConanAPI') - @patch('cppython.plugins.conan.resolution._resolve_profiles') - @patch('cppython.plugins.conan.resolution._detect_cmake_program') - def test_with_profiles( - self, mock_detect_cmake: Mock, mock_resolve_profiles: Mock, mock_conan_api_class: Mock - ) -> None: - """Test resolving ConanData with profile configuration.""" - mock_detect_cmake.return_value = None # No cmake detected for test - mock_conan_api = Mock() - mock_conan_api_class.return_value = mock_conan_api - - mock_host_profile = Mock(spec=Profile) - mock_build_profile = Mock(spec=Profile) - mock_resolve_profiles.return_value = (mock_host_profile, mock_build_profile) - - data = {'host_profile': 'linux-x64', 'build_profile': 'linux-gcc11', 'remotes': ['conancenter']} - core_data = Mock(spec=CorePluginData) - - result = resolve_conan_data(data, core_data) - - assert isinstance(result, ConanData) - assert result.host_profile == mock_host_profile - assert result.build_profile == mock_build_profile - assert result.remotes == ['conancenter'] - - # Verify profile resolution was called correctly - mock_resolve_profiles.assert_called_once_with('linux-x64', 'linux-gcc11', mock_conan_api, None) - - @patch('cppython.plugins.conan.resolution.ConanAPI') - @patch('cppython.plugins.conan.resolution._resolve_profiles') - @patch('cppython.plugins.conan.resolution._detect_cmake_program') - def test_default_profiles( - self, mock_detect_cmake: Mock, mock_resolve_profiles: Mock, mock_conan_api_class: Mock - ) -> None: - """Test resolving ConanData with default profile configuration.""" - mock_detect_cmake.return_value = None # No cmake detected for test - mock_conan_api = Mock() - mock_conan_api_class.return_value = mock_conan_api - - mock_host_profile = Mock(spec=Profile) - mock_build_profile = Mock(spec=Profile) - mock_resolve_profiles.return_value = (mock_host_profile, mock_build_profile) - - data = {} # Empty data should use defaults - core_data = Mock(spec=CorePluginData) - - result = resolve_conan_data(data, core_data) - - assert isinstance(result, ConanData) - assert result.host_profile == mock_host_profile - assert result.build_profile == mock_build_profile - assert result.remotes == ['conancenter'] # Default remote - - # Verify profile resolution was called with default values - mock_resolve_profiles.assert_called_once_with('default', 'default', mock_conan_api, None) - - @patch('cppython.plugins.conan.resolution.ConanAPI') - @patch('cppython.plugins.conan.resolution._resolve_profiles') - @patch('cppython.plugins.conan.resolution._detect_cmake_program') - def test_null_profiles( - self, mock_detect_cmake: Mock, mock_resolve_profiles: Mock, mock_conan_api_class: Mock - ) -> None: - """Test resolving ConanData with null profile configuration.""" - mock_detect_cmake.return_value = None # No cmake detected for test - mock_conan_api = Mock() - mock_conan_api_class.return_value = mock_conan_api - - mock_host_profile = Mock(spec=Profile) - mock_build_profile = Mock(spec=Profile) - mock_resolve_profiles.return_value = (mock_host_profile, mock_build_profile) - - data = {'host_profile': None, 'build_profile': None, 'remotes': [], 'skip_upload': False} - core_data = Mock(spec=CorePluginData) - - result = resolve_conan_data(data, core_data) - - assert isinstance(result, ConanData) - assert result.host_profile == mock_host_profile - assert result.build_profile == mock_build_profile - assert result.remotes == [] - assert result.skip_upload is False - - # Verify profile resolution was called with None values - mock_resolve_profiles.assert_called_once_with(None, None, mock_conan_api, None) - - @patch('cppython.plugins.conan.resolution.ConanAPI') - @patch('cppython.plugins.conan.resolution._profile_post_process') - def test_auto_detected_profile_processing(self, mock_post_process: Mock, mock_conan_api_class: Mock): - """Test that auto-detected profiles get proper post-processing. - - Args: - mock_post_process: Mock for _profile_post_process function - mock_conan_api_class: Mock for ConanAPI class - """ - mock_conan_api = Mock() - mock_conan_api_class.return_value = mock_conan_api - - # Configure the mock to simulate no default profiles - mock_conan_api.profiles.get_default_host.side_effect = Exception('No default profile') - mock_conan_api.profiles.get_default_build.side_effect = Exception('No default profile') - - # Create a profile that simulates auto-detection - mock_profile = Mock() - mock_profile.settings = {'os': 'Windows', 'arch': 'x86_64'} - mock_profile.process_settings = Mock() - mock_profile.conf = Mock() - mock_profile.conf.validate = Mock() - mock_profile.conf.rebase_conf_definition = Mock() - - mock_conan_api.profiles.detect.return_value = mock_profile - mock_conan_api.config.global_conf = Mock() - - # Call the resolution - this should trigger auto-detection and post-processing - host_profile, build_profile = _resolve_profiles(None, None, mock_conan_api, cmake_program=None) - - # Verify that auto-detection was called for both profiles - assert mock_conan_api.profiles.detect.call_count == EXPECTED_PROFILE_CALL_COUNT - - # Verify that post-processing was called for both profiles - assert mock_post_process.call_count == EXPECTED_PROFILE_CALL_COUNT