#
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}