Skip to main content

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}