// SPDX-License-Identifier: MPL-2.0
use std::collections::BTreeSet as Set;
use std::error::Error;
use std::fmt::{Debug, Display};
use crate::internal::{Id, State};
use crate::{Map, Package, PubGrubError, VersionSet};
use log::{debug, info};
/// Tracks package versions whose dependency incompatibilities have already been added.
///
/// Before the first backtrack, entries are stored in a compact history because decisions cannot
/// be revisited. The history is promoted to a deduplicating map only once revisits become possible.
struct AddedDependencies
{
before_first_backtrack: Vec<(Id
, V)>,
after_first_backtrack: Option, Set>>,
}
impl AddedDependencies
{
fn new() -> Self {
Self {
before_first_backtrack: Vec::new(),
after_first_backtrack: None,
}
}
/// Records a package version and returns whether its dependency incompatibilities still need
/// to be added.
///
/// `has_backtracked` must remain true after the first backtrack so that later decisions are
/// deduplicated against the complete pre-backtrack history.
fn insert(&mut self, package: Id
, version: V, has_backtracked: bool) -> bool {
if !has_backtracked {
// Without a backtrack, an earlier decision cannot be revisited. Keep a compact history
// so the common case does not allocate a map entry and tree node per package.
self.before_first_backtrack.push((package, version));
return true;
}
self.after_first_backtrack
.get_or_insert_with(|| {
let mut added = Map::, Set>::default();
for (package, version) in std::mem::take(&mut self.before_first_backtrack) {
added.entry(package).or_default().insert(version);
}
added
})
.entry(package)
.or_default()
.insert(version)
}
}
/// Statistics on how often a package conflicted with other packages.
#[derive(Debug, Default, Clone)]
pub struct PackageResolutionStatistics {
// We track these fields separately but currently don't expose them separately to keep the
// stable API slim. Please be encouraged to try different combinations of them and report if
// you find better metrics that should be exposed.
//
// Say we have packages A and B, A having higher priority than B. We first decide A and then B,
// and then find B to conflict with A. We call be B "affected" and A "culprit" since the
// decisions for B is being rejected due to the decision we made for A earlier.
//
// If B is rejected due to its dependencies conflicting with A, we increase
// `dependencies_affected` for B and for `dependencies_culprit` A. If B is rejected in unit
// through an incompatibility with B, we increase `unit_propagation_affected` for B and for
// `unit_propagation_culprit` A.
unit_propagation_affected: u32,
unit_propagation_culprit: u32,
dependencies_affected: u32,
dependencies_culprit: u32,
}
impl PackageResolutionStatistics {
/// The number of conflicts this package was involved in.
///
/// Processing packages with a high conflict count earlier usually speeds up resolution.
///
/// Whenever a package is part of the root cause incompatibility of a conflict, we increase its
/// count by one. Since the structure of the incompatibilities may change, this count too may
/// change in the future.
pub fn conflict_count(&self) -> u32 {
self.unit_propagation_affected
+ self.unit_propagation_culprit
+ self.dependencies_affected
+ self.dependencies_culprit
}
}
/// The resolved dependencies and their versions.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SelectedDependencies(Map);
impl SelectedDependencies {
/// Iterate over the resolved dependencies and their versions.
pub fn iter(&self) -> impl Iterator- {
self.0.iter()
}
/// Get the version of a specific dependencies.
pub fn get(&self, package: &P) -> Option<&V> {
self.0.get(package)
}
}
impl
FromIterator<(P, V)> for SelectedDependencies {
fn from_iter>(iter: I) -> Self {
Self(Map::from_iter(iter))
}
}
impl IntoIterator for SelectedDependencies {
type Item = (P, V);
type IntoIter = as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
/// Finds a set of packages satisfying dependency bounds for a given package + version pair.
///
/// It consists in efficiently finding a set of packages and versions
/// that satisfy all the constraints of a given project dependencies.
/// In addition, when that is not possible,
/// PubGrub tries to provide a very human-readable and clear
/// explanation as to why that failed.
/// Below is an example of explanation present in
/// the introductory blog post about PubGrub
/// (Although this crate is not yet capable of building formatting quite this nice.)
///
/// ```txt
/// Because dropdown >=2.0.0 depends on icons >=2.0.0 and
/// root depends on icons <2.0.0, dropdown >=2.0.0 is forbidden.
///
/// And because menu >=1.1.0 depends on dropdown >=2.0.0,
/// menu >=1.1.0 is forbidden.
///
/// And because menu <1.1.0 depends on dropdown >=1.0.0 <2.0.0
/// which depends on intl <4.0.0, every version of menu
/// requires intl <4.0.0.
///
/// So, because root depends on both menu >=1.0.0 and intl >=5.0.0,
/// version solving failed.
/// ```
///
/// Is generic over an implementation of [DependencyProvider] which represents where the dependency constraints come from.
/// The associated types on the DependencyProvider allow flexibility for the representation of
/// package names, version requirements, version numbers, and other things.
/// See its documentation for more details.
/// For simple cases [OfflineDependencyProvider](crate::OfflineDependencyProvider) may be sufficient.
///
/// ## API
///
/// ```
/// # use std::convert::Infallible;
/// # use pubgrub::{resolve, OfflineDependencyProvider, PubGrubError, Ranges};
/// #
/// # type NumVS = Ranges;
/// #
/// # fn try_main() -> Result<(), PubGrubError>> {
/// # let dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new();
/// # let package = "root";
/// # let version = 1u32;
/// let solution = resolve(&dependency_provider, package, version)?;
/// # Ok(())
/// # }
/// # fn main() {
/// # assert!(matches!(try_main(), Err(PubGrubError::NoSolution(_))));
/// # }
/// ```
///
/// The call to [resolve] for a given package at a given version
/// will compute the set of packages and versions needed
/// to satisfy the dependencies of that package and version pair.
/// If there is no solution, the reason will be provided as clear as possible.
#[cold]
pub fn resolve(
dependency_provider: &DP,
package: DP::P,
version: impl Into,
) -> Result, PubGrubError> {
let mut state: State = State::init(package.clone(), version.into());
let mut conflict_tracker: Map, PackageResolutionStatistics> = Map::default();
let mut added_dependencies = AddedDependencies::new();
let mut next = state.root_package;
loop {
dependency_provider
.should_cancel()
.map_err(|err| PubGrubError::ErrorInShouldCancel(err))?;
info!(
"unit_propagation: {:?} = '{}'",
next, state.package_store[next]
);
let satisfier_causes = state.unit_propagation(next)?;
for (affected, incompat) in satisfier_causes {
conflict_tracker
.entry(affected)
.or_default()
.unit_propagation_affected += 1;
for conflict_package in state.conflict_packages(&incompat) {
if conflict_package == affected {
continue;
}
conflict_tracker
.entry(conflict_package)
.or_default()
.unit_propagation_culprit += 1;
}
}
debug!(
"Partial solution after unit propagation: {}",
state.partial_solution.display(&state.package_store)
);
let Some((highest_priority_pkg, term_intersection)) =
state.partial_solution.pick_highest_priority_pkg(|p, r| {
dependency_provider.prioritize(
&state.package_store[p],
r,
conflict_tracker.entry(p).or_default(),
)
})
else {
return Ok(SelectedDependencies(
state
.partial_solution
.extract_solution()
.map(|(p, v)| (state.package_store[p].clone(), v))
.collect(),
));
};
next = highest_priority_pkg;
let decision = dependency_provider
.choose_version(&state.package_store[next], term_intersection)
.map_err(|err| PubGrubError::ErrorChoosingVersion {
package: state.package_store[next].clone(),
source: err,
})?;
info!(
"DP chose: {:?} = '{}' @ {:?}",
next, state.package_store[next], decision
);
// Pick the next compatible version.
let v = match decision {
None => {
let versions = term_intersection.clone();
state.add_no_versions(next, versions);
continue;
}
Some(x) => x,
};
if !term_intersection.contains(&v) {
panic!(
"`choose_version` picked an incompatible version for package {}, {} is not in {}",
state.package_store[next], v, term_intersection
);
}
let is_new_dependency =
added_dependencies.insert(next, v.clone(), state.partial_solution.has_backtracked());
if is_new_dependency {
// Retrieve that package dependencies.
let p = next;
let dependencies = dependency_provider
.get_dependencies(&state.package_store[p], &v)
.map_err(|err| PubGrubError::ErrorRetrievingDependencies {
package: state.package_store[p].clone(),
version: v.clone(),
source: err,
})?;
let dependencies = match dependencies {
Dependencies::Unavailable(reason) => {
state.add_unavailable(p, DP::VS::singleton(v), reason);
continue;
}
Dependencies::Available(x) => x,
};
// Add that package and version if the dependencies are not problematic.
let versions = ::singleton(v.clone());
if let Some(conflict) =
state.add_package_version_dependencies(p, v, versions, dependencies)
{
conflict_tracker.entry(p).or_default().dependencies_affected += 1;
for incompat_package in state.conflict_packages(&conflict) {
if incompat_package == p {
continue;
}
conflict_tracker
.entry(incompat_package)
.or_default()
.dependencies_culprit += 1;
}
}
} else {
// `dep_incompats` are already in `incompatibilities` so we know there are not satisfied
// terms and can add the decision directly.
info!(
"add_decision (not first time): {:?} = '{}' @ {}",
next, state.package_store[next], v
);
state.partial_solution.add_decision(next, v);
}
}
}
/// The dependencies of a package with their version ranges.
///
/// There is a difference in semantics between an empty [DependencyConstraints] and
/// [Dependencies::Unavailable]:
/// The former means the package has no dependency and it is a known fact,
/// while the latter means they could not be fetched by the [DependencyProvider].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DependencyConstraints(Vec<(P, VS)>);
/// Backwards compatibility: Serialize as map.
#[cfg(feature = "serde")]
impl serde::Serialize
for DependencyConstraints
{
fn serialize(&self, serializer: S) -> Result
where
S: serde::Serializer,
{
Map::from_iter(self.0.iter().map(|(p, v)| (p, v))).serialize(serializer)
}
}
/// Backwards compatibility: Deserialize as map.
#[cfg(feature = "serde")]
impl<'de, P: Package + serde::Deserialize<'de>, VS: serde::Deserialize<'de>> serde::Deserialize<'de>
for DependencyConstraints
{
fn deserialize(deserializer: D) -> Result
where
D: serde::Deserializer<'de>,
{
Ok(Self::from_iter(Map::deserialize(deserializer)?))
}
}
impl DependencyConstraints
{
/// Iterate over each dependency in order.
pub fn iter(&self) -> impl Iterator- {
self.0.iter()
}
}
impl
Default for DependencyConstraints
{
fn default() -> Self {
Self(Vec::new())
}
}
impl
FromIterator<(P, VS)> for DependencyConstraints
{
fn from_iter>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl IntoIterator for DependencyConstraints
{
type Item = (P, VS);
type IntoIter = as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
/// An enum used by [DependencyProvider] that holds information about package dependencies.
/// For each [Package] there is a set of versions allowed as a dependency.
#[derive(Clone)]
pub enum Dependencies {
/// Package dependencies are unavailable with the reason why they are missing.
Unavailable(M),
/// Container for all available package versions.
Available(DependencyConstraints),
}
/// Trait that allows the algorithm to retrieve available packages and their dependencies.
/// An implementor needs to be supplied to the [resolve] function.
pub trait DependencyProvider {
/// How this provider stores the name of the packages.
type P: Package;
/// How this provider stores the versions of the packages.
///
/// A common choice is [`SemanticVersion`][crate::version::SemanticVersion].
type V: Debug + Display + Clone + Ord;
/// How this provider stores the version requirements for the packages.
/// The requirements must be able to process the same kind of version as this dependency provider.
///
/// A common choice is [`Ranges`][version_ranges::Ranges].
type VS: VersionSet;
/// The type returned from `prioritize`. The resolver does not care what type this is
/// as long as it can pick a largest one and clone it.
///
/// [`Reverse`](std::cmp::Reverse) can be useful if you want to pick the package with
/// the fewest versions that match the outstanding constraint.
type Priority: Ord + Clone;
/// Type for custom incompatibilities.
///
/// There are reasons in user code outside pubgrub that can cause packages or versions
/// to be unavailable. Examples:
/// * The version would require building the package, but builds are disabled.
/// * The package is not available in the cache, but internet access has been disabled.
/// * The package uses a legacy format not supported anymore.
///
/// The intended use is to track them in an enum and assign them to this type. You can also
/// assign [`String`] as placeholder.
type M: Eq + Clone + Debug + Display;
/// The kind of error returned from these methods.
///
/// Returning this signals that resolution should fail with this error.
type Err: Error + 'static;
/// Determine the order in which versions are chosen for packages.
///
/// Decisions are always made for the highest priority package first. The order of decisions
/// determines which solution is chosen and can drastically change the performances of the
/// solver. If there is a conflict between two package versions, decisions will be backtracked
/// until the lower priority package version is discarded preserving the higher priority
/// package. Usually, you want to decide more certain packages (e.g. those with a single version
/// constraint) and packages with more conflicts first.
///
/// The `package_conflicts_counts` argument provides access to some other heuristics that
/// are production users have found useful. Although the exact meaning/efficacy of those
/// arguments may change.
///
/// The function is called once for each new package and then cached until we detect a
/// (potential) change to `range`, otherwise it is cached, assuming that the priority only
/// depends on the arguments to this function.
///
/// If two packages have the same priority, PubGrub will bias toward a breadth first search.
fn prioritize(
&self,
package: &Self::P,
range: &Self::VS,
// TODO(konsti): Are we always refreshing the priorities when `PackageResolutionStatistics`
// changed for a package?
package_conflicts_counts: &PackageResolutionStatistics,
) -> Self::Priority;
/// Once the resolver has found the highest `Priority` package from all potential valid
/// packages, it needs to know what version of that package to use. The most common pattern
/// is to select the largest version that the range contains.
fn choose_version(
&self,
package: &Self::P,
range: &Self::VS,
) -> Result, Self::Err>;
/// Retrieves the package dependencies.
/// Return [Dependencies::Unavailable] if its dependencies are unavailable.
#[allow(clippy::type_complexity)]
fn get_dependencies(
&self,
package: &Self::P,
version: &Self::V,
) -> Result, Self::Err>;
/// This is called fairly regularly during the resolution,
/// if it returns an Err then resolution will be terminated.
/// This is helpful if you want to add some form of early termination like a timeout,
/// or you want to add some form of user feedback if things are taking a while.
/// If not provided the resolver will run as long as needed.
fn should_cancel(&self) -> Result<(), Self::Err> {
Ok(())
}
}