Skip to main content

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