miden_project/dependencies/resolver/index.rs
1use alloc::collections::BTreeMap;
2use core::borrow::Borrow;
3
4use pubgrub::VersionSet as _;
5
6use super::*;
7use crate::{Package, Version, VersionRequirement};
8
9/// A type alias for an ordered map of packages and the set of versions that can satisfy the
10/// requirement for that package.
11pub type PackageRequirements = BTreeMap<PackageId, VersionSet>;
12
13/// [PackageIndex] is used as an in-memory database of package information needed for dependency
14/// resolution for Miden projects. It records and can provide information about:
15///
16/// * All packages for which there is at least one version available
17/// * All versions of a package that are available
18/// * The dependencies for each version of a package, and their version requirements.
19///
20/// Additionally, [PackageIndex] implements version selection and dependency resolution for packages
21/// in the index. Since this functionality is central to any tooling around Miden projects, we
22/// implement it here, rather than push the responsibility onto downstream crates.
23///
24/// This structure _does not_ provide any of the following:
25///
26/// * Package metadata beyond version and dependency requirements
27/// * MAST of indexed packages
28/// * A guarantee that indexed packages actually exist or that their sources/MAST can be obtained
29///
30/// It is up to downstream consumers of the index to construct it and populate it with packages
31/// known to them, such that packages selected by [`PackageResolver::resolve`] can be resolved to
32/// relevant artifacts, e.g. an assembled package, Miden project sources, etc.
33#[derive(Default)]
34pub struct PackageIndex {
35 packages: BTreeMap<PackageId, BTreeMap<Version, PackageRequirements>>,
36}
37
38/// Construction
39impl PackageIndex {
40 /// Register `package` in the index.
41 ///
42 /// This is essentially a convenience wrapper around [`PackageIndex::insert`] for the common
43 /// case of populating the index with package information loaded from a Miden project.
44 pub fn register(&mut self, package: &Package) {
45 let name = PackageId::from(package.name().into_inner());
46 let version = package.version().into_inner().clone();
47 let version = Version::from(version);
48 let deps = package.dependencies().iter().map(|dep| {
49 let name = PackageId::from(dep.name().clone());
50 let req = match dep.required_version() {
51 None => VersionSet::full(),
52 Some(req) => VersionSet::from(req),
53 };
54 (name, req)
55 });
56
57 self.insert(name, version, deps);
58 }
59
60 /// Insert a new entry in the index for `name` and `version`, with `dependencies` providing the
61 /// set of version constraints for the package dependencies.
62 pub fn insert<D, P, V>(&mut self, name: impl Into<PackageId>, version: Version, dependencies: D)
63 where
64 D: IntoIterator<Item = (P, V)>,
65 P: Into<PackageId>,
66 V: Into<VersionSet>,
67 {
68 let name = name.into();
69 let deps = dependencies.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
70
71 self.insert_or_refine(name, version, deps);
72 }
73
74 /// Add or update an entry in the index for `name` and `version`, as follows:
75 ///
76 /// * If there are no versions recorded for `name`, initialize the index for `name` with the
77 /// provided `version` and `deps.
78 /// * If there are existing versions for `name`, but `version` is new, and an entry for
79 /// `version` with `deps`.
80 /// * If there is an existing entry for `version`:
81 /// * Overwrite the previous dependency map with `deps` for the entry
82 /// * If the entry in the index does not have an associated package digest, but one was given
83 /// with `version`, then update the entry in the index to reflect the more precise version
84 /// data.
85 /// * If the entry in the index has an associated package digest, but `version` does not, then
86 /// ignore the insertion entirely, in order to preserve the dependencies that were recorded
87 /// with the more precise version.
88 ///
89 /// In effect, this method ensures that we only expand or refine the index.
90 fn insert_or_refine(&mut self, name: PackageId, version: Version, deps: PackageRequirements) {
91 use alloc::collections::btree_map::Entry;
92
93 match self.packages.entry(name) {
94 Entry::Vacant(entry) => {
95 let versions = BTreeMap::from_iter([(version, deps)]);
96 entry.insert(versions);
97 },
98 Entry::Occupied(mut entry) => {
99 let versions = entry.get_mut();
100 match versions.entry(version.clone()) {
101 Entry::Vacant(entry) => {
102 entry.insert(deps);
103 },
104 Entry::Occupied(mut entry) => {
105 // Do not overwrite an existing entry if that entry has an associated
106 // package digest, so as to preserve the more precise version information.
107 if entry.key().digest.is_none() {
108 // If `version` has an associated package digest, then we need to
109 // recreate the entry in the index to ensure that the more precise
110 // version is used
111 if version.digest.is_some() {
112 let _ = entry.remove_entry();
113 versions.insert(version, deps);
114 } else {
115 // Otherwise, treat this like a normal `insert` and overwrite the
116 // dependencies associated with this version entry.
117 entry.insert(deps);
118 }
119 }
120 },
121 }
122 },
123 }
124 }
125}
126
127/// Queries
128impl PackageIndex {
129 /// Returns true if package `name` has any available versions in the index.
130 #[inline(always)]
131 pub fn is_available<Q>(&self, name: &Q) -> bool
132 where
133 PackageId: Borrow<Q>,
134 Q: Ord + ?Sized,
135 {
136 self.packages.contains_key(name)
137 }
138
139 /// Returns true if package `name` with `version` is available in the index.
140 #[inline(always)]
141 pub fn is_version_available<Q>(&self, name: &Q, version: &Version) -> bool
142 where
143 PackageId: Borrow<Q>,
144 Q: Ord + ?Sized,
145 {
146 self.packages
147 .get(name)
148 .map(|versions| versions.contains_key(version))
149 .unwrap_or(false)
150 }
151
152 /// Search for a version of package `name` that matches `requirement`, and return its
153 /// dependencies.
154 ///
155 /// The version selected by this method will be the latest one which satisfies `requirement`.
156 #[inline(always)]
157 pub fn find<Q>(
158 &self,
159 name: &Q,
160 requirement: &VersionRequirement,
161 ) -> Option<&PackageRequirements>
162 where
163 PackageId: Borrow<Q>,
164 Q: Ord + ?Sized,
165 {
166 self.packages.get(name).and_then(|versions| {
167 versions
168 .iter()
169 .rev()
170 .find_map(|(v, deps)| if v.satisfies(requirement) { Some(deps) } else { None })
171 })
172 }
173
174 /// Get the set of versions associated with `name` in the index.
175 #[inline(always)]
176 pub(crate) fn get<Q>(&self, name: &Q) -> Option<&BTreeMap<Version, PackageRequirements>>
177 where
178 PackageId: Borrow<Q>,
179 Q: Ord + ?Sized,
180 {
181 self.packages.get(name)
182 }
183
184 /// Get an iterator over all versions of `package` available in the index, in descending order.
185 ///
186 /// NOTE: Descending order here is determined by the `Ord` implementation of [Version], which
187 /// orders versions by their semantic versioning scheme, and disambiguates using package digests
188 /// when known.
189 pub fn available_versions<Q>(&self, package: &Q) -> impl Iterator<Item = &Version>
190 where
191 PackageId: Borrow<Q>,
192 Q: Ord + ?Sized,
193 {
194 self.packages.get(package).into_iter().flat_map(|k| k.keys()).rev()
195 }
196
197 /// Get an iterator over all versions of `package` that satisfy `requirement.
198 pub fn list_versions<'a, Q>(
199 &'a self,
200 package: &'a Q,
201 requirement: &'a VersionRequirement,
202 ) -> impl Iterator<Item = &'a Version> + 'a
203 where
204 PackageId: Borrow<Q>,
205 Q: Ord + ?Sized,
206 {
207 self.available_versions(package).filter(|v| v.satisfies(requirement))
208 }
209}
210
211impl<'a, V, D> FromIterator<(&'a str, V)> for PackageIndex
212where
213 D: IntoIterator<Item = (&'a str, VersionSet)>,
214 V: IntoIterator<Item = (Version, D)>,
215{
216 fn from_iter<I>(iter: I) -> Self
217 where
218 I: IntoIterator<Item = (&'a str, V)>,
219 {
220 let mut index = Self::default();
221 for (name, versions) in iter {
222 let name = PackageId::from(name);
223 for (version, deps) in versions {
224 let deps = deps.into_iter().map(|(name, vs)| (PackageId::from(name), vs));
225 index.insert(name.clone(), version, deps);
226 }
227 }
228 index
229 }
230}