cargo/core/resolver/
version_prefs.rs

1//! This module implements support for preferring some versions of a package
2//! over other versions.
3
4use std::cmp::Ordering;
5use std::collections::{HashMap, HashSet};
6
7use cargo_util_schemas::core::PartialVersion;
8
9use crate::core::{Dependency, PackageId, Summary};
10use crate::util::interning::InternedString;
11
12/// A collection of preferences for particular package versions.
13///
14/// This is built up with [`Self::prefer_package_id`] and [`Self::prefer_dependency`], then used to sort the set of
15/// summaries for a package during resolution via [`Self::sort_summaries`].
16///
17/// As written, a version is either "preferred" or "not preferred".  Later extensions may
18/// introduce more granular preferences.
19#[derive(Default)]
20pub struct VersionPreferences {
21    try_to_use: HashSet<PackageId>,
22    prefer_patch_deps: HashMap<InternedString, HashSet<Dependency>>,
23    version_ordering: VersionOrdering,
24    rust_versions: Vec<PartialVersion>,
25}
26
27#[derive(Copy, Clone, Default, PartialEq, Eq, Hash, Debug)]
28pub enum VersionOrdering {
29    #[default]
30    MaximumVersionsFirst,
31    MinimumVersionsFirst,
32}
33
34impl VersionPreferences {
35    /// Indicate that the given package (specified as a [`PackageId`]) should be preferred.
36    pub fn prefer_package_id(&mut self, pkg_id: PackageId) {
37        self.try_to_use.insert(pkg_id);
38    }
39
40    /// Indicate that the given package (specified as a [`Dependency`])  should be preferred.
41    pub fn prefer_dependency(&mut self, dep: Dependency) {
42        self.prefer_patch_deps
43            .entry(dep.package_name())
44            .or_insert_with(HashSet::new)
45            .insert(dep);
46    }
47
48    pub fn version_ordering(&mut self, ordering: VersionOrdering) {
49        self.version_ordering = ordering;
50    }
51
52    pub fn rust_versions(&mut self, vers: Vec<PartialVersion>) {
53        self.rust_versions = vers;
54    }
55
56    /// Sort (and filter) the given vector of summaries in-place
57    ///
58    /// Note: all summaries presumed to be for the same package.
59    ///
60    /// Sort order:
61    /// 1. Preferred packages
62    /// 2. Most compatible [`VersionPreferences::rust_versions`]
63    /// 3. `first_version`, falling back to [`VersionPreferences::version_ordering`] when `None`
64    ///
65    /// Filtering:
66    /// - `first_version`
67    pub fn sort_summaries(
68        &self,
69        summaries: &mut Vec<Summary>,
70        first_version: Option<VersionOrdering>,
71    ) {
72        let should_prefer = |pkg_id: &PackageId| {
73            self.try_to_use.contains(pkg_id)
74                || self
75                    .prefer_patch_deps
76                    .get(&pkg_id.name())
77                    .map(|deps| deps.iter().any(|d| d.matches_id(*pkg_id)))
78                    .unwrap_or(false)
79        };
80        summaries.sort_unstable_by(|a, b| {
81            let prefer_a = should_prefer(&a.package_id());
82            let prefer_b = should_prefer(&b.package_id());
83            let previous_cmp = prefer_a.cmp(&prefer_b).reverse();
84            if previous_cmp != Ordering::Equal {
85                return previous_cmp;
86            }
87
88            if !self.rust_versions.is_empty() {
89                let a_compat_count = self.msrv_compat_count(a);
90                let b_compat_count = self.msrv_compat_count(b);
91                if b_compat_count != a_compat_count {
92                    return b_compat_count.cmp(&a_compat_count);
93                }
94            }
95
96            let cmp = a.version().cmp(b.version());
97            match first_version.unwrap_or(self.version_ordering) {
98                VersionOrdering::MaximumVersionsFirst => cmp.reverse(),
99                VersionOrdering::MinimumVersionsFirst => cmp,
100            }
101        });
102        if first_version.is_some() && !summaries.is_empty() {
103            let _ = summaries.split_off(1);
104        }
105    }
106
107    fn msrv_compat_count(&self, summary: &Summary) -> usize {
108        let Some(rust_version) = summary.rust_version() else {
109            return self.rust_versions.len();
110        };
111
112        self.rust_versions
113            .iter()
114            .filter(|max| rust_version.is_compatible_with(max))
115            .count()
116    }
117}
118
119#[cfg(test)]
120mod test {
121    use super::*;
122    use crate::core::SourceId;
123    use std::collections::BTreeMap;
124
125    fn pkgid(name: &str, version: &str) -> PackageId {
126        let src_id =
127            SourceId::from_url("registry+https://github.com/rust-lang/crates.io-index").unwrap();
128        PackageId::try_new(name, version, src_id).unwrap()
129    }
130
131    fn dep(name: &str, version: &str) -> Dependency {
132        let src_id =
133            SourceId::from_url("registry+https://github.com/rust-lang/crates.io-index").unwrap();
134        Dependency::parse(name, Some(version), src_id).unwrap()
135    }
136
137    fn summ(name: &str, version: &str, msrv: Option<&str>) -> Summary {
138        let pkg_id = pkgid(name, version);
139        let features = BTreeMap::new();
140        Summary::new(
141            pkg_id,
142            Vec::new(),
143            &features,
144            None::<&String>,
145            msrv.map(|m| m.parse().unwrap()),
146        )
147        .unwrap()
148    }
149
150    fn describe(summaries: &Vec<Summary>) -> String {
151        let strs: Vec<String> = summaries
152            .iter()
153            .map(|summary| format!("{}/{}", summary.name(), summary.version()))
154            .collect();
155        strs.join(", ")
156    }
157
158    #[test]
159    fn test_prefer_package_id() {
160        let mut vp = VersionPreferences::default();
161        vp.prefer_package_id(pkgid("foo", "1.2.3"));
162
163        let mut summaries = vec![
164            summ("foo", "1.2.4", None),
165            summ("foo", "1.2.3", None),
166            summ("foo", "1.1.0", None),
167            summ("foo", "1.0.9", None),
168        ];
169
170        vp.version_ordering(VersionOrdering::MaximumVersionsFirst);
171        vp.sort_summaries(&mut summaries, None);
172        assert_eq!(
173            describe(&summaries),
174            "foo/1.2.3, foo/1.2.4, foo/1.1.0, foo/1.0.9".to_string()
175        );
176
177        vp.version_ordering(VersionOrdering::MinimumVersionsFirst);
178        vp.sort_summaries(&mut summaries, None);
179        assert_eq!(
180            describe(&summaries),
181            "foo/1.2.3, foo/1.0.9, foo/1.1.0, foo/1.2.4".to_string()
182        );
183    }
184
185    #[test]
186    fn test_prefer_dependency() {
187        let mut vp = VersionPreferences::default();
188        vp.prefer_dependency(dep("foo", "=1.2.3"));
189
190        let mut summaries = vec![
191            summ("foo", "1.2.4", None),
192            summ("foo", "1.2.3", None),
193            summ("foo", "1.1.0", None),
194            summ("foo", "1.0.9", None),
195        ];
196
197        vp.version_ordering(VersionOrdering::MaximumVersionsFirst);
198        vp.sort_summaries(&mut summaries, None);
199        assert_eq!(
200            describe(&summaries),
201            "foo/1.2.3, foo/1.2.4, foo/1.1.0, foo/1.0.9".to_string()
202        );
203
204        vp.version_ordering(VersionOrdering::MinimumVersionsFirst);
205        vp.sort_summaries(&mut summaries, None);
206        assert_eq!(
207            describe(&summaries),
208            "foo/1.2.3, foo/1.0.9, foo/1.1.0, foo/1.2.4".to_string()
209        );
210    }
211
212    #[test]
213    fn test_prefer_both() {
214        let mut vp = VersionPreferences::default();
215        vp.prefer_package_id(pkgid("foo", "1.2.3"));
216        vp.prefer_dependency(dep("foo", "=1.1.0"));
217
218        let mut summaries = vec![
219            summ("foo", "1.2.4", None),
220            summ("foo", "1.2.3", None),
221            summ("foo", "1.1.0", None),
222            summ("foo", "1.0.9", None),
223        ];
224
225        vp.version_ordering(VersionOrdering::MaximumVersionsFirst);
226        vp.sort_summaries(&mut summaries, None);
227        assert_eq!(
228            describe(&summaries),
229            "foo/1.2.3, foo/1.1.0, foo/1.2.4, foo/1.0.9".to_string()
230        );
231
232        vp.version_ordering(VersionOrdering::MinimumVersionsFirst);
233        vp.sort_summaries(&mut summaries, None);
234        assert_eq!(
235            describe(&summaries),
236            "foo/1.1.0, foo/1.2.3, foo/1.0.9, foo/1.2.4".to_string()
237        );
238    }
239
240    #[test]
241    fn test_single_rust_version() {
242        let mut vp = VersionPreferences::default();
243        vp.rust_versions(vec!["1.50".parse().unwrap()]);
244
245        let mut summaries = vec![
246            summ("foo", "1.2.4", None),
247            summ("foo", "1.2.3", Some("1.60")),
248            summ("foo", "1.2.2", None),
249            summ("foo", "1.2.1", Some("1.50")),
250            summ("foo", "1.2.0", None),
251            summ("foo", "1.1.0", Some("1.40")),
252            summ("foo", "1.0.9", None),
253        ];
254
255        vp.version_ordering(VersionOrdering::MaximumVersionsFirst);
256        vp.sort_summaries(&mut summaries, None);
257        assert_eq!(
258            describe(&summaries),
259            "foo/1.2.4, foo/1.2.2, foo/1.2.1, foo/1.2.0, foo/1.1.0, foo/1.0.9, foo/1.2.3"
260                .to_string()
261        );
262
263        vp.version_ordering(VersionOrdering::MinimumVersionsFirst);
264        vp.sort_summaries(&mut summaries, None);
265        assert_eq!(
266            describe(&summaries),
267            "foo/1.0.9, foo/1.1.0, foo/1.2.0, foo/1.2.1, foo/1.2.2, foo/1.2.4, foo/1.2.3"
268                .to_string()
269        );
270    }
271
272    #[test]
273    fn test_multiple_rust_versions() {
274        let mut vp = VersionPreferences::default();
275        vp.rust_versions(vec!["1.45".parse().unwrap(), "1.55".parse().unwrap()]);
276
277        let mut summaries = vec![
278            summ("foo", "1.2.4", None),
279            summ("foo", "1.2.3", Some("1.60")),
280            summ("foo", "1.2.2", None),
281            summ("foo", "1.2.1", Some("1.50")),
282            summ("foo", "1.2.0", None),
283            summ("foo", "1.1.0", Some("1.40")),
284            summ("foo", "1.0.9", None),
285        ];
286
287        vp.version_ordering(VersionOrdering::MaximumVersionsFirst);
288        vp.sort_summaries(&mut summaries, None);
289        assert_eq!(
290            describe(&summaries),
291            "foo/1.2.4, foo/1.2.2, foo/1.2.0, foo/1.1.0, foo/1.0.9, foo/1.2.1, foo/1.2.3"
292                .to_string()
293        );
294
295        vp.version_ordering(VersionOrdering::MinimumVersionsFirst);
296        vp.sort_summaries(&mut summaries, None);
297        assert_eq!(
298            describe(&summaries),
299            "foo/1.0.9, foo/1.1.0, foo/1.2.0, foo/1.2.2, foo/1.2.4, foo/1.2.1, foo/1.2.3"
300                .to_string()
301        );
302    }
303
304    #[test]
305    fn test_empty_summaries() {
306        let vp = VersionPreferences::default();
307        let mut summaries = vec![];
308
309        vp.sort_summaries(&mut summaries, Some(VersionOrdering::MaximumVersionsFirst));
310        assert_eq!(summaries, vec![]);
311    }
312}