Skip to content

Commit 91f1d16

Browse files
committed
Profile Schema
1 parent ad02ab3 commit 91f1d16

File tree

6 files changed

+459
-89
lines changed

6 files changed

+459
-89
lines changed

cppython/plugins/conan/plugin.py

Lines changed: 4 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import requests
1313
from conan.api.conan_api import ConanAPI
1414
from conan.api.model import ListPattern
15-
from conan.internal.model.profile import Profile
1615

1716
from cppython.core.plugin_schema.generator import SyncConsumer
1817
from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData, SupportedProviderFeatures
@@ -109,8 +108,8 @@ def _install_dependencies(self, *, update: bool = False) -> None:
109108
all_remotes = conan_api.remotes.list()
110109
logger.debug('Available remotes: %s', [remote.name for remote in all_remotes])
111110

112-
# Get profiles with fallback to auto-detection
113-
profile_host, profile_build = self._get_profiles(conan_api)
111+
# Get profiles from resolved data
112+
profile_host, profile_build = self.data.host_profile, self.data.build_profile
114113

115114
path = str(conanfile_path)
116115
remotes = all_remotes
@@ -249,8 +248,8 @@ def publish(self) -> None:
249248
remotes=all_remotes, # Use all remotes for dependency resolution during export
250249
)
251250

252-
# Step 2: Get profiles with fallback to auto-detection
253-
profile_host, profile_build = self._get_profiles(conan_api)
251+
# Step 2: Get profiles from resolved data
252+
profile_host, profile_build = self.data.host_profile, self.data.build_profile
254253

255254
# Step 3: Build dependency graph for the package - prepare parameters
256255
path = str(conanfile_path)
@@ -305,68 +304,3 @@ def publish(self) -> None:
305304
)
306305
else:
307306
raise ProviderInstallationError('conan', 'No packages found to upload')
308-
309-
def _apply_profile_processing(self, profiles: list[Profile], conan_api: ConanAPI, cache_settings: Any) -> None:
310-
"""Apply profile plugin and settings processing to a list of profiles.
311-
312-
Args:
313-
profiles: List of profiles to process
314-
conan_api: The Conan API instance
315-
cache_settings: The settings configuration
316-
"""
317-
logger = logging.getLogger('cppython.conan')
318-
319-
# Apply profile plugin processing
320-
try:
321-
profile_plugin = conan_api.profiles._load_profile_plugin()
322-
if profile_plugin is not None:
323-
for profile in profiles:
324-
try:
325-
profile_plugin(profile)
326-
except Exception as plugin_error:
327-
logger.warning('Profile plugin failed for profile: %s', str(plugin_error))
328-
except (AttributeError, Exception):
329-
logger.debug('Profile plugin not available or failed to load')
330-
331-
# Process settings to initialize processed_settings
332-
for profile in profiles:
333-
try:
334-
profile.process_settings(cache_settings)
335-
except (AttributeError, Exception) as settings_error:
336-
logger.debug('Settings processing failed for profile: %s', str(settings_error))
337-
338-
def _get_profiles(self, conan_api: ConanAPI) -> tuple[Profile, Profile]:
339-
"""Get Conan profiles with fallback to auto-detection.
340-
341-
Args:
342-
conan_api: The Conan API instance
343-
344-
Returns:
345-
A tuple of (profile_host, profile_build) objects
346-
"""
347-
logger = logging.getLogger('cppython.conan')
348-
349-
try:
350-
# Gather default profile paths, these can raise exceptions if not available
351-
profile_host_path = conan_api.profiles.get_default_host()
352-
profile_build_path = conan_api.profiles.get_default_build()
353-
354-
# Load the actual profile objects, can raise if data is invalid
355-
profile_host = conan_api.profiles.get_profile([profile_host_path])
356-
profile_build = conan_api.profiles.get_profile([profile_build_path])
357-
358-
logger.debug('Using existing default profiles')
359-
return profile_host, profile_build
360-
361-
except Exception as e:
362-
logger.warning('Default profiles not available, using auto-detection. Conan message: %s', str(e))
363-
364-
# Create auto-detected profiles
365-
profiles = [conan_api.profiles.detect(), conan_api.profiles.detect()]
366-
cache_settings = conan_api.config.settings_yml
367-
368-
# Apply profile plugin processing to both profiles
369-
self._apply_profile_processing(profiles, conan_api, cache_settings)
370-
371-
logger.debug('Auto-detected profiles with plugin processing applied')
372-
return profiles[0], profiles[1]

cppython/plugins/conan/resolution.py

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,124 @@
11
"""Provides functionality to resolve Conan-specific data for the CPPython project."""
22

3+
import logging
34
from typing import Any
45

6+
from conan.api.conan_api import ConanAPI
7+
from conan.internal.model.profile import Profile
58
from packaging.requirements import Requirement
69

710
from cppython.core.exception import ConfigException
811
from cppython.core.schema import CorePluginData
912
from cppython.plugins.conan.schema import ConanConfiguration, ConanData, ConanDependency
13+
from cppython.utility.exception import ProviderConfigurationError
14+
15+
16+
def _profile_post_process(profiles: list[Profile], conan_api: ConanAPI, cache_settings: Any) -> None:
17+
"""Apply profile plugin and settings processing to a list of profiles.
18+
19+
Args:
20+
profiles: List of profiles to process
21+
conan_api: The Conan API instance
22+
cache_settings: The settings configuration
23+
"""
24+
logger = logging.getLogger('cppython.conan')
25+
26+
# Apply profile plugin processing
27+
try:
28+
profile_plugin = conan_api.profiles._load_profile_plugin()
29+
if profile_plugin is not None:
30+
for profile in profiles:
31+
try:
32+
profile_plugin(profile)
33+
except Exception as plugin_error:
34+
logger.warning('Profile plugin failed for profile: %s', str(plugin_error))
35+
except (AttributeError, Exception):
36+
logger.debug('Profile plugin not available or failed to load')
37+
38+
# Process settings to initialize processed_settings
39+
for profile in profiles:
40+
try:
41+
profile.process_settings(cache_settings)
42+
except (AttributeError, Exception) as settings_error:
43+
logger.debug('Settings processing failed for profile: %s', str(settings_error))
44+
45+
46+
def _resolve_profiles(
47+
host_profile_name: str | None, build_profile_name: str | None, conan_api: ConanAPI
48+
) -> tuple[Profile, Profile]:
49+
"""Resolve host and build profiles, with fallback to auto-detection.
50+
51+
Args:
52+
host_profile_name: The host profile name to resolve, or None for auto-detection
53+
build_profile_name: The build profile name to resolve, or None for auto-detection
54+
conan_api: The Conan API instance
55+
56+
Returns:
57+
A tuple of (host_profile, build_profile)
58+
"""
59+
logger = logging.getLogger('cppython.conan')
60+
61+
def _resolve_profile(profile_name: str | None, is_host: bool) -> Profile:
62+
"""Helper to resolve a single profile."""
63+
profile_type = 'host' if is_host else 'build'
64+
65+
if profile_name is not None and profile_name != 'default':
66+
# Explicitly specified profile name (not the default) - fail if not found
67+
try:
68+
logger.debug('Loading %s profile: %s', profile_type, profile_name)
69+
profile = conan_api.profiles.get_profile([profile_name])
70+
logger.debug('Successfully loaded %s profile: %s', profile_type, profile_name)
71+
return profile
72+
except Exception as e:
73+
logger.error('Failed to load %s profile %s: %s', profile_type, profile_name, str(e))
74+
raise ProviderConfigurationError(
75+
'conan',
76+
f'Failed to load {profile_type} profile {profile_name}: {str(e)}',
77+
f'{profile_type}_profile',
78+
) from e
79+
elif profile_name == 'default':
80+
# Try to load default profile, but fall back to auto-detection if it fails
81+
try:
82+
logger.debug('Loading %s profile: %s', profile_type, profile_name)
83+
profile = conan_api.profiles.get_profile([profile_name])
84+
logger.debug('Successfully loaded %s profile: %s', profile_type, profile_name)
85+
return profile
86+
except Exception as e:
87+
logger.debug(
88+
'Failed to load %s profile %s: %s. Falling back to auto-detection.',
89+
profile_type,
90+
profile_name,
91+
str(e),
92+
)
93+
# Fall back to auto-detection
94+
95+
try:
96+
if is_host:
97+
default_profile_path = conan_api.profiles.get_default_host()
98+
else:
99+
default_profile_path = conan_api.profiles.get_default_build()
100+
101+
profile = conan_api.profiles.get_profile([default_profile_path])
102+
logger.debug('Using default %s profile', profile_type)
103+
return profile
104+
except Exception as e:
105+
logger.warning('Default %s profile not available, using auto-detection: %s', profile_type, str(e))
106+
107+
# Create auto-detected profile
108+
profile = conan_api.profiles.detect()
109+
cache_settings = conan_api.config.settings_yml
110+
111+
# Apply profile plugin processing
112+
_profile_post_process([profile], conan_api, cache_settings)
113+
114+
logger.debug('Auto-detected %s profile with plugin processing applied', profile_type)
115+
return profile
116+
117+
# Resolve both profiles
118+
host_profile = _resolve_profile(host_profile_name, is_host=True)
119+
build_profile = _resolve_profile(build_profile_name, is_host=False)
120+
121+
return host_profile, build_profile
10122

11123

12124
def resolve_conan_dependency(requirement: Requirement) -> ConanDependency:
@@ -43,4 +155,14 @@ def resolve_conan_data(data: dict[str, Any], core_data: CorePluginData) -> Conan
43155
"""
44156
parsed_data = ConanConfiguration(**data)
45157

46-
return ConanData(remotes=parsed_data.remotes)
158+
# Initialize Conan API for profile resolution
159+
conan_api = ConanAPI()
160+
161+
# Resolve profiles
162+
host_profile, build_profile = _resolve_profiles(parsed_data.host_profile, parsed_data.build_profile, conan_api)
163+
164+
return ConanData(
165+
remotes=parsed_data.remotes,
166+
host_profile=host_profile,
167+
build_profile=build_profile,
168+
)

cppython/plugins/conan/schema.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from typing import Annotated
99

10+
from conan.internal.model.profile import Profile
1011
from pydantic import Field
1112

1213
from cppython.core.schema import CPPythonModel
@@ -31,6 +32,8 @@ class ConanData(CPPythonModel):
3132
"""Resolved conan data"""
3233

3334
remotes: list[str]
35+
host_profile: Profile
36+
build_profile: Profile
3437

3538
@property
3639
def local_only(self) -> bool:
@@ -45,3 +48,17 @@ class ConanConfiguration(CPPythonModel):
4548
list[str],
4649
Field(description='List of remotes to upload to. Empty list means the local conan cache will be used.'),
4750
] = ['conancenter']
51+
host_profile: Annotated[
52+
str | None,
53+
Field(
54+
description='Conan host profile defining the target platform where the built software will run. '
55+
'Used for cross-compilation scenarios.'
56+
),
57+
] = 'default'
58+
build_profile: Annotated[
59+
str | None,
60+
Field(
61+
description='Conan build profile defining the platform where the compilation process executes. '
62+
'Typically matches the development machine.'
63+
),
64+
] = 'default'

tests/unit/plugins/conan/test_install.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,15 @@ def mock_resolve(requirement: Requirement) -> ConanDependency:
136136
# Verify Conan API was attempted
137137
mock_conan_api_constructor.assert_called_once()
138138

139-
def test_install_with_profile_exception(
139+
def test_install_with_default_profiles(
140140
self,
141141
plugin: ConanProvider,
142142
conan_temp_conanfile: Path,
143143
conan_mock_dependencies: list[Requirement],
144144
conan_setup_mocks: dict[str, Mock],
145145
conan_mock_api: Mock,
146146
) -> None:
147-
"""Test install method when profile operations throw exceptions but detect() works
147+
"""Test install method uses pre-resolved profiles from plugin construction
148148
149149
Args:
150150
plugin: The plugin instance
@@ -153,23 +153,21 @@ def test_install_with_profile_exception(
153153
conan_setup_mocks: Dictionary containing all mocks
154154
conan_mock_api: Mock ConanAPI instance
155155
"""
156-
# Configure the API mock to throw exception on profile calls but detect() works
157-
conan_mock_api.profiles.get_default_host.side_effect = Exception('Profile not found')
158-
159156
# Setup dependencies
160157
plugin.core_data.cppython_data.dependencies = conan_mock_dependencies
161158

162-
# Execute - should succeed using fallback detect profiles
159+
# Execute - should use the profiles resolved during plugin construction
163160
plugin.install()
164161

165-
# Verify that the fallback was used
162+
# Verify that the API was used for installation
166163
conan_setup_mocks['conan_api_constructor'].assert_called_once()
167-
conan_mock_api.profiles.get_default_host.assert_called_once()
168-
169-
# Verify detect was called for fallback (should be called twice for fallback)
170-
assert conan_mock_api.profiles.detect.call_count >= EXPECTED_PROFILE_CALLS
171164

172-
# Verify the rest of the process continued
165+
# Verify the rest of the process continued with resolved profiles
173166
conan_mock_api.graph.load_graph_consumer.assert_called_once()
174167
conan_mock_api.install.install_binaries.assert_called_once()
175168
conan_mock_api.install.install_consumer.assert_called_once()
169+
170+
# Verify that the resolved profiles were used in the graph loading
171+
call_args = conan_mock_api.graph.load_graph_consumer.call_args
172+
assert call_args.kwargs['profile_host'] == plugin.data.host_profile
173+
assert call_args.kwargs['profile_build'] == plugin.data.build_profile

tests/unit/plugins/conan/test_publish.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,10 @@ def test_publish_no_packages_found(
179179
with pytest.raises(ProviderInstallationError, match='No packages found to upload'):
180180
plugin.publish()
181181

182-
def test_publish_uses_default_profiles(
182+
def test_publish_with_default_profiles(
183183
self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture
184184
) -> None:
185-
"""Test that publish uses default profiles from API
185+
"""Test that publish uses pre-resolved profiles from plugin construction
186186
187187
Args:
188188
plugin: The plugin instance
@@ -203,10 +203,11 @@ def test_publish_uses_default_profiles(
203203
# Execute publish
204204
plugin.publish()
205205

206-
# Verify profiles were obtained from API
207-
conan_mock_api_publish.profiles.get_default_host.assert_called_once()
208-
conan_mock_api_publish.profiles.get_default_build.assert_called_once()
209-
conan_mock_api_publish.profiles.get_profile.assert_called()
206+
# Verify that the resolved profiles were used in the graph loading
207+
conan_mock_api_publish.graph.load_graph_consumer.assert_called_once()
208+
call_args = conan_mock_api_publish.graph.load_graph_consumer.call_args
209+
assert call_args.kwargs['profile_host'] == plugin.data.host_profile
210+
assert call_args.kwargs['profile_build'] == plugin.data.build_profile
210211

211212
def test_publish_upload_parameters(
212213
self, plugin: ConanProvider, conan_mock_api_publish: Mock, conan_temp_conanfile: None, mocker: MockerFixture

0 commit comments

Comments
 (0)