pubgrub/
solver.rs

1// SPDX-License-Identifier: MPL-2.0
2
3use std::collections::BTreeSet as Set;
4use std::error::Error;
5use std::fmt::{Debug, Display};
6
7use log::{debug, info};
8
9use crate::internal::{Id, Incompatibility, State};
10use crate::{
11    DependencyConstraints, Map, Package, PubGrubError, SelectedDependencies, Term, VersionSet,
12};
13
14/// Statistics on how often a package conflicted with other packages.
15#[derive(Debug, Default, Clone)]
16pub struct PackageResolutionStatistics {
17    // We track these fields separately but currently don't expose them separately to keep the
18    // stable API slim. Please be encouraged to try different combinations of them and report if
19    // you find better metrics that should be exposed.
20    //
21    // Say we have packages A and B, A having higher priority than B. We first decide A and then B,
22    // and then find B to conflict with A. We call be B "affected" and A "culprit" since the
23    // decisions for B is being rejected due to the decision we made for A earlier.
24    //
25    // If B is rejected due to its dependencies conflicting with A, we increase
26    // `dependencies_affected` for B and for `dependencies_culprit` A. If B is rejected in unit
27    // through an incompatibility with B, we increase `unit_propagation_affected` for B and for
28    // `unit_propagation_culprit` A.
29    unit_propagation_affected: u32,
30    unit_propagation_culprit: u32,
31    dependencies_affected: u32,
32    dependencies_culprit: u32,
33}
34
35impl PackageResolutionStatistics {
36    /// The number of conflicts this package was involved in.
37    ///
38    /// Processing packages with a high conflict count earlier usually speeds up resolution.
39    ///
40    /// Whenever a package is part of the root cause incompatibility of a conflict, we increase its
41    /// count by one. Since the structure of the incompatibilities may change, this count too may
42    /// change in the future.
43    pub fn conflict_count(&self) -> u32 {
44        self.unit_propagation_affected
45            + self.unit_propagation_culprit
46            + self.dependencies_affected
47            + self.dependencies_culprit
48    }
49}
50
51/// Finds a set of packages satisfying dependency bounds for a given package + version pair.
52///
53/// It consists in efficiently finding a set of packages and versions
54/// that satisfy all the constraints of a given project dependencies.
55/// In addition, when that is not possible,
56/// PubGrub tries to provide a very human-readable and clear
57/// explanation as to why that failed.
58/// Below is an example of explanation present in
59/// the introductory blog post about PubGrub
60/// (Although this crate is not yet capable of building formatting quite this nice.)
61///
62/// ```txt
63/// Because dropdown >=2.0.0 depends on icons >=2.0.0 and
64///   root depends on icons <2.0.0, dropdown >=2.0.0 is forbidden.
65///
66/// And because menu >=1.1.0 depends on dropdown >=2.0.0,
67///   menu >=1.1.0 is forbidden.
68///
69/// And because menu <1.1.0 depends on dropdown >=1.0.0 <2.0.0
70///   which depends on intl <4.0.0, every version of menu
71///   requires intl <4.0.0.
72///
73/// So, because root depends on both menu >=1.0.0 and intl >=5.0.0,
74///   version solving failed.
75/// ```
76///
77/// Is generic over an implementation of [DependencyProvider] which represents where the dependency constraints come from.
78/// The associated types on the DependencyProvider allow flexibility for the representation of
79/// package names, version requirements, version numbers, and other things.
80/// See its documentation for more details.
81/// For simple cases [OfflineDependencyProvider](crate::OfflineDependencyProvider) may be sufficient.
82///
83/// ## API
84///
85/// ```
86/// # use std::convert::Infallible;
87/// # use pubgrub::{resolve, OfflineDependencyProvider, PubGrubError, Ranges};
88/// #
89/// # type NumVS = Ranges<u32>;
90/// #
91/// # fn try_main() -> Result<(), PubGrubError<OfflineDependencyProvider<&'static str, NumVS>>> {
92/// #     let dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new();
93/// #     let package = "root";
94/// #     let version = 1u32;
95/// let solution = resolve(&dependency_provider, package, version)?;
96/// #     Ok(())
97/// # }
98/// # fn main() {
99/// #     assert!(matches!(try_main(), Err(PubGrubError::NoSolution(_))));
100/// # }
101/// ```
102///
103/// The call to [resolve] for a given package at a given version
104/// will compute the set of packages and versions needed
105/// to satisfy the dependencies of that package and version pair.
106/// If there is no solution, the reason will be provided as clear as possible.
107#[cold]
108pub fn resolve<DP: DependencyProvider>(
109    dependency_provider: &DP,
110    package: DP::P,
111    version: impl Into<DP::V>,
112) -> Result<SelectedDependencies<DP>, PubGrubError<DP>> {
113    let mut state: State<DP> = State::init(package.clone(), version.into());
114    let mut conflict_tracker: Map<Id<DP::P>, PackageResolutionStatistics> = Map::default();
115    let mut added_dependencies: Map<Id<DP::P>, Set<DP::V>> = Map::default();
116    let mut next = state.root_package;
117    loop {
118        dependency_provider
119            .should_cancel()
120            .map_err(|err| PubGrubError::ErrorInShouldCancel(err))?;
121
122        info!(
123            "unit_propagation: {:?} = '{}'",
124            &next, state.package_store[next]
125        );
126        let satisfier_causes = state.unit_propagation(next)?;
127        for (affected, incompat) in satisfier_causes {
128            conflict_tracker
129                .entry(affected)
130                .or_default()
131                .unit_propagation_affected += 1;
132            for (conflict_package, _) in state.incompatibility_store[incompat].iter() {
133                if conflict_package == affected {
134                    continue;
135                }
136                conflict_tracker
137                    .entry(conflict_package)
138                    .or_default()
139                    .unit_propagation_culprit += 1;
140            }
141        }
142
143        debug!(
144            "Partial solution after unit propagation: {}",
145            state.partial_solution.display(&state.package_store)
146        );
147
148        let Some((highest_priority_pkg, term_intersection)) =
149            state.partial_solution.pick_highest_priority_pkg(|p, r| {
150                dependency_provider.prioritize(
151                    &state.package_store[p],
152                    r,
153                    conflict_tracker.entry(p).or_default(),
154                )
155            })
156        else {
157            return Ok(state
158                .partial_solution
159                .extract_solution()
160                .map(|(p, v)| (state.package_store[p].clone(), v))
161                .collect());
162        };
163        next = highest_priority_pkg;
164
165        let decision = dependency_provider
166            .choose_version(&state.package_store[next], term_intersection)
167            .map_err(|err| PubGrubError::ErrorChoosingVersion {
168                package: state.package_store[next].clone(),
169                source: err,
170            })?;
171
172        info!(
173            "DP chose: {:?} = '{}' @ {:?}",
174            &next, state.package_store[next], decision
175        );
176
177        // Pick the next compatible version.
178        let v = match decision {
179            None => {
180                let inc =
181                    Incompatibility::no_versions(next, Term::Positive(term_intersection.clone()));
182                state.add_incompatibility(inc);
183                continue;
184            }
185            Some(x) => x,
186        };
187
188        if !term_intersection.contains(&v) {
189            panic!(
190                "`choose_version` picked an incompatible version for package {}, {} is not in {}",
191                state.package_store[next], v, term_intersection
192            );
193        }
194
195        let is_new_dependency = added_dependencies
196            .entry(next)
197            .or_default()
198            .insert(v.clone());
199
200        if is_new_dependency {
201            // Retrieve that package dependencies.
202            let p = next;
203            let dependencies = dependency_provider
204                .get_dependencies(&state.package_store[p], &v)
205                .map_err(|err| PubGrubError::ErrorRetrievingDependencies {
206                    package: state.package_store[p].clone(),
207                    version: v.clone(),
208                    source: err,
209                })?;
210
211            let dependencies = match dependencies {
212                Dependencies::Unavailable(reason) => {
213                    state.add_incompatibility(Incompatibility::custom_version(
214                        p,
215                        v.clone(),
216                        reason,
217                    ));
218                    continue;
219                }
220                Dependencies::Available(x) => x,
221            };
222
223            // Add that package and version if the dependencies are not problematic.
224            if let Some(conflict) =
225                state.add_package_version_dependencies(p, v.clone(), dependencies)
226            {
227                conflict_tracker.entry(p).or_default().dependencies_affected += 1;
228                for (incompat_package, _) in state.incompatibility_store[conflict].iter() {
229                    if incompat_package == p {
230                        continue;
231                    }
232                    conflict_tracker
233                        .entry(incompat_package)
234                        .or_default()
235                        .dependencies_culprit += 1;
236                }
237            }
238        } else {
239            // `dep_incompats` are already in `incompatibilities` so we know there are not satisfied
240            // terms and can add the decision directly.
241            info!(
242                "add_decision (not first time): {:?} = '{}' @ {}",
243                &next, state.package_store[next], v
244            );
245            state.partial_solution.add_decision(next, v);
246        }
247    }
248}
249
250/// An enum used by [DependencyProvider] that holds information about package dependencies.
251/// For each [Package] there is a set of versions allowed as a dependency.
252#[derive(Clone)]
253pub enum Dependencies<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> {
254    /// Package dependencies are unavailable with the reason why they are missing.
255    Unavailable(M),
256    /// Container for all available package versions.
257    Available(DependencyConstraints<P, VS>),
258}
259
260/// Trait that allows the algorithm to retrieve available packages and their dependencies.
261/// An implementor needs to be supplied to the [resolve] function.
262pub trait DependencyProvider {
263    /// How this provider stores the name of the packages.
264    type P: Package;
265
266    /// How this provider stores the versions of the packages.
267    ///
268    /// A common choice is [`SemanticVersion`][crate::version::SemanticVersion].
269    type V: Debug + Display + Clone + Ord;
270
271    /// How this provider stores the version requirements for the packages.
272    /// The requirements must be able to process the same kind of version as this dependency provider.
273    ///
274    /// A common choice is [`Ranges`][version_ranges::Ranges].
275    type VS: VersionSet<V = Self::V>;
276
277    /// The type returned from `prioritize`. The resolver does not care what type this is
278    /// as long as it can pick a largest one and clone it.
279    ///
280    /// [`Reverse`](std::cmp::Reverse) can be useful if you want to pick the package with
281    /// the fewest versions that match the outstanding constraint.
282    type Priority: Ord + Clone;
283
284    /// Type for custom incompatibilities.
285    ///
286    /// There are reasons in user code outside pubgrub that can cause packages or versions
287    /// to be unavailable. Examples:
288    /// * The version would require building the package, but builds are disabled.
289    /// * The package is not available in the cache, but internet access has been disabled.
290    /// * The package uses a legacy format not supported anymore.
291    ///
292    /// The intended use is to track them in an enum and assign them to this type. You can also
293    /// assign [`String`] as placeholder.
294    type M: Eq + Clone + Debug + Display;
295
296    /// The kind of error returned from these methods.
297    ///
298    /// Returning this signals that resolution should fail with this error.
299    type Err: Error + 'static;
300
301    /// Determine the order in which versions are chosen for packages.
302    ///
303    /// Decisions are always made for the highest priority package first. The order of decisions
304    /// determines which solution is chosen and can drastically change the performances of the
305    /// solver. If there is a conflict between two package versions, decisions will be backtracked
306    /// until the lower priority package version is discarded preserving the higher priority
307    /// package. Usually, you want to decide more certain packages (e.g. those with a single version
308    /// constraint) and packages with more conflicts first.
309    ///
310    /// The `package_conflicts_counts` argument provides access to some other heuristics that
311    /// are production users have found useful. Although the exact meaning/efficacy of those
312    /// arguments may change.
313    ///
314    /// The function is called once for each new package and then cached until we detect a
315    /// (potential) change to `range`, otherwise it is cached, assuming that the priority only
316    /// depends on the arguments to this function.
317    ///
318    /// If two packages have the same priority, PubGrub will bias toward a breadth first search.
319    fn prioritize(
320        &self,
321        package: &Self::P,
322        range: &Self::VS,
323        // TODO(konsti): Are we always refreshing the priorities when `PackageResolutionStatistics`
324        // changed for a package?
325        package_conflicts_counts: &PackageResolutionStatistics,
326    ) -> Self::Priority;
327
328    /// Once the resolver has found the highest `Priority` package from all potential valid
329    /// packages, it needs to know what version of that package to use. The most common pattern
330    /// is to select the largest version that the range contains.
331    fn choose_version(
332        &self,
333        package: &Self::P,
334        range: &Self::VS,
335    ) -> Result<Option<Self::V>, Self::Err>;
336
337    /// Retrieves the package dependencies.
338    /// Return [Dependencies::Unavailable] if its dependencies are unavailable.
339    #[allow(clippy::type_complexity)]
340    fn get_dependencies(
341        &self,
342        package: &Self::P,
343        version: &Self::V,
344    ) -> Result<Dependencies<Self::P, Self::VS, Self::M>, Self::Err>;
345
346    /// This is called fairly regularly during the resolution,
347    /// if it returns an Err then resolution will be terminated.
348    /// This is helpful if you want to add some form of early termination like a timeout,
349    /// or you want to add some form of user feedback if things are taking a while.
350    /// If not provided the resolver will run as long as needed.
351    fn should_cancel(&self) -> Result<(), Self::Err> {
352        Ok(())
353    }
354}