Remove special casing from no solution error (#5067)

The only pubgrub error that can occur is a `NoSolutionError`, and the
only place it can occur is `unit_propagation`, all other variants if
`PubGrubError` are unreachable. By changing the return type on pubgrub's
side (https://github.com/astral-sh/pubgrub/pull/28), we can remove the
pattern matching and the `unreachable!()` asserts on `PubGrubError`.

Our pubgrub error wrapper used to have a two phased initialization,
first mostly stubs in `solve[_tracked]()` and then adding the actual
context in `resolve()`. When constructing the error in `solve` we
already have all this context, so we can unify this to a regular
constructor and remove the special casing in `resolve()` and `hints()`.
This commit is contained in:
konsti 2024-07-15 17:43:35 +02:00 committed by GitHub
parent 00c055a6bd
commit e34ab96e80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 225 additions and 358 deletions

2
Cargo.lock generated
View file

@ -2700,7 +2700,7 @@ dependencies = [
[[package]] [[package]]
name = "pubgrub" name = "pubgrub"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/astral-sh/pubgrub?rev=b4435e2f3af10dab2336a0345b35dcd622699d06#b4435e2f3af10dab2336a0345b35dcd622699d06" source = "git+https://github.com/astral-sh/pubgrub?rev=3f0ba760951ab0deeac874b98bb18fc90103fcf7#3f0ba760951ab0deeac874b98bb18fc90103fcf7"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"log", "log",

View file

@ -107,7 +107,7 @@ path-slash = { version = "0.2.1" }
pathdiff = { version = "0.2.1" } pathdiff = { version = "0.2.1" }
petgraph = { version = "0.6.4" } petgraph = { version = "0.6.4" }
platform-info = { version = "2.0.2" } platform-info = { version = "2.0.2" }
pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "b4435e2f3af10dab2336a0345b35dcd622699d06" } pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "3f0ba760951ab0deeac874b98bb18fc90103fcf7" }
pyo3 = { version = "0.21.0" } pyo3 = { version = "0.21.0" }
pyo3-log = { version = "0.10.0" } pyo3-log = { version = "0.10.0" }
quote = { version = "1.0.36" } quote = { version = "1.0.36" }

View file

@ -2,10 +2,9 @@ use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Formatter; use std::fmt::Formatter;
use std::sync::Arc; use std::sync::Arc;
use dashmap::DashMap;
use pubgrub::range::Range; use pubgrub::range::Range;
use pubgrub::report::{DefaultStringReporter, DerivationTree, External, Reporter}; use pubgrub::report::{DefaultStringReporter, DerivationTree, External, Reporter};
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::FxHashMap;
use distribution_types::{BuiltDist, IndexLocations, InstalledDist, SourceDist}; use distribution_types::{BuiltDist, IndexLocations, InstalledDist, SourceDist};
use pep440_rs::Version; use pep440_rs::Version;
@ -19,9 +18,7 @@ use crate::pubgrub::{
PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter, PubGrubSpecifierError, PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter, PubGrubSpecifierError,
}; };
use crate::python_requirement::PythonRequirement; use crate::python_requirement::PythonRequirement;
use crate::resolver::{ use crate::resolver::{IncompletePackage, UnavailablePackage, UnavailableReason};
FxOnceMap, IncompletePackage, UnavailablePackage, UnavailableReason, VersionsResponse,
};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ResolveError { pub enum ResolveError {
@ -117,109 +114,90 @@ impl<T> From<tokio::sync::mpsc::error::SendError<T>> for ResolveError {
} }
} }
/// Given a [`DerivationTree`], collapse any [`External::FromDependencyOf`] incompatibilities /// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report.
/// wrap an [`PubGrubPackageInner::Extra`] package.
fn collapse_proxies(
derivation_tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
) {
match derivation_tree {
DerivationTree::External(_) => {}
DerivationTree::Derived(derived) => {
match (
Arc::make_mut(&mut derived.cause1),
Arc::make_mut(&mut derived.cause2),
) {
(
DerivationTree::External(External::FromDependencyOf(package, ..)),
ref mut cause,
) if matches!(
&**package,
PubGrubPackageInner::Extra { .. }
| PubGrubPackageInner::Marker { .. }
| PubGrubPackageInner::Dev { .. }
) =>
{
collapse_proxies(cause);
*derivation_tree = cause.clone();
}
(
ref mut cause,
DerivationTree::External(External::FromDependencyOf(package, ..)),
) if matches!(
&**package,
PubGrubPackageInner::Extra { .. }
| PubGrubPackageInner::Marker { .. }
| PubGrubPackageInner::Dev { .. }
) =>
{
collapse_proxies(cause);
*derivation_tree = cause.clone();
}
_ => {
collapse_proxies(Arc::make_mut(&mut derived.cause1));
collapse_proxies(Arc::make_mut(&mut derived.cause2));
}
}
}
}
}
impl ResolveError {
/// Convert an error from PubGrub to a resolver error.
///
/// [`ForkUrls`] breaks the usual pattern used here since it's part of one the [`SolveState`],
/// not of the [`ResolverState`], so we have to take it from the fork that errored instead of
/// being able to add it later.
pub(crate) fn from_pubgrub_error(
value: pubgrub::error::PubGrubError<UvDependencyProvider>,
fork_urls: ForkUrls,
) -> Self {
match value {
// These are all never type variant that can never match, but never is experimental
pubgrub::error::PubGrubError::ErrorChoosingPackageVersion(_)
| pubgrub::error::PubGrubError::ErrorInShouldCancel(_)
| pubgrub::error::PubGrubError::ErrorRetrievingDependencies { .. } => {
unreachable!()
}
pubgrub::error::PubGrubError::Failure(inner) => Self::Failure(inner),
pubgrub::error::PubGrubError::NoSolution(mut derivation_tree) => {
collapse_proxies(&mut derivation_tree);
Self::NoSolution(NoSolutionError {
derivation_tree,
// The following should be populated before display for the best error messages
available_versions: FxHashMap::default(),
selector: None,
python_requirement: None,
index_locations: None,
unavailable_packages: FxHashMap::default(),
incomplete_packages: FxHashMap::default(),
fork_urls,
})
}
pubgrub::error::PubGrubError::SelfDependency { package, version } => {
Self::SelfDependency {
package: Box::new(package),
version: Box::new(version),
}
}
}
}
}
/// A wrapper around [`pubgrub::error::PubGrubError::NoSolution`] that displays a resolution failure report.
#[derive(Debug)] #[derive(Debug)]
pub struct NoSolutionError { pub struct NoSolutionError {
derivation_tree: DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>, error: pubgrub::error::NoSolutionError<UvDependencyProvider>,
available_versions: FxHashMap<PubGrubPackage, BTreeSet<Version>>, available_versions: FxHashMap<PubGrubPackage, BTreeSet<Version>>,
selector: Option<CandidateSelector>, selector: CandidateSelector,
python_requirement: Option<PythonRequirement>, python_requirement: PythonRequirement,
index_locations: Option<IndexLocations>, index_locations: IndexLocations,
unavailable_packages: FxHashMap<PackageName, UnavailablePackage>, unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, IncompletePackage>>, incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, IncompletePackage>>,
fork_urls: ForkUrls, fork_urls: ForkUrls,
} }
impl NoSolutionError {
pub(crate) fn new(
error: pubgrub::error::NoSolutionError<UvDependencyProvider>,
available_versions: FxHashMap<PubGrubPackage, BTreeSet<Version>>,
selector: CandidateSelector,
python_requirement: PythonRequirement,
index_locations: IndexLocations,
unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, IncompletePackage>>,
fork_urls: ForkUrls,
) -> Self {
Self {
error,
available_versions,
selector,
python_requirement,
index_locations,
unavailable_packages,
incomplete_packages,
fork_urls,
}
}
/// Given a [`DerivationTree`], collapse any [`External::FromDependencyOf`] incompatibilities
/// wrap an [`PubGrubPackageInner::Extra`] package.
pub(crate) fn collapse_proxies(
derivation_tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
) {
match derivation_tree {
DerivationTree::External(_) => {}
DerivationTree::Derived(derived) => {
match (
Arc::make_mut(&mut derived.cause1),
Arc::make_mut(&mut derived.cause2),
) {
(
DerivationTree::External(External::FromDependencyOf(package, ..)),
ref mut cause,
) if matches!(
&**package,
PubGrubPackageInner::Extra { .. }
| PubGrubPackageInner::Marker { .. }
| PubGrubPackageInner::Dev { .. }
) =>
{
Self::collapse_proxies(cause);
*derivation_tree = cause.clone();
}
(
ref mut cause,
DerivationTree::External(External::FromDependencyOf(package, ..)),
) if matches!(
&**package,
PubGrubPackageInner::Extra { .. }
| PubGrubPackageInner::Marker { .. }
| PubGrubPackageInner::Dev { .. }
) =>
{
Self::collapse_proxies(cause);
*derivation_tree = cause.clone();
}
_ => {
Self::collapse_proxies(Arc::make_mut(&mut derived.cause1));
Self::collapse_proxies(Arc::make_mut(&mut derived.cause2));
}
}
}
}
}
}
impl std::error::Error for NoSolutionError {} impl std::error::Error for NoSolutionError {}
impl std::fmt::Display for NoSolutionError { impl std::fmt::Display for NoSolutionError {
@ -227,15 +205,14 @@ impl std::fmt::Display for NoSolutionError {
// Write the derivation report. // Write the derivation report.
let formatter = PubGrubReportFormatter { let formatter = PubGrubReportFormatter {
available_versions: &self.available_versions, available_versions: &self.available_versions,
python_requirement: self.python_requirement.as_ref(), python_requirement: &self.python_requirement,
}; };
let report = let report = DefaultStringReporter::report_with_formatter(&self.error, &formatter);
DefaultStringReporter::report_with_formatter(&self.derivation_tree, &formatter);
write!(f, "{report}")?; write!(f, "{report}")?;
// Include any additional hints. // Include any additional hints.
for hint in formatter.hints( for hint in formatter.hints(
&self.derivation_tree, &self.error,
&self.selector, &self.selector,
&self.index_locations, &self.index_locations,
&self.unavailable_packages, &self.unavailable_packages,
@ -248,113 +225,3 @@ impl std::fmt::Display for NoSolutionError {
Ok(()) Ok(())
} }
} }
impl NoSolutionError {
/// Update the available versions attached to the error using the given package version index.
///
/// Only packages used in the error's derivation tree will be retrieved.
#[must_use]
pub(crate) fn with_available_versions(
mut self,
visited: &FxHashSet<PackageName>,
package_versions: &FxOnceMap<PackageName, Arc<VersionsResponse>>,
) -> Self {
let mut available_versions = FxHashMap::default();
for package in self.derivation_tree.packages() {
match &**package {
PubGrubPackageInner::Root { .. } => {}
PubGrubPackageInner::Python { .. } => {}
PubGrubPackageInner::Marker { .. } => {}
PubGrubPackageInner::Extra { .. } => {}
PubGrubPackageInner::Dev { .. } => {}
PubGrubPackageInner::Package { name, .. } => {
// Avoid including available versions for packages that exist in the derivation
// tree, but were never visited during resolution. We _may_ have metadata for
// these packages, but it's non-deterministic, and omitting them ensures that
// we represent the state of the resolver at the time of failure.
if visited.contains(name) {
if let Some(response) = package_versions.get(name) {
if let VersionsResponse::Found(ref version_maps) = *response {
for version_map in version_maps {
available_versions
.entry(package.clone())
.or_insert_with(BTreeSet::new)
.extend(
version_map.iter().map(|(version, _)| version.clone()),
);
}
}
}
}
}
}
}
self.available_versions = available_versions;
self
}
/// Update the candidate selector attached to the error.
#[must_use]
pub(crate) fn with_selector(mut self, selector: CandidateSelector) -> Self {
self.selector = Some(selector);
self
}
/// Update the index locations attached to the error.
#[must_use]
pub(crate) fn with_index_locations(mut self, index_locations: &IndexLocations) -> Self {
self.index_locations = Some(index_locations.clone());
self
}
/// Update the unavailable packages attached to the error.
#[must_use]
pub(crate) fn with_unavailable_packages(
mut self,
unavailable_packages: &DashMap<PackageName, UnavailablePackage>,
) -> Self {
let mut new = FxHashMap::default();
for package in self.derivation_tree.packages() {
if let PubGrubPackageInner::Package { name, .. } = &**package {
if let Some(reason) = unavailable_packages.get(name) {
new.insert(name.clone(), reason.clone());
}
}
}
self.unavailable_packages = new;
self
}
/// Update the incomplete packages attached to the error.
#[must_use]
pub(crate) fn with_incomplete_packages(
mut self,
incomplete_packages: &DashMap<PackageName, DashMap<Version, IncompletePackage>>,
) -> Self {
let mut new = FxHashMap::default();
for package in self.derivation_tree.packages() {
if let PubGrubPackageInner::Package { name, .. } = &**package {
if let Some(versions) = incomplete_packages.get(name) {
for entry in versions.iter() {
let (version, reason) = entry.pair();
new.entry(name.clone())
.or_insert_with(BTreeMap::default)
.insert(version.clone(), reason.clone());
}
}
}
}
self.incomplete_packages = new;
self
}
/// Update the Python requirements attached to the error.
#[must_use]
pub(crate) fn with_python_requirement(
mut self,
python_requirement: &PythonRequirement,
) -> Self {
self.python_requirement = Some(python_requirement.clone());
self
}
}

View file

@ -30,7 +30,7 @@ pub(crate) struct PubGrubReportFormatter<'a> {
pub(crate) available_versions: &'a FxHashMap<PubGrubPackage, BTreeSet<Version>>, pub(crate) available_versions: &'a FxHashMap<PubGrubPackage, BTreeSet<Version>>,
/// The versions that were available for each package /// The versions that were available for each package
pub(crate) python_requirement: Option<&'a PythonRequirement>, pub(crate) python_requirement: &'a PythonRequirement,
} }
impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason> impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
@ -47,33 +47,31 @@ impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
format!("we are solving dependencies of {package} {version}") format!("we are solving dependencies of {package} {version}")
} }
External::NoVersions(package, set) => { External::NoVersions(package, set) => {
if let Some(python) = self.python_requirement { if matches!(
if matches!( &**package,
&**package, PubGrubPackageInner::Python(PubGrubPython::Target)
PubGrubPackageInner::Python(PubGrubPython::Target) ) {
) { return if let Some(target) = self.python_requirement.target() {
return if let Some(target) = python.target() { format!(
format!( "the requested {package} version ({target}) does not satisfy {}",
"the requested {package} version ({target}) does not satisfy {}",
PackageRange::compatibility(package, set)
)
} else {
format!(
"the requested {package} version does not satisfy {}",
PackageRange::compatibility(package, set)
)
};
}
if matches!(
&**package,
PubGrubPackageInner::Python(PubGrubPython::Installed)
) {
return format!(
"the current {package} version ({}) does not satisfy {}",
python.installed(),
PackageRange::compatibility(package, set) PackageRange::compatibility(package, set)
); )
} } else {
format!(
"the requested {package} version does not satisfy {}",
PackageRange::compatibility(package, set)
)
};
}
if matches!(
&**package,
PubGrubPackageInner::Python(PubGrubPython::Installed)
) {
return format!(
"the current {package} version ({}) does not satisfy {}",
self.python_requirement.installed(),
PackageRange::compatibility(package, set)
);
} }
let set = self.simplify_set(set, package); let set = self.simplify_set(set, package);
@ -404,8 +402,8 @@ impl PubGrubReportFormatter<'_> {
pub(crate) fn hints( pub(crate) fn hints(
&self, &self,
derivation_tree: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>, derivation_tree: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
selector: &Option<CandidateSelector>, selector: &CandidateSelector,
index_locations: &Option<IndexLocations>, index_locations: &IndexLocations,
unavailable_packages: &FxHashMap<PackageName, UnavailablePackage>, unavailable_packages: &FxHashMap<PackageName, UnavailablePackage>,
incomplete_packages: &FxHashMap<PackageName, BTreeMap<Version, IncompletePackage>>, incomplete_packages: &FxHashMap<PackageName, BTreeMap<Version, IncompletePackage>>,
fork_urls: &ForkUrls, fork_urls: &ForkUrls,
@ -417,28 +415,22 @@ impl PubGrubReportFormatter<'_> {
) => { ) => {
if let PubGrubPackageInner::Package { name, .. } = &**package { if let PubGrubPackageInner::Package { name, .. } = &**package {
// Check for no versions due to pre-release options. // Check for no versions due to pre-release options.
if let Some(selector) = selector { if !fork_urls.contains_key(name) {
if !fork_urls.contains_key(name) { self.prerelease_available_hint(package, name, set, selector, &mut hints);
self.prerelease_available_hint(
package, name, set, selector, &mut hints,
);
}
} }
} }
if let PubGrubPackageInner::Package { name, .. } = &**package { if let PubGrubPackageInner::Package { name, .. } = &**package {
// Check for no versions due to no `--find-links` flat index // Check for no versions due to no `--find-links` flat index
if let Some(index_locations) = index_locations { Self::index_hints(
Self::index_hints( package,
package, name,
name, set,
set, index_locations,
index_locations, unavailable_packages,
unavailable_packages, incomplete_packages,
incomplete_packages, &mut hints,
&mut hints, );
);
}
} }
} }
DerivationTree::External(External::FromDependencyOf( DerivationTree::External(External::FromDependencyOf(
@ -452,16 +444,15 @@ impl PubGrubReportFormatter<'_> {
&**dependency, &**dependency,
PubGrubPackageInner::Python(PubGrubPython::Target) PubGrubPackageInner::Python(PubGrubPython::Target)
) { ) {
if let Some(python) = self.python_requirement { if let Some(PythonTarget::RequiresPython(requires_python)) =
if let Some(PythonTarget::RequiresPython(requires_python)) = python.target() self.python_requirement.target()
{ {
hints.insert(PubGrubHint::RequiresPython { hints.insert(PubGrubHint::RequiresPython {
requires_python: requires_python.clone(), requires_python: requires_python.clone(),
package: package.clone(), package: package.clone(),
package_set: self.simplify_set(package_set, package).into_owned(), package_set: self.simplify_set(package_set, package).into_owned(),
package_requires_python: dependency_set.clone(), package_requires_python: dependency_set.clone(),
}); });
}
} }
} }
} }

View file

@ -2,7 +2,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
use std::collections::{BTreeMap, VecDeque}; use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::ops::Bound; use std::ops::Bound;
use std::sync::Arc; use std::sync::Arc;
@ -11,9 +11,8 @@ use std::{iter, thread};
use dashmap::DashMap; use dashmap::DashMap;
use either::Either; use either::Either;
use futures::{FutureExt, StreamExt, TryFutureExt}; use futures::{FutureExt, StreamExt};
use itertools::Itertools; use itertools::Itertools;
use pubgrub::error::PubGrubError;
use pubgrub::range::Range; use pubgrub::range::Range;
use pubgrub::solver::{Incompatibility, State}; use pubgrub::solver::{Incompatibility, State};
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
@ -24,8 +23,8 @@ use tracing::{debug, enabled, instrument, trace, warn, Level};
use distribution_types::{ use distribution_types::{
BuiltDist, CompatibleDist, Dist, DistributionMetadata, IncompatibleDist, IncompatibleSource, BuiltDist, CompatibleDist, Dist, DistributionMetadata, IncompatibleDist, IncompatibleSource,
IncompatibleWheel, InstalledDist, PythonRequirementKind, RemoteSource, ResolvedDist, IncompatibleWheel, IndexLocations, InstalledDist, PythonRequirementKind, RemoteSource,
ResolvedDistRef, SourceDist, VersionOrUrlRef, ResolvedDist, ResolvedDistRef, SourceDist, VersionOrUrlRef,
}; };
pub(crate) use locals::Locals; pub(crate) use locals::Locals;
use pep440_rs::{Version, MIN_VERSION}; use pep440_rs::{Version, MIN_VERSION};
@ -41,7 +40,7 @@ use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider};
use crate::candidate_selector::{CandidateDist, CandidateSelector}; use crate::candidate_selector::{CandidateDist, CandidateSelector};
use crate::dependency_provider::UvDependencyProvider; use crate::dependency_provider::UvDependencyProvider;
use crate::error::ResolveError; use crate::error::{NoSolutionError, ResolveError};
use crate::fork_urls::ForkUrls; use crate::fork_urls::ForkUrls;
use crate::manifest::Manifest; use crate::manifest::Manifest;
use crate::marker::{normalize, requires_python_marker}; use crate::marker::{normalize, requires_python_marker};
@ -245,51 +244,26 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
let (request_sink, request_stream) = mpsc::channel(300); let (request_sink, request_stream) = mpsc::channel(300);
// Run the fetcher. // Run the fetcher.
let requests_fut = state let requests_fut = state.clone().fetch(provider.clone(), request_stream).fuse();
.clone()
.fetch(provider.clone(), request_stream)
.map_err(|err| (err, FxHashSet::default()))
.fuse();
// Spawn the PubGrub solver on a dedicated thread. // Spawn the PubGrub solver on a dedicated thread.
let solver = state.clone(); let solver = state.clone();
let index_locations = provider.index_locations().clone();
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
thread::Builder::new() thread::Builder::new()
.name("uv-resolver".into()) .name("uv-resolver".into())
.spawn(move || { .spawn(move || {
let result = solver.solve(request_sink); let result = solver.solve(index_locations, request_sink);
tx.send(result).unwrap(); tx.send(result).unwrap();
}) })
.unwrap(); .unwrap();
let resolve_fut = async move { let resolve_fut = async move { rx.await.map_err(|_| ResolveError::ChannelClosed) };
rx.await
.map_err(|_| (ResolveError::ChannelClosed, FxHashSet::default()))
.and_then(|result| result)
};
// Wait for both to complete. // Wait for both to complete.
match tokio::try_join!(requests_fut, resolve_fut) { let ((), resolution) = tokio::try_join!(requests_fut, resolve_fut)?;
Ok(((), resolution)) => { state.on_complete();
state.on_complete(); resolution
Ok(resolution)
}
Err((err, visited)) => {
// Add version information to improve unsat error messages.
Err(if let ResolveError::NoSolution(err) = err {
ResolveError::NoSolution(
err.with_available_versions(&visited, state.index.packages())
.with_selector(state.selector.clone())
.with_python_requirement(&state.python_requirement)
.with_index_locations(provider.index_locations())
.with_unavailable_packages(&state.unavailable_packages)
.with_incomplete_packages(&state.incomplete_packages),
)
} else {
err
})
}
}
} }
} }
@ -297,19 +271,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
#[instrument(skip_all)] #[instrument(skip_all)]
fn solve( fn solve(
self: Arc<Self>, self: Arc<Self>,
request_sink: Sender<Request>, // No solution error context.
) -> Result<ResolutionGraph, (ResolveError, FxHashSet<PackageName>)> { index_locations: IndexLocations,
let mut visited = FxHashSet::default();
self.solve_tracked(&mut visited, request_sink)
.map_err(|err| (err, visited))
}
/// Run the PubGrub solver, updating the `visited` set for each package visited during
/// resolution.
#[instrument(skip_all)]
fn solve_tracked(
self: Arc<Self>,
visited: &mut FxHashSet<PackageName>,
request_sink: Sender<Request>, request_sink: Sender<Request>,
) -> Result<ResolutionGraph, ResolveError> { ) -> Result<ResolutionGraph, ResolveError> {
debug!( debug!(
@ -320,6 +283,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
debug!("Solving with target Python version: {}", target); debug!("Solving with target Python version: {}", target);
} }
let mut visited = FxHashSet::default();
let root = PubGrubPackage::from(PubGrubPackageInner::Root(self.project.clone())); let root = PubGrubPackage::from(PubGrubPackageInner::Root(self.project.clone()));
let mut prefetcher = BatchPrefetcher::default(); let mut prefetcher = BatchPrefetcher::default();
let state = ForkState { let state = ForkState {
@ -351,12 +316,14 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
let start = Instant::now(); let start = Instant::now();
loop { loop {
// Run unit propagation. // Run unit propagation.
state if let Err(err) = state.pubgrub.unit_propagation(state.next.clone()) {
.pubgrub return Err(self.convert_no_solution_err(
.unit_propagation(state.next.clone()) err,
.map_err(|err| { state.fork_urls.clone(),
ResolveError::from_pubgrub_error(err, state.fork_urls.clone()) &visited,
})?; &index_locations,
));
}
// Pre-visit all candidate packages, to allow metadata to be fetched in parallel. If // Pre-visit all candidate packages, to allow metadata to be fetched in parallel. If
// the dependency mode is direct, we only need to visit the root package. // the dependency mode is direct, we only need to visit the root package.
@ -420,14 +387,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
.pubgrub .pubgrub
.partial_solution .partial_solution
.term_intersection_for_package(&state.next) .term_intersection_for_package(&state.next)
.ok_or_else(|| { .expect("a package was chosen but we don't have a term.");
ResolveError::from_pubgrub_error(
PubGrubError::Failure(
"a package was chosen but we don't have a term.".into(),
),
state.fork_urls.clone(),
)
})?;
let decision = self.choose_version( let decision = self.choose_version(
&state.next, &state.next,
term_intersection.unwrap_positive(), term_intersection.unwrap_positive(),
@ -435,7 +395,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&preferences, &preferences,
&state.fork_urls, &state.fork_urls,
&state.python_requirement, &state.python_requirement,
visited, &mut visited,
&request_sink, &request_sink,
)?; )?;
@ -534,7 +494,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&self.urls, &self.urls,
dependencies.clone(), dependencies.clone(),
&self.git, &self.git,
&prefetcher,
)?; )?;
// Emit a request to fetch the metadata for each registry package. // Emit a request to fetch the metadata for each registry package.
for dependency in &dependencies { for dependency in &dependencies {
@ -605,7 +564,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&self.urls, &self.urls,
fork.dependencies.clone(), fork.dependencies.clone(),
&self.git, &self.git,
&prefetcher,
)?; )?;
// Emit a request to fetch the metadata for each registry package. // Emit a request to fetch the metadata for each registry package.
for dependency in &fork.dependencies { for dependency in &fork.dependencies {
@ -1806,6 +1764,75 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
} }
} }
fn convert_no_solution_err(
&self,
mut err: pubgrub::error::NoSolutionError<UvDependencyProvider>,
fork_urls: ForkUrls,
visited: &FxHashSet<PackageName>,
index_locations: &IndexLocations,
) -> ResolveError {
NoSolutionError::collapse_proxies(&mut err);
let mut unavailable_packages = FxHashMap::default();
for package in err.packages() {
if let PubGrubPackageInner::Package { name, .. } = &**package {
if let Some(reason) = self.unavailable_packages.get(name) {
unavailable_packages.insert(name.clone(), reason.clone());
}
}
}
let mut incomplete_packages = FxHashMap::default();
for package in err.packages() {
if let PubGrubPackageInner::Package { name, .. } = &**package {
if let Some(versions) = self.incomplete_packages.get(name) {
for entry in versions.iter() {
let (version, reason) = entry.pair();
incomplete_packages
.entry(name.clone())
.or_insert_with(BTreeMap::default)
.insert(version.clone(), reason.clone());
}
}
}
}
let mut available_versions = FxHashMap::default();
for package in err.packages() {
let PubGrubPackageInner::Package { name, .. } = &**package else {
continue;
};
if !visited.contains(name) {
// Avoid including available versions for packages that exist in the derivation
// tree, but were never visited during resolution. We _may_ have metadata for
// these packages, but it's non-deterministic, and omitting them ensures that
// we represent the self of the resolver at the time of failure.
continue;
}
if let Some(response) = self.index.packages().get(name) {
if let VersionsResponse::Found(ref version_maps) = *response {
for version_map in version_maps {
available_versions
.entry(package.clone())
.or_insert_with(BTreeSet::new)
.extend(version_map.iter().map(|(version, _)| version.clone()));
}
}
}
}
ResolveError::NoSolution(NoSolutionError::new(
err,
available_versions,
self.selector.clone(),
self.python_requirement.clone(),
index_locations.clone(),
unavailable_packages,
incomplete_packages,
fork_urls,
))
}
fn on_progress(&self, package: &PubGrubPackage, version: &Version) { fn on_progress(&self, package: &PubGrubPackage, version: &Version) {
if let Some(reporter) = self.reporter.as_ref() { if let Some(reporter) = self.reporter.as_ref() {
match &**package { match &**package {
@ -1908,25 +1935,7 @@ impl ForkState {
urls: &Urls, urls: &Urls,
dependencies: Vec<PubGrubDependency>, dependencies: Vec<PubGrubDependency>,
git: &GitResolver, git: &GitResolver,
prefetcher: &BatchPrefetcher,
) -> Result<(), ResolveError> { ) -> Result<(), ResolveError> {
// Check for self-dependencies.
if dependencies
.iter()
.any(|dependency| dependency.package == self.next)
{
if enabled!(Level::DEBUG) {
prefetcher.log_tried_versions();
}
return Err(ResolveError::from_pubgrub_error(
PubGrubError::SelfDependency {
package: self.next.clone(),
version: version.clone(),
},
self.fork_urls.clone(),
));
}
for dependency in &dependencies { for dependency in &dependencies {
let PubGrubDependency { let PubGrubDependency {
package, package,