Skip to content

Commit 29cf016

Browse files
committed
Auto merge of #13537 - epage:msrv-compat, r=Eh2406
fix: Consistently compare MSRVs ### What does this PR try to resolve? Currently, we use several strategies to evaluate an MSRV - Relying in `impl Ord for RustVersion` (`dep_msrv <= pkg_msrv`) - 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 ### How should we test and review this PR? Refactors are split out I didn't go through and verify if or how the different approaches varied in behavior, instead consolidating on the one, so only unit tests around the consolidated behavior were added rather than trying to hit all of the corner cases within the various ways `RustVersion` is used. ### Additional information
2 parents 36cf66a + 134ed93 commit 29cf016

File tree

10 files changed

+220
-137
lines changed

10 files changed

+220
-137
lines changed

crates/cargo-util-schemas/src/manifest.rs renamed to crates/cargo-util-schemas/src/manifest/mod.rs

+4-75
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ use serde::{Deserialize, Serialize};
1616
use serde_untagged::UntaggedEnumVisitor;
1717

1818
use crate::core::PackageIdSpec;
19-
use crate::core::PartialVersion;
20-
use crate::core::PartialVersionError;
2119
use crate::restricted_names;
2220

21+
mod rust_version;
22+
2323
pub use crate::restricted_names::NameValidationError;
24+
pub use rust_version::RustVersion;
25+
pub use rust_version::RustVersionError;
2426

2527
/// This type is used to deserialize `Cargo.toml` files.
2628
#[derive(Debug, Deserialize, Serialize)]
@@ -1399,79 +1401,6 @@ pub enum TomlLintLevel {
13991401
Allow,
14001402
}
14011403

1402-
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize)]
1403-
#[serde(transparent)]
1404-
pub struct RustVersion(PartialVersion);
1405-
1406-
impl std::ops::Deref for RustVersion {
1407-
type Target = PartialVersion;
1408-
1409-
fn deref(&self) -> &Self::Target {
1410-
&self.0
1411-
}
1412-
}
1413-
1414-
impl std::str::FromStr for RustVersion {
1415-
type Err = RustVersionError;
1416-
1417-
fn from_str(value: &str) -> Result<Self, Self::Err> {
1418-
let partial = value.parse::<PartialVersion>();
1419-
let partial = partial.map_err(RustVersionErrorKind::PartialVersion)?;
1420-
partial.try_into()
1421-
}
1422-
}
1423-
1424-
impl TryFrom<PartialVersion> for RustVersion {
1425-
type Error = RustVersionError;
1426-
1427-
fn try_from(partial: PartialVersion) -> Result<Self, Self::Error> {
1428-
if partial.pre.is_some() {
1429-
return Err(RustVersionErrorKind::Prerelease.into());
1430-
}
1431-
if partial.build.is_some() {
1432-
return Err(RustVersionErrorKind::BuildMetadata.into());
1433-
}
1434-
Ok(Self(partial))
1435-
}
1436-
}
1437-
1438-
impl<'de> serde::Deserialize<'de> for RustVersion {
1439-
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1440-
where
1441-
D: serde::Deserializer<'de>,
1442-
{
1443-
UntaggedEnumVisitor::new()
1444-
.expecting("SemVer version")
1445-
.string(|value| value.parse().map_err(serde::de::Error::custom))
1446-
.deserialize(deserializer)
1447-
}
1448-
}
1449-
1450-
impl Display for RustVersion {
1451-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1452-
self.0.fmt(f)
1453-
}
1454-
}
1455-
1456-
/// Error parsing a [`RustVersion`].
1457-
#[derive(Debug, thiserror::Error)]
1458-
#[error(transparent)]
1459-
pub struct RustVersionError(#[from] RustVersionErrorKind);
1460-
1461-
/// Non-public error kind for [`RustVersionError`].
1462-
#[non_exhaustive]
1463-
#[derive(Debug, thiserror::Error)]
1464-
enum RustVersionErrorKind {
1465-
#[error("unexpected prerelease field, expected a version like \"1.32\"")]
1466-
Prerelease,
1467-
1468-
#[error("unexpected build field, expected a version like \"1.32\"")]
1469-
BuildMetadata,
1470-
1471-
#[error(transparent)]
1472-
PartialVersion(#[from] PartialVersionError),
1473-
}
1474-
14751404
#[derive(Copy, Clone, Debug)]
14761405
pub struct InvalidCargoFeatures {}
14771406

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
use std::fmt;
2+
use std::fmt::Display;
3+
4+
use serde_untagged::UntaggedEnumVisitor;
5+
6+
use crate::core::PartialVersion;
7+
use crate::core::PartialVersionError;
8+
9+
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize)]
10+
#[serde(transparent)]
11+
pub struct RustVersion(PartialVersion);
12+
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+
}
30+
31+
pub fn as_partial(&self) -> &PartialVersion {
32+
&self.0
33+
}
34+
}
35+
36+
impl std::str::FromStr for RustVersion {
37+
type Err = RustVersionError;
38+
39+
fn from_str(value: &str) -> Result<Self, Self::Err> {
40+
let partial = value.parse::<PartialVersion>();
41+
let partial = partial.map_err(RustVersionErrorKind::PartialVersion)?;
42+
partial.try_into()
43+
}
44+
}
45+
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+
55+
impl TryFrom<PartialVersion> for RustVersion {
56+
type Error = RustVersionError;
57+
58+
fn try_from(partial: PartialVersion) -> Result<Self, Self::Error> {
59+
if partial.pre.is_some() {
60+
return Err(RustVersionErrorKind::Prerelease.into());
61+
}
62+
if partial.build.is_some() {
63+
return Err(RustVersionErrorKind::BuildMetadata.into());
64+
}
65+
Ok(Self(partial))
66+
}
67+
}
68+
69+
impl<'de> serde::Deserialize<'de> for RustVersion {
70+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
71+
where
72+
D: serde::Deserializer<'de>,
73+
{
74+
UntaggedEnumVisitor::new()
75+
.expecting("SemVer version")
76+
.string(|value| value.parse().map_err(serde::de::Error::custom))
77+
.deserialize(deserializer)
78+
}
79+
}
80+
81+
impl Display for RustVersion {
82+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83+
self.0.fmt(f)
84+
}
85+
}
86+
87+
/// Error parsing a [`RustVersion`].
88+
#[derive(Debug, thiserror::Error)]
89+
#[error(transparent)]
90+
pub struct RustVersionError(#[from] RustVersionErrorKind);
91+
92+
/// Non-public error kind for [`RustVersionError`].
93+
#[non_exhaustive]
94+
#[derive(Debug, thiserror::Error)]
95+
enum RustVersionErrorKind {
96+
#[error("unexpected prerelease field, expected a version like \"1.32\"")]
97+
Prerelease,
98+
99+
#[error("unexpected build field, expected a version like \"1.32\"")]
100+
BuildMetadata,
101+
102+
#[error(transparent)]
103+
PartialVersion(#[from] PartialVersionError),
104+
}
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

+7-7
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

+10-13
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,11 +694,16 @@ 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-
req_msrv: &RustVersion,
697+
pkg_msrv: &PartialVersion,
706698
) -> Option<&'s Summary> {
707699
msrvs
708700
.iter()
709-
.filter(|(_, v)| v.as_ref().map(|msrv| req_msrv >= *msrv).unwrap_or(true))
701+
.filter(|(_, dep_msrv)| {
702+
dep_msrv
703+
.as_ref()
704+
.map(|dep_msrv| dep_msrv.is_compatible_with(pkg_msrv))
705+
.unwrap_or(true)
706+
})
710707
.map(|(s, _)| s)
711708
.last()
712709
.copied()

0 commit comments

Comments
 (0)