Skip to content

Commit 750d51f

Browse files
committed
Use Subprocess Calls
1 parent 8985b63 commit 750d51f

File tree

5 files changed

+394
-369
lines changed

5 files changed

+394
-369
lines changed

cppython/plugins/conan/plugin.py

Lines changed: 127 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
installation, and synchronization with other tools.
66
"""
77

8+
import subprocess
9+
from logging import getLogger
810
from pathlib import Path
911
from typing import Any
1012

11-
from conan.api.conan_api import ConanAPI
12-
from conan.api.model import ListPattern
13-
1413
from cppython.core.plugin_schema.generator import SyncConsumer
1514
from cppython.core.plugin_schema.provider import Provider, ProviderPluginGroupData, SupportedProviderFeatures
1615
from cppython.core.schema import CorePluginData, Information, SupportedFeatures, SyncData
@@ -19,7 +18,7 @@
1918
from cppython.plugins.conan.builder import Builder
2019
from cppython.plugins.conan.resolution import resolve_conan_data, resolve_conan_dependency
2120
from cppython.plugins.conan.schema import ConanData
22-
from cppython.utility.exception import NotSupportedError, ProviderConfigurationError, ProviderInstallationError
21+
from cppython.utility.exception import NotSupportedError, ProviderInstallationError
2322
from cppython.utility.utility import TypeName
2423

2524

@@ -58,43 +57,32 @@ def information() -> Information:
5857
return Information()
5958

6059
def _install_dependencies(self, *, update: bool = False) -> None:
61-
"""Install/update dependencies using Conan API.
60+
"""Install/update dependencies using Conan CLI.
6261
6362
Args:
6463
update: If True, check remotes for newer versions/revisions and install those.
6564
If False, use cached versions when available.
6665
"""
6766
operation = 'update' if update else 'install'
67+
logger = getLogger('cppython.conan')
6868

6969
try:
7070
# Setup environment and generate conanfile
71-
conan_api, conanfile_path = self._prepare_installation()
71+
conanfile_path = self._prepare_installation()
7272
except Exception as e:
7373
raise ProviderInstallationError('conan', f'Failed to prepare {operation} environment: {e}', e) from e
7474

7575
try:
76-
# Load dependency graph
77-
deps_graph = self._load_dependency_graph(conan_api, conanfile_path, update)
78-
except Exception as e:
79-
raise ProviderInstallationError('conan', f'Failed to load dependency graph: {e}', e) from e
80-
81-
try:
82-
# Install dependencies
83-
self._install_binaries(conan_api, deps_graph, update)
84-
except Exception as e:
85-
raise ProviderInstallationError('conan', f'Failed to install binary dependencies: {e}', e) from e
86-
87-
try:
88-
# Generate consumer files
89-
self._generate_consumer_files(conan_api, deps_graph)
76+
# Install dependencies using conan install command
77+
self._run_conan_install(conanfile_path, update, logger)
9078
except Exception as e:
91-
raise ProviderInstallationError('conan', f'Failed to generate consumer files: {e}', e) from e
79+
raise ProviderInstallationError('conan', f'Failed to install dependencies: {e}', e) from e
9280

93-
def _prepare_installation(self) -> tuple[ConanAPI, Path]:
81+
def _prepare_installation(self) -> Path:
9482
"""Prepare the installation environment and generate conanfile.
9583
9684
Returns:
97-
Tuple of (ConanAPI instance, conanfile path)
85+
Path to conanfile.py
9886
"""
9987
# Resolve dependencies and generate conanfile.py
10088
resolved_dependencies = [resolve_conan_dependency(req) for req in self.core_data.cppython_data.dependencies]
@@ -108,81 +96,63 @@ def _prepare_installation(self) -> tuple[ConanAPI, Path]:
10896
# Ensure build directory exists
10997
self.core_data.cppython_data.build_path.mkdir(parents=True, exist_ok=True)
11098

111-
# Setup paths and API
112-
conan_api = ConanAPI()
99+
# Setup paths
113100
project_root = self.core_data.project_data.project_root
114101
conanfile_path = project_root / 'conanfile.py'
115102

116103
if not conanfile_path.exists():
117104
raise FileNotFoundError('Generated conanfile.py not found')
118105

119-
return conan_api, conanfile_path
106+
return conanfile_path
120107

121-
def _load_dependency_graph(self, conan_api: ConanAPI, conanfile_path: Path, update: bool):
122-
"""Load and build the dependency graph.
108+
def _run_conan_install(self, conanfile_path: Path, update: bool, logger) -> None:
109+
"""Run conan install command.
123110
124111
Args:
125-
conan_api: The Conan API instance
126112
conanfile_path: Path to the conanfile.py
127113
update: Whether to check for updates
128-
129-
Returns:
130-
The loaded dependency graph
114+
logger: Logger instance
131115
"""
132-
all_remotes = conan_api.remotes.list()
133-
profile_host, profile_build = self.data.host_profile, self.data.build_profile
134-
135-
return conan_api.graph.load_graph_consumer(
136-
path=str(conanfile_path),
137-
name=None,
138-
version=None,
139-
user=None,
140-
channel=None,
141-
lockfile=None,
142-
remotes=all_remotes,
143-
update=update or None,
144-
check_updates=update,
145-
is_build_require=False,
146-
profile_host=profile_host,
147-
profile_build=profile_build,
148-
)
116+
# Build conan install command
117+
command = ['conan', 'install', str(conanfile_path)]
149118

150-
def _install_binaries(self, conan_api: ConanAPI, deps_graph, update: bool) -> None:
151-
"""Analyze and install binary dependencies.
119+
# Add build missing flag
120+
command.extend(['--build', 'missing'])
152121

153-
Args:
154-
conan_api: The Conan API instance
155-
deps_graph: The dependency graph
156-
update: Whether to check for updates
157-
"""
158-
all_remotes = conan_api.remotes.list()
159-
160-
# Analyze binaries to determine what needs to be built/downloaded
161-
conan_api.graph.analyze_binaries(
162-
graph=deps_graph,
163-
build_mode=['missing'],
164-
remotes=all_remotes,
165-
update=update or None,
166-
lockfile=None,
167-
)
122+
# Add profiles if specified
123+
if self.data.host_profile and str(self.data.host_profile) != 'default':
124+
command.extend(['--profile:host', str(self.data.host_profile)])
125+
else:
126+
command.extend(['--profile:host', 'default'])
168127

169-
# Install all dependencies
170-
conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes)
128+
if self.data.build_profile and str(self.data.build_profile) != 'default':
129+
command.extend(['--profile:build', str(self.data.build_profile)])
130+
else:
131+
command.extend(['--profile:build', 'default'])
171132

172-
def _generate_consumer_files(self, conan_api: ConanAPI, deps_graph) -> None:
173-
"""Generate consumer files (CMake toolchain, deps, etc.).
133+
# Add update flag if needed
134+
if update:
135+
command.append('--update')
174136

175-
Args:
176-
conan_api: The Conan API instance
177-
deps_graph: The dependency graph
178-
"""
179-
project_root = self.core_data.project_data.project_root
137+
# Add output folder
138+
build_path = self.core_data.cppython_data.build_path
139+
command.extend(['--output-folder', str(build_path)])
180140

181-
conan_api.install.install_consumer(
182-
deps_graph=deps_graph,
183-
generators=None, # Our conanfile.py template defines this
184-
source_folder=str(project_root),
185-
)
141+
# Log the command being executed
142+
logger.info('Executing conan command: %s', ' '.join(command))
143+
144+
try:
145+
subprocess.run(
146+
command,
147+
cwd=str(self.core_data.project_data.project_root),
148+
check=True,
149+
capture_output=True,
150+
text=True,
151+
)
152+
except subprocess.CalledProcessError as e:
153+
error_msg = e.stderr if e.stderr else str(e)
154+
logger.error('Conan install failed: %s', error_msg, exc_info=True)
155+
raise ProviderInstallationError('conan', error_msg, e) from e
186156

187157
def install(self) -> None:
188158
"""Installs the provider"""
@@ -242,101 +212,90 @@ def publish(self) -> None:
242212
"""Publishes the package using conan create workflow."""
243213
project_root = self.core_data.project_data.project_root
244214
conanfile_path = project_root / 'conanfile.py'
215+
logger = getLogger('cppython.conan')
245216

246217
if not conanfile_path.exists():
247218
raise FileNotFoundError(f'conanfile.py not found at {conanfile_path}')
248219

249-
conan_api = ConanAPI()
250-
251-
all_remotes = conan_api.remotes.list()
252-
253-
# Configure remotes for upload
254-
configured_remotes = self._get_configured_remotes(all_remotes)
255-
256-
# Export the recipe to cache
257-
ref, _ = conan_api.export.export(
258-
path=str(conanfile_path),
259-
name=None,
260-
version=None,
261-
user=None,
262-
channel=None,
263-
lockfile=None,
264-
remotes=all_remotes,
265-
)
266-
267-
# Build dependency graph and install
268-
profile_host, profile_build = self.data.host_profile, self.data.build_profile
269-
deps_graph = conan_api.graph.load_graph_consumer(
270-
path=str(conanfile_path),
271-
name=None,
272-
version=None,
273-
user=None,
274-
channel=None,
275-
lockfile=None,
276-
remotes=all_remotes, # Use all remotes for dependency resolution
277-
update=None,
278-
check_updates=False,
279-
is_build_require=False,
280-
profile_host=profile_host,
281-
profile_build=profile_build,
282-
)
283-
284-
# Analyze and build binaries
285-
conan_api.graph.analyze_binaries(
286-
graph=deps_graph,
287-
build_mode=['*'],
288-
remotes=all_remotes, # Use all remotes for dependency resolution
289-
update=None,
290-
lockfile=None,
291-
)
292-
293-
conan_api.install.install_binaries(deps_graph=deps_graph, remotes=all_remotes)
294-
295-
if not self.data.skip_upload:
296-
self._upload_package(conan_api, ref, configured_remotes)
220+
try:
221+
# Build conan create command
222+
command = ['conan', 'create', str(conanfile_path)]
223+
224+
# Add build mode (build everything for publishing)
225+
command.extend(['--build', 'missing'])
226+
227+
# Add profiles
228+
if self.data.host_profile and str(self.data.host_profile) != 'default':
229+
command.extend(['--profile:host', str(self.data.host_profile)])
230+
else:
231+
command.extend(['--profile:host', 'default'])
232+
233+
if self.data.build_profile and str(self.data.build_profile) != 'default':
234+
command.extend(['--profile:build', str(self.data.build_profile)])
235+
else:
236+
command.extend(['--profile:build', 'default'])
237+
238+
# Log the command being executed
239+
logger.info('Executing conan create command: %s', ' '.join(command))
240+
241+
# Run conan create
242+
subprocess.run(
243+
command,
244+
cwd=str(project_root),
245+
check=True,
246+
capture_output=True,
247+
text=True,
248+
)
297249

298-
def _get_configured_remotes(self, all_remotes):
299-
"""Get and validate configured remotes for upload.
250+
# Upload if not skipped
251+
if not self.data.skip_upload:
252+
self._upload_package(logger)
300253

301-
Note: This only affects upload behavior. For dependency resolution,
302-
we always use all available system remotes regardless of this config.
303-
"""
304-
# If skip_upload is True, don't upload anywhere
305-
if self.data.skip_upload:
306-
return []
254+
except subprocess.CalledProcessError as e:
255+
error_msg = e.stderr if e.stderr else str(e)
256+
logger.error('Conan create failed: %s', error_msg, exc_info=True)
257+
raise ProviderInstallationError('conan', error_msg, e) from e
307258

308-
# If no remotes specified, upload to all available remotes
259+
def _upload_package(self, logger) -> None:
260+
"""Upload the package to configured remotes using CLI commands."""
261+
# If no remotes configured, upload to all remotes
309262
if not self.data.remotes:
310-
return all_remotes
263+
# Upload to all available remotes
264+
command = ['conan', 'upload', '*', '--all', '--confirm']
265+
else:
266+
# Upload only to specified remotes
267+
for remote in self.data.remotes:
268+
command = ['conan', 'upload', '*', '--remote', remote, '--all', '--confirm']
269+
270+
# Log the command being executed
271+
logger.info('Executing conan upload command: %s', ' '.join(command))
272+
273+
try:
274+
subprocess.run(
275+
command,
276+
cwd=str(self.core_data.project_data.project_root),
277+
check=True,
278+
capture_output=True,
279+
text=True,
280+
)
281+
except subprocess.CalledProcessError as e:
282+
error_msg = e.stderr if e.stderr else str(e)
283+
logger.error('Conan upload failed for remote %s: %s', remote, error_msg, exc_info=True)
284+
raise ProviderInstallationError('conan', f'Upload to {remote} failed: {error_msg}', e) from e
285+
return
286+
287+
# Log the command for uploading to all remotes
288+
logger.info('Executing conan upload command: %s', ' '.join(command))
311289

312-
# Otherwise, upload only to specified remotes
313-
configured_remotes = [remote for remote in all_remotes if remote.name in self.data.remotes]
314-
315-
if not configured_remotes:
316-
available_remotes = [remote.name for remote in all_remotes]
317-
raise ProviderConfigurationError(
318-
'conan',
319-
f'No configured remotes found. Available: {available_remotes}, Configured: {self.data.remotes}',
320-
'remotes',
290+
try:
291+
subprocess.run(
292+
command,
293+
cwd=str(self.core_data.project_data.project_root),
294+
check=True,
295+
capture_output=True,
296+
text=True,
321297
)
322-
323-
return configured_remotes
324-
325-
def _upload_package(self, conan_api, ref, configured_remotes):
326-
"""Upload the package to configured remotes."""
327-
ref_pattern = ListPattern(f'{ref.name}/*', package_id='*', only_recipe=False)
328-
package_list = conan_api.list.select(ref_pattern)
329-
330-
if not package_list.recipes:
331-
raise ProviderInstallationError('conan', 'No packages found to upload')
332-
333-
remote = configured_remotes[0]
334-
conan_api.upload.upload_full(
335-
package_list=package_list,
336-
remote=remote,
337-
enabled_remotes=configured_remotes,
338-
check_integrity=False,
339-
force=False,
340-
metadata=None,
341-
dry_run=False,
342-
)
298+
except subprocess.CalledProcessError as e:
299+
error_msg = e.stderr if e.stderr else str(e)
300+
logger.error('Conan upload failed: %s', error_msg, exc_info=True)
301+
raise ProviderInstallationError('conan', error_msg, e) from e

0 commit comments

Comments
 (0)