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