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}