Skip to content

Commit 134ed93

Browse files
committed
fix: Consistently compare MSRVs
We used several strategies - Relying in `impl Ord for RustVersion` - Converting to version requirements - Decrementing a version This consolidates around one strategy: `RustVersion::is_compatible_with` - Ensure the comparisons have the same behavior - Centralize knowledge of how to handle pre-release rustc - Losslessly allow comparing with either rustc or workspace msrv
1 parent 1616881 commit 134ed93

File tree

8 files changed

+125
-56
lines changed

8 files changed

+125
-56
lines changed

crates/cargo-util-schemas/src/manifest/rust_version.rs

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,25 @@ use crate::core::PartialVersionError;
1010
#[serde(transparent)]
1111
pub struct RustVersion(PartialVersion);
1212

13-
impl std::ops::Deref for RustVersion {
14-
type Target = PartialVersion;
13+
impl RustVersion {
14+
pub fn is_compatible_with(&self, rustc: &PartialVersion) -> bool {
15+
let msrv = self.0.to_caret_req();
16+
// Remove any pre-release identifiers for easier comparison
17+
let rustc = semver::Version {
18+
major: rustc.major,
19+
minor: rustc.minor.unwrap_or_default(),
20+
patch: rustc.patch.unwrap_or_default(),
21+
pre: Default::default(),
22+
build: Default::default(),
23+
};
24+
msrv.matches(&rustc)
25+
}
26+
27+
pub fn into_partial(self) -> PartialVersion {
28+
self.0
29+
}
1530

16-
fn deref(&self) -> &Self::Target {
31+
pub fn as_partial(&self) -> &PartialVersion {
1732
&self.0
1833
}
1934
}
@@ -28,6 +43,15 @@ impl std::str::FromStr for RustVersion {
2843
}
2944
}
3045

46+
impl TryFrom<semver::Version> for RustVersion {
47+
type Error = RustVersionError;
48+
49+
fn try_from(version: semver::Version) -> Result<Self, Self::Error> {
50+
let version = PartialVersion::from(version);
51+
Self::try_from(version)
52+
}
53+
}
54+
3155
impl TryFrom<PartialVersion> for RustVersion {
3256
type Error = RustVersionError;
3357

@@ -78,3 +102,72 @@ enum RustVersionErrorKind {
78102
#[error(transparent)]
79103
PartialVersion(#[from] PartialVersionError),
80104
}
105+
106+
#[cfg(test)]
107+
mod test {
108+
use super::*;
109+
110+
#[test]
111+
fn is_compatible_with_rustc() {
112+
let cases = &[
113+
("1", "1.70.0", true),
114+
("1.30", "1.70.0", true),
115+
("1.30.10", "1.70.0", true),
116+
("1.70", "1.70.0", true),
117+
("1.70.0", "1.70.0", true),
118+
("1.70.1", "1.70.0", false),
119+
("1.70", "1.70.0-nightly", true),
120+
("1.70.0", "1.70.0-nightly", true),
121+
("1.71", "1.70.0", false),
122+
("2", "1.70.0", false),
123+
];
124+
let mut passed = true;
125+
for (msrv, rustc, expected) in cases {
126+
let msrv: RustVersion = msrv.parse().unwrap();
127+
let rustc = PartialVersion::from(semver::Version::parse(rustc).unwrap());
128+
if msrv.is_compatible_with(&rustc) != *expected {
129+
println!("failed: {msrv} is_compatible_with {rustc} == {expected}");
130+
passed = false;
131+
}
132+
}
133+
assert!(passed);
134+
}
135+
136+
#[test]
137+
fn is_compatible_with_workspace_msrv() {
138+
let cases = &[
139+
("1", "1", true),
140+
("1", "1.70", true),
141+
("1", "1.70.0", true),
142+
("1.30", "1", false),
143+
("1.30", "1.70", true),
144+
("1.30", "1.70.0", true),
145+
("1.30.10", "1", false),
146+
("1.30.10", "1.70", true),
147+
("1.30.10", "1.70.0", true),
148+
("1.70", "1", false),
149+
("1.70", "1.70", true),
150+
("1.70", "1.70.0", true),
151+
("1.70.0", "1", false),
152+
("1.70.0", "1.70", true),
153+
("1.70.0", "1.70.0", true),
154+
("1.70.1", "1", false),
155+
("1.70.1", "1.70", false),
156+
("1.70.1", "1.70.0", false),
157+
("1.71", "1", false),
158+
("1.71", "1.70", false),
159+
("1.71", "1.70.0", false),
160+
("2", "1.70.0", false),
161+
];
162+
let mut passed = true;
163+
for (dep_msrv, ws_msrv, expected) in cases {
164+
let dep_msrv: RustVersion = dep_msrv.parse().unwrap();
165+
let ws_msrv = ws_msrv.parse::<RustVersion>().unwrap().into_partial();
166+
if dep_msrv.is_compatible_with(&ws_msrv) != *expected {
167+
println!("failed: {dep_msrv} is_compatible_with {ws_msrv} == {expected}");
168+
passed = false;
169+
}
170+
}
171+
assert!(passed);
172+
}
173+
}

src/cargo/core/resolver/version_prefs.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use std::cmp::Ordering;
55
use std::collections::{HashMap, HashSet};
66

7-
use cargo_util_schemas::manifest::RustVersion;
7+
use cargo_util_schemas::core::PartialVersion;
88

99
use crate::core::{Dependency, PackageId, Summary};
1010
use crate::util::interning::InternedString;
@@ -21,7 +21,7 @@ pub struct VersionPreferences {
2121
try_to_use: HashSet<PackageId>,
2222
prefer_patch_deps: HashMap<InternedString, HashSet<Dependency>>,
2323
version_ordering: VersionOrdering,
24-
max_rust_version: Option<RustVersion>,
24+
max_rust_version: Option<PartialVersion>,
2525
}
2626

2727
#[derive(Copy, Clone, Default, PartialEq, Eq, Hash, Debug)]
@@ -49,7 +49,7 @@ impl VersionPreferences {
4949
self.version_ordering = ordering;
5050
}
5151

52-
pub fn max_rust_version(&mut self, ver: Option<RustVersion>) {
52+
pub fn max_rust_version(&mut self, ver: Option<PartialVersion>) {
5353
self.max_rust_version = ver;
5454
}
5555

@@ -92,8 +92,8 @@ impl VersionPreferences {
9292
(Some(a), Some(b)) if a == b => {}
9393
// Primary comparison
9494
(Some(a), Some(b)) => {
95-
let a_is_compat = a <= max_rust_version;
96-
let b_is_compat = b <= max_rust_version;
95+
let a_is_compat = a.is_compatible_with(max_rust_version);
96+
let b_is_compat = b.is_compatible_with(max_rust_version);
9797
match (a_is_compat, b_is_compat) {
9898
(true, true) => {} // fallback
9999
(false, false) => {} // fallback
@@ -103,14 +103,14 @@ impl VersionPreferences {
103103
}
104104
// Prioritize `None` over incompatible
105105
(None, Some(b)) => {
106-
if b <= max_rust_version {
106+
if b.is_compatible_with(max_rust_version) {
107107
return Ordering::Greater;
108108
} else {
109109
return Ordering::Less;
110110
}
111111
}
112112
(Some(a), None) => {
113-
if a <= max_rust_version {
113+
if a.is_compatible_with(max_rust_version) {
114114
return Ordering::Less;
115115
} else {
116116
return Ordering::Greater;

src/cargo/ops/cargo_add/mod.rs

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -619,21 +619,13 @@ fn get_latest_dependency(
619619
let (req_msrv, is_msrv) = spec
620620
.rust_version()
621621
.cloned()
622-
.map(|msrv| CargoResult::Ok((msrv.clone(), true)))
622+
.map(|msrv| CargoResult::Ok((msrv.clone().into_partial(), true)))
623623
.unwrap_or_else(|| {
624624
let rustc = gctx.load_global_rustc(None)?;
625625

626626
// Remove any pre-release identifiers for easier comparison
627-
let current_version = &rustc.version;
628-
let untagged_version = RustVersion::try_from(PartialVersion {
629-
major: current_version.major,
630-
minor: Some(current_version.minor),
631-
patch: Some(current_version.patch),
632-
pre: None,
633-
build: None,
634-
})
635-
.unwrap();
636-
Ok((untagged_version, false))
627+
let rustc_version = rustc.version.clone().into();
628+
Ok((rustc_version, false))
637629
})?;
638630

639631
let msrvs = possibilities
@@ -702,14 +694,14 @@ ignoring {dependency}@{latest_version} (which requires rustc {latest_rust_versio
702694
/// - `msrvs` is sorted by version
703695
fn latest_compatible<'s>(
704696
msrvs: &[(&'s Summary, Option<&RustVersion>)],
705-
pkg_msrv: &RustVersion,
697+
pkg_msrv: &PartialVersion,
706698
) -> Option<&'s Summary> {
707699
msrvs
708700
.iter()
709701
.filter(|(_, dep_msrv)| {
710702
dep_msrv
711703
.as_ref()
712-
.map(|dep_msrv| pkg_msrv >= *dep_msrv)
704+
.map(|dep_msrv| dep_msrv.is_compatible_with(pkg_msrv))
713705
.unwrap_or(true)
714706
})
715707
.map(|(s, _)| s)

src/cargo/ops/cargo_compile/mod.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -480,13 +480,7 @@ pub fn create_bcx<'a, 'gctx>(
480480
}
481481

482482
if honor_rust_version {
483-
// Remove any pre-release identifiers for easier comparison
484-
let rustc_version = &target_data.rustc.version;
485-
let rustc_version_untagged = semver::Version::new(
486-
rustc_version.major,
487-
rustc_version.minor,
488-
rustc_version.patch,
489-
);
483+
let rustc_version = target_data.rustc.version.clone().into();
490484

491485
let mut incompatible = Vec::new();
492486
let mut local_incompatible = false;
@@ -495,8 +489,7 @@ pub fn create_bcx<'a, 'gctx>(
495489
continue;
496490
};
497491

498-
let pkg_msrv_req = pkg_msrv.to_caret_req();
499-
if pkg_msrv_req.matches(&rustc_version_untagged) {
492+
if pkg_msrv.is_compatible_with(&rustc_version) {
500493
continue;
501494
}
502495

src/cargo/ops/cargo_install.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::{drop_println, ops};
1515

1616
use anyhow::{bail, Context as _};
1717
use cargo_util::paths;
18+
use cargo_util_schemas::core::PartialVersion;
1819
use itertools::Itertools;
1920
use semver::VersionReq;
2021
use tempfile::Builder as TempFileBuilder;
@@ -66,7 +67,7 @@ impl<'gctx> InstallablePackage<'gctx> {
6667
force: bool,
6768
no_track: bool,
6869
needs_update_if_source_is_index: bool,
69-
current_rust_version: Option<&semver::Version>,
70+
current_rust_version: Option<&PartialVersion>,
7071
) -> CargoResult<Option<Self>> {
7172
if let Some(name) = krate {
7273
if name == "." {
@@ -625,15 +626,7 @@ pub fn install(
625626

626627
let current_rust_version = if opts.honor_rust_version {
627628
let rustc = gctx.load_global_rustc(None)?;
628-
629-
// Remove any pre-release identifiers for easier comparison
630-
let current_version = &rustc.version;
631-
let untagged_version = semver::Version::new(
632-
current_version.major,
633-
current_version.minor,
634-
current_version.patch,
635-
);
636-
Some(untagged_version)
629+
Some(rustc.version.clone().into())
637630
} else {
638631
None
639632
};

src/cargo/ops/common_for_install_and_uninstall.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::task::Poll;
88

99
use anyhow::{bail, format_err, Context as _};
1010
use cargo_util::paths;
11+
use cargo_util_schemas::core::PartialVersion;
1112
use ops::FilterRule;
1213
use serde::{Deserialize, Serialize};
1314

@@ -569,7 +570,7 @@ pub fn select_dep_pkg<T>(
569570
dep: Dependency,
570571
gctx: &GlobalContext,
571572
needs_update: bool,
572-
current_rust_version: Option<&semver::Version>,
573+
current_rust_version: Option<&PartialVersion>,
573574
) -> CargoResult<Package>
574575
where
575576
T: Source,
@@ -596,8 +597,7 @@ where
596597
{
597598
Some(summary) => {
598599
if let (Some(current), Some(msrv)) = (current_rust_version, summary.rust_version()) {
599-
let msrv_req = msrv.to_caret_req();
600-
if !msrv_req.matches(current) {
600+
if !msrv.is_compatible_with(current) {
601601
let name = summary.name();
602602
let ver = summary.version();
603603
let extra = if dep.source_id().is_registry() {
@@ -616,7 +616,7 @@ where
616616
.filter(|summary| {
617617
summary
618618
.rust_version()
619-
.map(|msrv| msrv.to_caret_req().matches(current))
619+
.map(|msrv| msrv.is_compatible_with(current))
620620
.unwrap_or(true)
621621
})
622622
.max_by_key(|s| s.package_id())
@@ -689,7 +689,7 @@ pub fn select_pkg<T, F>(
689689
dep: Option<Dependency>,
690690
mut list_all: F,
691691
gctx: &GlobalContext,
692-
current_rust_version: Option<&semver::Version>,
692+
current_rust_version: Option<&PartialVersion>,
693693
) -> CargoResult<Package>
694694
where
695695
T: Source,

src/cargo/ops/resolve.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ use crate::util::cache_lock::CacheLockMode;
7373
use crate::util::errors::CargoResult;
7474
use crate::util::CanonicalUrl;
7575
use anyhow::Context as _;
76+
use cargo_util_schemas::manifest::RustVersion;
7677
use std::collections::{HashMap, HashSet};
7778
use tracing::{debug, trace};
7879

@@ -318,7 +319,7 @@ pub fn resolve_with_previous<'gctx>(
318319
version_prefs.version_ordering(VersionOrdering::MinimumVersionsFirst)
319320
}
320321
if ws.gctx().cli_unstable().msrv_policy {
321-
version_prefs.max_rust_version(ws.rust_version().cloned());
322+
version_prefs.max_rust_version(ws.rust_version().cloned().map(RustVersion::into_partial));
322323
}
323324

324325
// This is a set of PackageIds of `[patch]` entries, and some related locked PackageIds, for

src/cargo/util/toml/mod.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use crate::AlreadyPrintedError;
99
use anyhow::{anyhow, bail, Context as _};
1010
use cargo_platform::Platform;
1111
use cargo_util::paths;
12-
use cargo_util_schemas::core::PartialVersion;
1312
use cargo_util_schemas::manifest;
1413
use cargo_util_schemas::manifest::RustVersion;
1514
use itertools::Itertools;
@@ -581,11 +580,9 @@ pub fn to_real_manifest(
581580
.with_context(|| "failed to parse the `edition` key")?;
582581
package.edition = Some(manifest::InheritableField::Value(edition.to_string()));
583582
if let Some(pkg_msrv) = &rust_version {
584-
let pkg_msrv_req = pkg_msrv.to_caret_req();
585583
if let Some(edition_msrv) = edition.first_version() {
586-
let unsupported =
587-
semver::Version::new(edition_msrv.major, edition_msrv.minor - 1, 9999);
588-
if pkg_msrv_req.matches(&unsupported) {
584+
let edition_msrv = RustVersion::try_from(edition_msrv).unwrap();
585+
if !edition_msrv.is_compatible_with(pkg_msrv.as_partial()) {
589586
bail!(
590587
"rust-version {} is older than first version ({}) required by \
591588
the specified edition ({})",
@@ -603,9 +600,9 @@ pub fn to_real_manifest(
603600
.iter()
604601
.filter(|e| {
605602
e.first_version()
606-
.map(|edition_msrv| {
607-
let edition_msrv = PartialVersion::from(edition_msrv);
608-
edition_msrv <= **pkg_msrv
603+
.map(|e| {
604+
let e = RustVersion::try_from(e).unwrap();
605+
e.is_compatible_with(pkg_msrv.as_partial())
609606
})
610607
.unwrap_or_default()
611608
})

0 commit comments

Comments
 (0)