Skip to content

Commit 5020ad0

Browse files
committed
Update Conan Installation Process
1 parent 5d51200 commit 5020ad0

File tree

3 files changed

+259
-91
lines changed

3 files changed

+259
-91
lines changed

cppython/plugins/conan/plugin.py

Lines changed: 96 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"""
77

88
import logging
9-
import subprocess
109
from pathlib import Path
1110
from typing import Any
1211

@@ -72,7 +71,7 @@ def information() -> Information:
7271
return Information()
7372

7473
def _install_dependencies(self, *, update: bool = False) -> None:
75-
"""Install/update dependencies using conan CLI command.
74+
"""Install/update dependencies using Conan API.
7675
7776
Args:
7877
update: If True, check remotes for newer versions/revisions and install those.
@@ -95,50 +94,85 @@ def _install_dependencies(self, *, update: bool = False) -> None:
9594
self.core_data.cppython_data.build_path.mkdir(parents=True, exist_ok=True)
9695
logger.debug('Created build path: %s', self.core_data.cppython_data.build_path)
9796

98-
# Build conan install command
97+
# Initialize Conan API
98+
conan_api = ConanAPI()
99+
100+
# Get project paths
99101
project_root = self.core_data.project_data.project_root
100102
conanfile_path = project_root / 'conanfile.py'
101103

102104
if not conanfile_path.exists():
103105
raise ProviderInstallationError('conan', 'Generated conanfile.py not found')
104106

105-
# Prepare conan install command
106-
cmd = [
107-
'conan',
108-
'install',
109-
str(conanfile_path),
110-
'--output-folder',
111-
str(self.core_data.cppython_data.build_path),
112-
'--build',
113-
'missing',
114-
]
115-
116-
if update:
117-
cmd.extend(['--update'])
118-
119-
logger.debug('Running conan command: %s', ' '.join(cmd))
120-
121-
# Execute conan install command
122-
result = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True, check=False)
123-
124-
# Log output for debugging
125-
if result.stdout:
126-
logger.debug('Conan stdout:\n%s', result.stdout)
127-
if result.stderr:
128-
logger.debug('Conan stderr:\n%s', result.stderr)
129-
130-
# Check for success
131-
if result.returncode != 0:
132-
error_msg = f'Conan install failed with return code {result.returncode}'
133-
if result.stderr:
134-
error_msg += f': {result.stderr}'
135-
raise ProviderInstallationError('conan', error_msg)
136-
137-
logger.debug('Successfully installed dependencies using conan CLI')
138-
139-
except subprocess.SubprocessError as e:
140-
operation = 'update' if update else 'install'
141-
raise ProviderInstallationError('conan', f'Failed to {operation} dependencies: {e}', e) from e
107+
# Get all remotes
108+
all_remotes = conan_api.remotes.list()
109+
logger.debug('Available remotes: %s', [remote.name for remote in all_remotes])
110+
111+
# Get default profiles, handle case when no default profile exists
112+
try:
113+
profile_host_path = conan_api.profiles.get_default_host()
114+
profile_build_path = conan_api.profiles.get_default_build()
115+
116+
# Ensure we have valid profile paths
117+
if profile_host_path is None:
118+
# Create a minimal default profile if none exists
119+
profile_host = conan_api.profiles.get_profile([])
120+
else:
121+
profile_host = conan_api.profiles.get_profile([profile_host_path])
122+
123+
if profile_build_path is None:
124+
# Create a minimal default profile if none exists
125+
profile_build = conan_api.profiles.get_profile([])
126+
else:
127+
profile_build = conan_api.profiles.get_profile([profile_build_path])
128+
129+
except Exception:
130+
# If profile operations fail, create minimal default profiles
131+
profile_host = conan_api.profiles.get_profile([])
132+
profile_build = conan_api.profiles.get_profile([])
133+
134+
logger.debug('Using profiles: host=%s, build=%s', profile_host, profile_build)
135+
136+
# Build dependency graph
137+
deps_graph = conan_api.graph.load_graph_consumer(
138+
path=str(conanfile_path),
139+
name=None,
140+
version=None,
141+
user=None,
142+
channel=None,
143+
profile_host=profile_host,
144+
profile_build=profile_build,
145+
lockfile=None,
146+
remotes=all_remotes,
147+
update=None if not update else True,
148+
check_updates=update,
149+
is_build_require=False,
150+
)
151+
152+
logger.debug('Dependency graph loaded with %d nodes', len(deps_graph.nodes))
153+
154+
# Analyze binaries to determine what needs to be built/downloaded
155+
conan_api.graph.analyze_binaries(
156+
graph=deps_graph,
157+
build_mode=['missing'], # Only build what's missing
158+
remotes=all_remotes,
159+
update=None if not update else True,
160+
lockfile=None,
161+
)
162+
163+
# Install all dependencies
164+
conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes)
165+
166+
# Generate files for the consumer (conandata.yml, conan_toolchain.cmake, etc.)
167+
conan_api.install.install_consumer(
168+
deps_graph=deps_graph,
169+
generators=['CMakeToolchain', 'CMakeDeps'],
170+
source_folder=str(project_root),
171+
output_folder=str(self.core_data.cppython_data.build_path),
172+
)
173+
174+
logger.debug('Successfully installed dependencies using Conan API')
175+
142176
except Exception as e:
143177
operation = 'update' if update else 'install'
144178
error_msg = str(e)
@@ -232,11 +266,28 @@ def publish(self) -> None:
232266
remotes=all_remotes, # Use all remotes for dependency resolution during export
233267
)
234268

235-
# Step 2: Get default profiles
236-
profile_host_path = conan_api.profiles.get_default_host()
237-
profile_build_path = conan_api.profiles.get_default_build()
238-
profile_host = conan_api.profiles.get_profile([profile_host_path])
239-
profile_build = conan_api.profiles.get_profile([profile_build_path])
269+
# Step 2: Get default profiles, handle case when no default profile exists
270+
try:
271+
profile_host_path = conan_api.profiles.get_default_host()
272+
profile_build_path = conan_api.profiles.get_default_build()
273+
274+
# Ensure we have valid profile paths
275+
if profile_host_path is None:
276+
# Create a minimal default profile if none exists
277+
profile_host = conan_api.profiles.get_profile([])
278+
else:
279+
profile_host = conan_api.profiles.get_profile([profile_host_path])
280+
281+
if profile_build_path is None:
282+
# Create a minimal default profile if none exists
283+
profile_build = conan_api.profiles.get_profile([])
284+
else:
285+
profile_build = conan_api.profiles.get_profile([profile_build_path])
286+
287+
except Exception:
288+
# If profile operations fail, create minimal default profiles
289+
profile_host = conan_api.profiles.get_profile([])
290+
profile_build = conan_api.profiles.get_profile([])
240291

241292
# Step 3: Build dependency graph for the package
242293
deps_graph = conan_api.graph.load_graph_consumer(

tests/fixtures/conan.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,24 @@ def fixture_conan_mock_api(mocker: MockerFixture) -> Mock:
2525

2626
# Mock graph module
2727
mock_deps_graph = mocker.Mock()
28+
mock_deps_graph.nodes = []
2829
mock_api.graph.load_graph_consumer = mocker.Mock(return_value=mock_deps_graph)
30+
mock_api.graph.analyze_binaries = mocker.Mock()
2931

3032
# Mock install module
3133
mock_api.install.install_binaries = mocker.Mock()
34+
mock_api.install.install_consumer = mocker.Mock()
3235

3336
# Mock remotes module
3437
mock_remote = mocker.Mock()
3538
mock_remote.name = 'conancenter'
3639
mock_api.remotes.list = mocker.Mock(return_value=[mock_remote])
3740

38-
# Mock profiles module
39-
mock_profile_host = mocker.Mock()
40-
mock_profile_build = mocker.Mock()
41-
mock_api.profiles.get_default_host = mocker.Mock(return_value='/path/to/default/host')
42-
mock_api.profiles.get_default_build = mocker.Mock(return_value='/path/to/default/build')
43-
mock_api.profiles.get_profile = mocker.Mock(
44-
side_effect=lambda paths: mock_profile_host if 'host' in paths[0] else mock_profile_build
45-
)
41+
# Mock profiles module - simulate no default profile by default
42+
mock_profile = mocker.Mock()
43+
mock_api.profiles.get_default_host = mocker.Mock(return_value=None)
44+
mock_api.profiles.get_default_build = mocker.Mock(return_value=None)
45+
mock_api.profiles.get_profile = mocker.Mock(return_value=mock_profile)
4646

4747
return mock_api
4848

0 commit comments

Comments
 (0)