miden_project/dependencies/resolver/provider.rs
1use alloc::{boxed::Box, format, string::String};
2use core::{cmp::Reverse, convert::Infallible};
3
4use pubgrub::{Dependencies, DependencyProvider, SelectedDependencies};
5use smallvec::SmallVec;
6
7use super::{version_set::VersionSetFilter, *};
8use crate::{SemVer, Version};
9
10/// Represents package priorities in the resolver.
11///
12/// The order of the variants here matters, as the earlier a variant appears, the lower its
13/// priority. The resolver will solve for packages with higher priority before those with lower
14/// priority, and so it is desirable to prioritize packages with a higher chance of conflict and/or
15/// more precise version requirements, as it has a large impact on the performance of the resolver,
16/// by eliminating a lot of versions from the tree.
17///
18/// In general, the goal of this prioritization scheme is to guide the resolver so it can make
19/// better selections, and aid in resolving conflicts.
20#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
21pub enum PackagePriority {
22 /// The package has no specific priority, or low priority
23 Weak(Reverse<u8>),
24 /// The package version range is constrained to a single version, so we want to handle these
25 /// packages before other packages, as it will help with version selection for packages with
26 /// lower priority.
27 Singleton(Reverse<u8>),
28 /// The package is constrained to an exact content digest, which is more precise than a single
29 /// version constraint, as there may be multiple instances of a package for a given semantic
30 /// version with differing digests. Thus we prioritize these before the other types above.
31 Digest(Reverse<u8>),
32 /// The package is in the same workspace as the root. We prioritize these before specific
33 /// versions, as it would be highly unintuitive for dependencies on packages in the workspace
34 /// to resolve to versions which are _not_ in the workspace. The purpose of prioritizing this
35 /// higher is that it will ensure that when referencing a workspace dependency via path, which
36 /// must happen _in_ the workspace itself, that we always choose the version at that path first,
37 /// and raise a conflict if other requirements on the same package cannot be unified to that
38 /// version.
39 Workspace,
40 /// The package is the root package, and so we always choose it first
41 Root,
42}
43
44/// As implied by the name, this is the implementation of the dependency/package resolver.
45///
46/// The resolver is constructed for a specific root package, along with a reference to the current
47/// state of the package index. Once constructed, you then ask the resolver for the dependencies
48/// of the given package using [`PackageResolver::resolve`]. It is then up to the caller to take
49/// those package and version selections and do something with them, typically fetching the actual
50/// package content and using it for program execution or assembly of a new package.
51pub struct PackageResolver<'a> {
52 index: &'a PackageIndex,
53 context: ResolverContext,
54}
55
56/// Construction
57impl<'a> PackageResolver<'a> {
58 /// Create a [PackageResolver] for `package` in `workspace`, using `index`.
59 ///
60 /// It is assumed that the caller has already:
61 ///
62 /// 1. Ensured that `index` has been seeded with `package`, along with other members of
63 /// `workspace`, and all versions of all packages known to the system that may be referenced
64 /// as dependencies, directly or transitively.
65 /// 2. Verified that `package` is a member of `workspace`. While resolution can still succeed if
66 /// that isn't the case, it may cause unexpected conflicts or selections to occur due to the
67 /// special prioritization given to workspace members.
68 pub fn for_package_in_workspace(
69 package: &'a crate::Package,
70 workspace: &'a crate::Workspace,
71 index: &'a PackageIndex,
72 ) -> Self {
73 Self {
74 index,
75 context: ResolverContext::Workspace {
76 members: SmallVec::from_iter(
77 workspace.members().iter().map(|p| p.name().into_inner().into()),
78 ),
79 root: package.name().into_inner().into(),
80 root_version: package.version().into_inner().clone(),
81 },
82 }
83 }
84
85 /// Create a [PackageResolver] for `package`, using `index`.
86 ///
87 /// For packages that are members of a workspace, you should prefer
88 /// [`Self::for_package_in_workspace`].
89 ///
90 /// It is assumed that the caller has already:
91 ///
92 /// 1. Ensured that `index` has been seeded with `package`, along with all versions of all
93 /// packages known to the system that may be referenced as dependencies, directly or
94 /// transitively.
95 pub fn for_package(package: &'a crate::Package, index: &'a PackageIndex) -> Self {
96 Self {
97 index,
98 context: ResolverContext::Package {
99 root: package.name().into_inner().into(),
100 version: package.version().into_inner().clone(),
101 },
102 }
103 }
104}
105
106/// Dependency resolution
107impl<'a> PackageResolver<'a> {
108 pub fn resolve(self) -> Result<SelectedDependencies<Self>, DependencyResolutionError> {
109 let (package, version) = match &self.context {
110 ResolverContext::Workspace { root, root_version: version, .. }
111 | ResolverContext::Package { root, version } => (root, version),
112 };
113 self.resolve_for(package.clone(), version.clone())
114 }
115
116 /// Resolve dependencies for `package` with the given `version`.
117 ///
118 /// If successful, this returns a map of [PackageId] to [Version] for all dependencies required
119 /// by `package`, transitively.
120 ///
121 /// If unsuccesssful, a [DependencyResolutionError] is returned that indicates why it failed.
122 pub fn resolve_for(
123 &self,
124 package: impl Into<PackageId>,
125 version: SemVer,
126 ) -> Result<SelectedDependencies<Self>, DependencyResolutionError> {
127 let package = package.into();
128 let version = Version { version, digest: None };
129 pubgrub::resolve(self, package, version).map_err(DependencyResolutionError::from)
130 }
131}
132
133/// Context used by [PackageResolver] to aid in version selection during resolution.
134enum ResolverContext {
135 Workspace {
136 members: SmallVec<[PackageId; 2]>,
137 root: PackageId,
138 root_version: SemVer,
139 },
140 Package {
141 root: PackageId,
142 version: SemVer,
143 },
144}
145
146impl ResolverContext {
147 /// Attempt to prioritize `package` based on the current resolver context.
148 ///
149 /// Returns `None` if no specific priority can be determined based solely on the package name
150 /// and resolver context.
151 pub fn try_prioritize(&self, package: &PackageId) -> Option<PackagePriority> {
152 match self {
153 Self::Workspace { members, root, .. } => {
154 if package == root {
155 Some(PackagePriority::Root)
156 } else if members.contains(package) {
157 Some(PackagePriority::Workspace)
158 } else {
159 None
160 }
161 },
162 Self::Package { root, .. } => {
163 if package == root {
164 Some(PackagePriority::Root)
165 } else {
166 None
167 }
168 },
169 }
170 }
171}
172
173/// This provides the version selection/dependency resolution functionality for [PackageIndex] via
174/// [`pubgrub`](https://pubgrub-rs.github.io/pubgrub/pubgrub/).
175///
176/// We provide a custom implementation, as our versioning scheme must handle a mix of requirements
177/// including both semantic versioning schemes as well as requirements on a specific package digest.
178impl<'a> DependencyProvider for PackageResolver<'a> {
179 /// The type used to identify packages in the provider
180 type P = PackageId;
181 /// The type used to represent package versions
182 type V = Version;
183 /// The type used to represent version requirements.
184 ///
185 /// NOTE: Requirements must be able to process versions of type `V`
186 type VS = VersionSet;
187 /// The error type used to communicate our own version selection issues, e.g.:
188 ///
189 /// * The version would require building the package, but builds are disabled.
190 /// * The package is not available in the cache, but internet access has been disabled.
191 /// * The package uses a legacy format not supported anymore.
192 ///
193 /// Currently we're using `String` here as a placeholder until we determine a more appropriate
194 /// error type.
195 type M = String;
196 /// The type used to represent prioritize package versions during selection.
197 type Priority = PackagePriority;
198 /// The error type returned from all functions of this trait that return errors.
199 ///
200 /// Returning this signals that resolution should fail with this error.
201 ///
202 /// We're using `Infallible` here currently, as we don't produce any errors that need to be
203 /// returned yet, this is likely to change in the near future.
204 type Err = Infallible;
205
206 fn prioritize(
207 &self,
208 package: &Self::P,
209 range: &Self::VS,
210 _package_conflicts_counts: &pubgrub::PackageResolutionStatistics,
211 ) -> Self::Priority {
212 if let Some(prio) = self.context.try_prioritize(package) {
213 prio
214 } else if matches!(range.filter(), VersionSetFilter::Digest(_)) {
215 PackagePriority::Digest(Reverse(3))
216 } else if range.range().as_singleton().is_some() {
217 PackagePriority::Singleton(Reverse(2))
218 } else {
219 PackagePriority::Weak(Reverse(1))
220 }
221 }
222
223 fn choose_version(
224 &self,
225 package: &Self::P,
226 range: &Self::VS,
227 ) -> Result<Option<Self::V>, Self::Err> {
228 let Some(versions) = self.index.get(package) else {
229 return Ok(None);
230 };
231 let filter = range.filter();
232 let ranges = range.range();
233 if ranges.is_empty()
234 && let VersionSetFilter::Digest(digests) = filter
235 {
236 let version = versions
237 .keys()
238 .rev()
239 .find(|v| v.digest.as_ref().is_some_and(|digest| digests.contains(digest)))
240 .cloned();
241
242 Ok(version)
243 } else if let Some(version) = ranges.as_singleton()
244 && let Some((v, _)) = versions.get_key_value(version)
245 {
246 Ok(Some(v.clone()))
247 } else if let Some((start, end)) = ranges.bounding_range() {
248 let range = (start.cloned(), end.cloned());
249 let version = versions
250 .range(range)
251 .rev()
252 .find_map(|(v, _)| {
253 if filter.matches(v) && ranges.contains(&v.version) {
254 Some(v)
255 } else {
256 None
257 }
258 })
259 .cloned();
260 Ok(version)
261 } else {
262 let version = versions
263 .keys()
264 .rev()
265 .find(|v| filter.matches(v) && ranges.contains(&v.version))
266 .cloned();
267
268 Ok(version)
269 }
270 }
271
272 fn get_dependencies(
273 &self,
274 package: &Self::P,
275 version: &Self::V,
276 ) -> Result<pubgrub::Dependencies<Self::P, Self::VS, Self::M>, Self::Err> {
277 // If `package` has no available versions in the index, indicate that to the caller
278 let Some(available) = self.index.get(package) else {
279 return Ok(Dependencies::Unavailable(format!(
280 "no package named '{package}' found in registry"
281 )));
282 };
283
284 // If `version` is not in the index, indicate that to the caller
285 let Some(deps) = available.get(version) else {
286 return Ok(Dependencies::Unavailable(format!(
287 "version '{version}' of '{package}' was not found in registry"
288 )));
289 };
290
291 Ok(Dependencies::Available(
292 deps.iter().map(|(name, range)| (name.clone(), range.clone())).collect(),
293 ))
294 }
295
296 fn should_cancel(&self) -> Result<(), Self::Err> {
297 // TODO(pauls): We may wish to introduce a configurable resolution timeout, and return `Err`
298 // from this method if resolution exceeds some specified threshold.
299 //
300 // To do so, we may need to define a new struct that wraps a reference to the index with the
301 // timeout configuration, and move the implementation of this trait to that type instead,
302 // as there isn't a good place to manage the timeout on PackageIndex itself.
303 Ok(())
304 }
305}
306
307/// The error type raised when [PackageResolver::resolve] fails.
308#[derive(thiserror::Error)]
309pub enum DependencyResolutionError {
310 /// No solution was possible due to conflicts or unsatisified constraints.
311 #[error("dependency resolution failed: {}", format_solution_error(.0))]
312 NoSolution(Box<pubgrub::DerivationTree<PackageId, VersionSet, String>>),
313 /// Resolution could not proceed because one or more dependent packages were unavailable.
314 #[error("could not get dependencies for '{package}' version '{version}': {error}")]
315 FailedRetreivingDependencies {
316 package: PackageId,
317 version: Version,
318 error: String,
319 },
320 /// Resolution could not proceed because the resolver was unable to choose an appropriate
321 /// version for a given package.
322 #[error("could not choose version for {package}: {error}")]
323 FailedToChooseVersion { package: PackageId, error: String },
324 /// Resolution was cancelled by the implementation of
325 /// [`pubgrub::DependencyProvider::should_cancel`] - typically due to timeout or some other
326 /// external signal.
327 #[error("dependency resolution was cancelled: {reason}")]
328 Cancelled { reason: String },
329}
330
331impl core::fmt::Debug for DependencyResolutionError {
332 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
333 core::fmt::Display::fmt(self, f)
334 }
335}
336
337fn format_solution_error(tree: &pubgrub::DerivationTree<PackageId, VersionSet, String>) -> String {
338 use pubgrub::{DefaultStringReporter, Reporter};
339
340 DefaultStringReporter::report(tree)
341}
342
343impl From<pubgrub::DerivationTree<PackageId, VersionSet, String>> for DependencyResolutionError {
344 fn from(tree: pubgrub::DerivationTree<PackageId, VersionSet, String>) -> Self {
345 Self::NoSolution(Box::new(tree))
346 }
347}
348
349impl From<pubgrub::PubGrubError<PackageResolver<'_>>> for DependencyResolutionError {
350 fn from(error: pubgrub::PubGrubError<PackageResolver<'_>>) -> Self {
351 use pubgrub::PubGrubError;
352
353 match error {
354 PubGrubError::NoSolution(tree) => Self::from(tree),
355 PubGrubError::ErrorRetrievingDependencies { package, version, source: _ } => {
356 Self::FailedRetreivingDependencies { package, version, error: String::new() }
357 },
358 PubGrubError::ErrorChoosingVersion { package, source: _ } => {
359 Self::FailedToChooseVersion { package, error: String::new() }
360 },
361 PubGrubError::ErrorInShouldCancel(_err) => Self::Cancelled { reason: String::new() },
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use alloc::vec;
369
370 use miden_core::{Word, crypto::hash::Rpo256};
371 use pubgrub::{SelectedDependencies, VersionSet as _};
372
373 use super::*;
374 use crate::VersionReq;
375
376 fn select<'a>(packages: &[(&str, &str)]) -> SelectedDependencies<PackageResolver<'a>> {
377 packages
378 .iter()
379 .map(|(name, version)| {
380 let name = PackageId::from(*name);
381 let version = match version.split_once('@') {
382 Some((v, hex)) => {
383 let version = v.parse::<SemVer>().unwrap();
384 let word = Word::parse(hex).unwrap();
385 Version { version, digest: Some(word.into()) }
386 },
387 None => Version {
388 version: version.parse::<SemVer>().unwrap(),
389 digest: None,
390 },
391 };
392 (name, version)
393 })
394 .collect()
395 }
396
397 fn assert_selected<'a>(
398 resolved: &SelectedDependencies<PackageResolver<'a>>,
399 expected: &SelectedDependencies<PackageResolver<'a>>,
400 ) {
401 use core::cmp::Ordering;
402
403 match resolved.len().cmp(&expected.len()) {
404 Ordering::Equal | Ordering::Greater => {
405 for (k, v) in resolved.iter() {
406 assert_eq!(
407 expected.get(k),
408 Some(v),
409 "unexpected dependency found in selection"
410 );
411 }
412 },
413 Ordering::Less => {
414 for (k, v) in expected.iter() {
415 assert_eq!(resolved.get(k), Some(v), "missing expected dependency '{k}@{v}'");
416 }
417 },
418 }
419 }
420
421 /// This test verifies that version requirements for both semantic versions and content digests
422 /// can be resolved, and that requirements on specific content digests are resolved correctly.
423 #[test]
424 fn resolver_resolve_mixed_versioning_schemes() {
425 let digest = Rpo256::hash(b"the digest for 'b'");
426 let index = PackageIndex::from_iter([
427 ("a", vec![("0.1.0".parse().unwrap(), vec![("b", VersionSet::from(digest))])]),
428 (
429 "b",
430 vec![
431 (
432 Version::new("1.0.0".parse().unwrap(), digest),
433 vec![
434 ("c", VersionSet::full()),
435 ("d", VersionSet::from("=0.2.1".parse::<VersionReq>().unwrap())),
436 ],
437 ),
438 (
439 "1.0.1".parse().unwrap(),
440 vec![
441 ("c", VersionSet::full()),
442 ("d", VersionSet::from("=0.2.1".parse::<VersionReq>().unwrap())),
443 ],
444 ),
445 ],
446 ),
447 (
448 "c",
449 vec![("0.2.0".parse().unwrap(), vec![]), ("0.3.0".parse().unwrap(), vec![])],
450 ),
451 (
452 "d",
453 vec![
454 ("0.1.0".parse().unwrap(), vec![]),
455 ("0.2.1".parse().unwrap(), vec![]),
456 ("0.2.5".parse().unwrap(), vec![]),
457 ("1.0.0".parse().unwrap(), vec![]),
458 ],
459 ),
460 ]);
461
462 let resolver = PackageResolver {
463 index: &index,
464 context: ResolverContext::Package {
465 root: PackageId::from("a"),
466 version: "0.1.0".parse().unwrap(),
467 },
468 };
469 let selected = resolver.resolve().expect("failed to resolve 'a'");
470 let b_version = format!("1.0.0@{digest}");
471 let expected =
472 select(&[("a", "0.1.0"), ("b", b_version.as_str()), ("c", "0.3.0"), ("d", "0.2.1")]);
473
474 assert_selected(&selected, &expected);
475 }
476
477 /// In this test:
478 ///
479 /// * a@0.1.0 depends on b@1.0.0
480 /// * b@1.0.0 depends on d@0.2.1
481 /// * d@0.2.1 does not exist in the index
482 ///
483 /// As a result, the requirement on d@0.2.1 by b is unsatisfiable, and b does not permit any of
484 /// the other available versions from being selected (it wants _only_ 0.2.1).
485 #[test]
486 #[should_panic = "Because there is no version of d in >= 0.2.1 and < 0.2.2"]
487 fn resolver_resolve_package_not_found() {
488 let index = PackageIndex::from_iter([
489 (
490 "a",
491 vec![(
492 "0.1.0".parse().unwrap(),
493 vec![("b", VersionSet::from("=1.0.0".parse::<VersionReq>().unwrap()))],
494 )],
495 ),
496 (
497 "b",
498 vec![(
499 "1.0.0".parse().unwrap(),
500 vec![
501 ("c", VersionSet::full()),
502 ("d", VersionSet::from("=0.2.1".parse::<VersionReq>().unwrap())),
503 ],
504 )],
505 ),
506 (
507 "c",
508 vec![("0.2.0".parse().unwrap(), vec![]), ("0.3.0".parse().unwrap(), vec![])],
509 ),
510 (
511 "d",
512 vec![
513 ("0.1.0".parse().unwrap(), vec![]),
514 ("0.2.5".parse().unwrap(), vec![]),
515 ("1.0.0".parse().unwrap(), vec![]),
516 ],
517 ),
518 ]);
519
520 let resolver = PackageResolver {
521 index: &index,
522 context: ResolverContext::Package {
523 root: PackageId::from("a"),
524 version: "0.1.0".parse().unwrap(),
525 },
526 };
527 let _ = resolver.resolve().unwrap();
528 }
529
530 /// In this test:
531 ///
532 /// * a@0.1.0 depends on b@1.0.0
533 /// * b@1.0.0 depends on d@0.2.1 and any version of c
534 /// * c@0.2.0 depends on d@0.2.5
535 /// * c@0.3.0 depends on any d that matches ^1.0.0
536 ///
537 /// As a result, there is a conflict in the requirements for b and c; the former has a strict
538 /// requirement on 0.2.1, but both versions of c that are available require newer versions of
539 /// d than 0.2.1.
540 #[test]
541 #[should_panic = "because c =0.3.0 depends on d >= 1.0.0 and < 2.0.0, c depends on d >= 0.2.5 and < 0.2.6, or >= 1.0.0 and < 2.0.0"]
542 fn resolver_resolve_package_conflict() {
543 let index = PackageIndex::from_iter([
544 (
545 "a",
546 vec![(
547 "0.1.0".parse().unwrap(),
548 vec![("b", VersionSet::from("=1.0.0".parse::<VersionReq>().unwrap()))],
549 )],
550 ),
551 (
552 "b",
553 vec![(
554 "1.0.0".parse().unwrap(),
555 vec![
556 ("c", VersionSet::full()),
557 ("d", VersionSet::from("=0.2.1".parse::<VersionReq>().unwrap())),
558 ],
559 )],
560 ),
561 (
562 "c",
563 vec![
564 (
565 "0.2.0".parse().unwrap(),
566 vec![("d", VersionSet::from("=0.2.5".parse::<VersionReq>().unwrap()))],
567 ),
568 (
569 "0.3.0".parse().unwrap(),
570 vec![("d", VersionSet::from("^1.0.0".parse::<VersionReq>().unwrap()))],
571 ),
572 ],
573 ),
574 (
575 "d",
576 vec![
577 ("0.1.0".parse().unwrap(), vec![]),
578 ("0.2.1".parse().unwrap(), vec![]),
579 ("0.2.5".parse().unwrap(), vec![]),
580 ("1.0.0".parse().unwrap(), vec![]),
581 ],
582 ),
583 ]);
584
585 let resolver = PackageResolver {
586 index: &index,
587 context: ResolverContext::Package {
588 root: PackageId::from("a"),
589 version: "0.1.0".parse().unwrap(),
590 },
591 };
592 let _ = resolver.resolve().unwrap();
593 }
594
595 /// In this test:
596 ///
597 /// * a@0.1.0 depends on b@1.0.0
598 /// * b@1.0.0 depends on d matching ^0.2.1 and any version of c
599 /// * c@0.2.0 depends on d matching ^0.2.5
600 /// * c@0.3.0 depends on d matching ^1.0.0
601 ///
602 /// While b@1.0.0 and c have differing requirements on d - c@0.2.0 has a requirement on d that
603 /// is compatible with b's requirement on d (according to semantic versioning rules).
604 ///
605 /// As a result, the resolver is expected to select c@0.2.0 here, as it is the only version
606 /// that satisfies all requirements in the dependency tree.
607 #[test]
608 fn resolver_resolve_compatible_packages() {
609 let index = PackageIndex::from_iter([
610 (
611 "a",
612 vec![(
613 "0.1.0".parse().unwrap(),
614 vec![("b", VersionSet::from("=1.0.0".parse::<VersionReq>().unwrap()))],
615 )],
616 ),
617 (
618 "b",
619 vec![(
620 "1.0.0".parse().unwrap(),
621 vec![
622 ("c", VersionSet::full()),
623 ("d", VersionSet::from("^0.2.1".parse::<VersionReq>().unwrap())),
624 ],
625 )],
626 ),
627 (
628 "c",
629 vec![
630 (
631 "0.2.0".parse().unwrap(),
632 vec![("d", VersionSet::from("^0.2.5".parse::<VersionReq>().unwrap()))],
633 ),
634 (
635 "0.3.0".parse().unwrap(),
636 vec![("d", VersionSet::from("^1.0.0".parse::<VersionReq>().unwrap()))],
637 ),
638 ],
639 ),
640 (
641 "d",
642 vec![
643 ("0.1.0".parse().unwrap(), vec![]),
644 ("0.2.1".parse().unwrap(), vec![]),
645 ("0.2.5".parse().unwrap(), vec![]),
646 ("1.0.0".parse().unwrap(), vec![]),
647 ],
648 ),
649 ]);
650
651 let resolver = PackageResolver {
652 index: &index,
653 context: ResolverContext::Package {
654 root: PackageId::from("a"),
655 version: "0.1.0".parse().unwrap(),
656 },
657 };
658
659 let selected = resolver.resolve().expect("failed to resolve");
660 let expected = select(&[("a", "0.1.0"), ("b", "1.0.0"), ("c", "0.2.0"), ("d", "0.2.5")]);
661
662 assert_selected(&selected, &expected);
663 }
664}