diff --git a/crates/pixi_core/src/lock_file/resolve/build_dispatch.rs b/crates/pixi_core/src/lock_file/resolve/build_dispatch.rs index 1eb4da5605..85c9617bd3 100644 --- a/crates/pixi_core/src/lock_file/resolve/build_dispatch.rs +++ b/crates/pixi_core/src/lock_file/resolve/build_dispatch.rs @@ -236,10 +236,12 @@ pub struct LazyBuildDispatchDependencies { extra_build_requires: OnceCell, /// Package-specific configuration settings package_config_settings: OnceCell, + /// The last initialization error that occurred + last_error: OnceCell, } #[derive(Debug, thiserror::Error, miette::Diagnostic)] -enum LazyBuildDispatchError { +pub enum LazyBuildDispatchError { #[error( "installation of conda environment is required to solve PyPI source dependencies but `--no-install` flag has been set" )] @@ -268,6 +270,11 @@ impl IsBuildBackendError for LazyBuildDispatchError { } impl<'a> LazyBuildDispatch<'a> { + /// Get the last initialization error if available + pub fn last_initialization_error(&self) -> Option<&LazyBuildDispatchError> { + self.lazy_deps.last_error.get() + } + /// Create a new `PixiBuildDispatch` instance. #[allow(clippy::too_many_arguments)] pub fn new( @@ -414,11 +421,14 @@ impl BuildContext for LazyBuildDispatch<'_> { // // Even though initialize does not initialize twice, we check it beforehand // because the initialization takes time - self.get_or_try_init() - .await - .expect("could not initialize build dispatch correctly") - .interpreter() - .await + match self.get_or_try_init().await { + Ok(dispatch) => dispatch.interpreter().await, + Err(e) => { + // Store the error for later retrieval + let _ = self.lazy_deps.last_error.set(e); + panic!("could not initialize build dispatch correctly") + } + } } fn cache(&self) -> &uv_cache::Cache { diff --git a/crates/pixi_core/src/lock_file/resolve/pypi.rs b/crates/pixi_core/src/lock_file/resolve/pypi.rs index bb912332de..c3077f9cd1 100644 --- a/crates/pixi_core/src/lock_file/resolve/pypi.rs +++ b/crates/pixi_core/src/lock_file/resolve/pypi.rs @@ -3,12 +3,15 @@ use std::{ collections::{HashMap, HashSet}, iter::once, ops::Deref, + panic, path::{Path, PathBuf}, rc::Rc, str::FromStr, sync::Arc, }; +use futures::FutureExt; + use chrono::{DateTime, Utc}; use indexmap::{IndexMap, IndexSet}; use indicatif::ProgressBar; @@ -212,6 +215,10 @@ pub enum SolveError { }, #[error("failed to resolve pypi dependencies")] Other(#[from] ResolveError), + #[error("build dispatch initialization failed: {message}")] + BuildDispatchPanic { message: String }, + #[error("unexpected panic during PyPI resolution: {message}")] + GeneralPanic { message: String }, } /// Creates a custom `SolveError` from a `ResolveError`. @@ -626,30 +633,70 @@ pub async fn resolve_pypi( // We need a new in-memory index for the resolver so that it does not conflict // with the build dispatch one. As we have noted in the comment above. let resolver_in_memory_index = InMemoryIndex::default(); - let resolution = Resolver::new_custom_io( - manifest, - options, - &context.hash_strategy, - resolver_env, - &marker_environment, - Some(tags), - &PythonRequirement::from_marker_environment(&marker_environment, requires_python.clone()), - Conflicts::default(), - &resolver_in_memory_index, - context.shared_state.git(), - &context.capabilities, - &index_locations, - provider, - EmptyInstalledPackages, - ) - .into_diagnostic() - .context("failed to resolve pypi dependencies")? - .with_reporter(UvReporter::new_arc( - UvReporterOptions::new().with_existing(pb.clone()), - )) - .resolve() - .await - .map_err(|e| create_solve_error(e, &conda_python_packages))?; + + // Wrap the resolution in panic catching to handle conda prefix initialization failures + let resolution_future = panic::AssertUnwindSafe(async { + let resolver = Resolver::new_custom_io( + manifest, + options, + &context.hash_strategy, + resolver_env, + &marker_environment, + Some(tags), + &PythonRequirement::from_marker_environment( + &marker_environment, + requires_python.clone(), + ), + Conflicts::default(), + &resolver_in_memory_index, + context.shared_state.git(), + &context.capabilities, + &index_locations, + provider, + EmptyInstalledPackages, + ) + .into_diagnostic() + .context("failed to resolve pypi dependencies") + .map_err(|e| SolveError::GeneralPanic { + message: format!("Failed to create resolver: {}", e), + })? + .with_reporter(UvReporter::new_arc( + UvReporterOptions::new().with_existing(pb.clone()), + )); + + resolver + .resolve() + .await + .map_err(|e| create_solve_error(e, &conda_python_packages)) + }); + + // We try to distinguis between build dispatch panics and any other panics that occur + let resolution = match resolution_future.catch_unwind().await { + Ok(result) => result?, + Err(panic_payload) => { + // Try to get the stored initialization error from the lazy build dispatch + if let Some(stored_error) = lazy_build_dispatch.last_initialization_error() { + return Err(SolveError::BuildDispatchPanic { + message: format!("{}", stored_error), + } + .into()); + } else { + // Use the original panic message for general panics + let panic_message = if let Some(s) = panic_payload.downcast_ref::() { + s.clone() + } else if let Some(&s) = panic_payload.downcast_ref::<&str>() { + s.to_string() + } else { + "unknown panic occurred during PyPI resolution".to_string() + }; + + return Err(SolveError::GeneralPanic { + message: panic_message, + } + .into()); + } + } + }; let resolution = Resolution::from(resolution); // Print the overridden package requests