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