diff --git a/crates/cargo-util-schemas/src/manifest.rs b/crates/cargo-util-schemas/src/manifest/mod.rs similarity index 95% rename from crates/cargo-util-schemas/src/manifest.rs rename to crates/cargo-util-schemas/src/manifest/mod.rs index f6d131f533d..75f7f628bef 100644 --- a/crates/cargo-util-schemas/src/manifest.rs +++ b/crates/cargo-util-schemas/src/manifest/mod.rs @@ -16,11 +16,13 @@ use serde::{Deserialize, Serialize}; use serde_untagged::UntaggedEnumVisitor; use crate::core::PackageIdSpec; -use crate::core::PartialVersion; -use crate::core::PartialVersionError; use crate::restricted_names; +mod rust_version; + pub use crate::restricted_names::NameValidationError; +pub use rust_version::RustVersion; +pub use rust_version::RustVersionError; /// This type is used to deserialize `Cargo.toml` files. #[derive(Debug, Deserialize, Serialize)] @@ -1399,79 +1401,6 @@ pub enum TomlLintLevel { Allow, } -#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize)] -#[serde(transparent)] -pub struct RustVersion(PartialVersion); - -impl std::ops::Deref for RustVersion { - type Target = PartialVersion; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::str::FromStr for RustVersion { - type Err = RustVersionError; - - fn from_str(value: &str) -> Result { - let partial = value.parse::(); - let partial = partial.map_err(RustVersionErrorKind::PartialVersion)?; - partial.try_into() - } -} - -impl TryFrom for RustVersion { - type Error = RustVersionError; - - fn try_from(partial: PartialVersion) -> Result { - if partial.pre.is_some() { - return Err(RustVersionErrorKind::Prerelease.into()); - } - if partial.build.is_some() { - return Err(RustVersionErrorKind::BuildMetadata.into()); - } - Ok(Self(partial)) - } -} - -impl<'de> serde::Deserialize<'de> for RustVersion { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - UntaggedEnumVisitor::new() - .expecting("SemVer version") - .string(|value| value.parse().map_err(serde::de::Error::custom)) - .deserialize(deserializer) - } -} - -impl Display for RustVersion { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -/// Error parsing a [`RustVersion`]. -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct RustVersionError(#[from] RustVersionErrorKind); - -/// Non-public error kind for [`RustVersionError`]. -#[non_exhaustive] -#[derive(Debug, thiserror::Error)] -enum RustVersionErrorKind { - #[error("unexpected prerelease field, expected a version like \"1.32\"")] - Prerelease, - - #[error("unexpected build field, expected a version like \"1.32\"")] - BuildMetadata, - - #[error(transparent)] - PartialVersion(#[from] PartialVersionError), -} - #[derive(Copy, Clone, Debug)] pub struct InvalidCargoFeatures {} diff --git a/crates/cargo-util-schemas/src/manifest/rust_version.rs b/crates/cargo-util-schemas/src/manifest/rust_version.rs new file mode 100644 index 00000000000..03ba94b3e71 --- /dev/null +++ b/crates/cargo-util-schemas/src/manifest/rust_version.rs @@ -0,0 +1,173 @@ +use std::fmt; +use std::fmt::Display; + +use serde_untagged::UntaggedEnumVisitor; + +use crate::core::PartialVersion; +use crate::core::PartialVersionError; + +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize)] +#[serde(transparent)] +pub struct RustVersion(PartialVersion); + +impl RustVersion { + pub fn is_compatible_with(&self, rustc: &PartialVersion) -> bool { + let msrv = self.0.to_caret_req(); + // Remove any pre-release identifiers for easier comparison + let rustc = semver::Version { + major: rustc.major, + minor: rustc.minor.unwrap_or_default(), + patch: rustc.patch.unwrap_or_default(), + pre: Default::default(), + build: Default::default(), + }; + msrv.matches(&rustc) + } + + pub fn into_partial(self) -> PartialVersion { + self.0 + } + + pub fn as_partial(&self) -> &PartialVersion { + &self.0 + } +} + +impl std::str::FromStr for RustVersion { + type Err = RustVersionError; + + fn from_str(value: &str) -> Result { + let partial = value.parse::(); + let partial = partial.map_err(RustVersionErrorKind::PartialVersion)?; + partial.try_into() + } +} + +impl TryFrom for RustVersion { + type Error = RustVersionError; + + fn try_from(version: semver::Version) -> Result { + let version = PartialVersion::from(version); + Self::try_from(version) + } +} + +impl TryFrom for RustVersion { + type Error = RustVersionError; + + fn try_from(partial: PartialVersion) -> Result { + if partial.pre.is_some() { + return Err(RustVersionErrorKind::Prerelease.into()); + } + if partial.build.is_some() { + return Err(RustVersionErrorKind::BuildMetadata.into()); + } + Ok(Self(partial)) + } +} + +impl<'de> serde::Deserialize<'de> for RustVersion { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .expecting("SemVer version") + .string(|value| value.parse().map_err(serde::de::Error::custom)) + .deserialize(deserializer) + } +} + +impl Display for RustVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +/// Error parsing a [`RustVersion`]. +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct RustVersionError(#[from] RustVersionErrorKind); + +/// Non-public error kind for [`RustVersionError`]. +#[non_exhaustive] +#[derive(Debug, thiserror::Error)] +enum RustVersionErrorKind { + #[error("unexpected prerelease field, expected a version like \"1.32\"")] + Prerelease, + + #[error("unexpected build field, expected a version like \"1.32\"")] + BuildMetadata, + + #[error(transparent)] + PartialVersion(#[from] PartialVersionError), +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn is_compatible_with_rustc() { + let cases = &[ + ("1", "1.70.0", true), + ("1.30", "1.70.0", true), + ("1.30.10", "1.70.0", true), + ("1.70", "1.70.0", true), + ("1.70.0", "1.70.0", true), + ("1.70.1", "1.70.0", false), + ("1.70", "1.70.0-nightly", true), + ("1.70.0", "1.70.0-nightly", true), + ("1.71", "1.70.0", false), + ("2", "1.70.0", false), + ]; + let mut passed = true; + for (msrv, rustc, expected) in cases { + let msrv: RustVersion = msrv.parse().unwrap(); + let rustc = PartialVersion::from(semver::Version::parse(rustc).unwrap()); + if msrv.is_compatible_with(&rustc) != *expected { + println!("failed: {msrv} is_compatible_with {rustc} == {expected}"); + passed = false; + } + } + assert!(passed); + } + + #[test] + fn is_compatible_with_workspace_msrv() { + let cases = &[ + ("1", "1", true), + ("1", "1.70", true), + ("1", "1.70.0", true), + ("1.30", "1", false), + ("1.30", "1.70", true), + ("1.30", "1.70.0", true), + ("1.30.10", "1", false), + ("1.30.10", "1.70", true), + ("1.30.10", "1.70.0", true), + ("1.70", "1", false), + ("1.70", "1.70", true), + ("1.70", "1.70.0", true), + ("1.70.0", "1", false), + ("1.70.0", "1.70", true), + ("1.70.0", "1.70.0", true), + ("1.70.1", "1", false), + ("1.70.1", "1.70", false), + ("1.70.1", "1.70.0", false), + ("1.71", "1", false), + ("1.71", "1.70", false), + ("1.71", "1.70.0", false), + ("2", "1.70.0", false), + ]; + let mut passed = true; + for (dep_msrv, ws_msrv, expected) in cases { + let dep_msrv: RustVersion = dep_msrv.parse().unwrap(); + let ws_msrv = ws_msrv.parse::().unwrap().into_partial(); + if dep_msrv.is_compatible_with(&ws_msrv) != *expected { + println!("failed: {dep_msrv} is_compatible_with {ws_msrv} == {expected}"); + passed = false; + } + } + assert!(passed); + } +} diff --git a/src/cargo/core/resolver/version_prefs.rs b/src/cargo/core/resolver/version_prefs.rs index 2d78f30862d..5e6cc230ffb 100644 --- a/src/cargo/core/resolver/version_prefs.rs +++ b/src/cargo/core/resolver/version_prefs.rs @@ -4,7 +4,7 @@ use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; -use cargo_util_schemas::manifest::RustVersion; +use cargo_util_schemas::core::PartialVersion; use crate::core::{Dependency, PackageId, Summary}; use crate::util::interning::InternedString; @@ -21,7 +21,7 @@ pub struct VersionPreferences { try_to_use: HashSet, prefer_patch_deps: HashMap>, version_ordering: VersionOrdering, - max_rust_version: Option, + max_rust_version: Option, } #[derive(Copy, Clone, Default, PartialEq, Eq, Hash, Debug)] @@ -49,7 +49,7 @@ impl VersionPreferences { self.version_ordering = ordering; } - pub fn max_rust_version(&mut self, ver: Option) { + pub fn max_rust_version(&mut self, ver: Option) { self.max_rust_version = ver; } @@ -92,8 +92,8 @@ impl VersionPreferences { (Some(a), Some(b)) if a == b => {} // Primary comparison (Some(a), Some(b)) => { - let a_is_compat = a <= max_rust_version; - let b_is_compat = b <= max_rust_version; + let a_is_compat = a.is_compatible_with(max_rust_version); + let b_is_compat = b.is_compatible_with(max_rust_version); match (a_is_compat, b_is_compat) { (true, true) => {} // fallback (false, false) => {} // fallback @@ -103,14 +103,14 @@ impl VersionPreferences { } // Prioritize `None` over incompatible (None, Some(b)) => { - if b <= max_rust_version { + if b.is_compatible_with(max_rust_version) { return Ordering::Greater; } else { return Ordering::Less; } } (Some(a), None) => { - if a <= max_rust_version { + if a.is_compatible_with(max_rust_version) { return Ordering::Less; } else { return Ordering::Greater; diff --git a/src/cargo/ops/cargo_add/mod.rs b/src/cargo/ops/cargo_add/mod.rs index eeebddaf873..61b12c7ac66 100644 --- a/src/cargo/ops/cargo_add/mod.rs +++ b/src/cargo/ops/cargo_add/mod.rs @@ -619,21 +619,13 @@ fn get_latest_dependency( let (req_msrv, is_msrv) = spec .rust_version() .cloned() - .map(|msrv| CargoResult::Ok((msrv.clone(), true))) + .map(|msrv| CargoResult::Ok((msrv.clone().into_partial(), true))) .unwrap_or_else(|| { let rustc = gctx.load_global_rustc(None)?; // Remove any pre-release identifiers for easier comparison - let current_version = &rustc.version; - let untagged_version = RustVersion::try_from(PartialVersion { - major: current_version.major, - minor: Some(current_version.minor), - patch: Some(current_version.patch), - pre: None, - build: None, - }) - .unwrap(); - Ok((untagged_version, false)) + let rustc_version = rustc.version.clone().into(); + Ok((rustc_version, false)) })?; let msrvs = possibilities @@ -702,11 +694,16 @@ ignoring {dependency}@{latest_version} (which requires rustc {latest_rust_versio /// - `msrvs` is sorted by version fn latest_compatible<'s>( msrvs: &[(&'s Summary, Option<&RustVersion>)], - req_msrv: &RustVersion, + pkg_msrv: &PartialVersion, ) -> Option<&'s Summary> { msrvs .iter() - .filter(|(_, v)| v.as_ref().map(|msrv| req_msrv >= *msrv).unwrap_or(true)) + .filter(|(_, dep_msrv)| { + dep_msrv + .as_ref() + .map(|dep_msrv| dep_msrv.is_compatible_with(pkg_msrv)) + .unwrap_or(true) + }) .map(|(s, _)| s) .last() .copied() diff --git a/src/cargo/ops/cargo_compile/mod.rs b/src/cargo/ops/cargo_compile/mod.rs index 4aaf4878712..d413672e243 100644 --- a/src/cargo/ops/cargo_compile/mod.rs +++ b/src/cargo/ops/cargo_compile/mod.rs @@ -480,35 +480,28 @@ pub fn create_bcx<'a, 'gctx>( } if honor_rust_version { - // Remove any pre-release identifiers for easier comparison - let current_version = &target_data.rustc.version; - let untagged_version = semver::Version::new( - current_version.major, - current_version.minor, - current_version.patch, - ); + let rustc_version = target_data.rustc.version.clone().into(); let mut incompatible = Vec::new(); let mut local_incompatible = false; for unit in unit_graph.keys() { - let Some(version) = unit.pkg.rust_version() else { + let Some(pkg_msrv) = unit.pkg.rust_version() else { continue; }; - let req = version.to_caret_req(); - if req.matches(&untagged_version) { + if pkg_msrv.is_compatible_with(&rustc_version) { continue; } local_incompatible |= unit.is_local(); - incompatible.push((unit, version)); + incompatible.push((unit, pkg_msrv)); } if !incompatible.is_empty() { use std::fmt::Write as _; let plural = if incompatible.len() == 1 { "" } else { "s" }; let mut message = format!( - "rustc {current_version} is not supported by the following package{plural}:\n" + "rustc {rustc_version} is not supported by the following package{plural}:\n" ); incompatible.sort_by_key(|(unit, _)| (unit.pkg.name(), unit.pkg.version())); for (unit, msrv) in incompatible { @@ -529,7 +522,7 @@ pub fn create_bcx<'a, 'gctx>( &mut message, "Either upgrade rustc or select compatible dependency versions with `cargo update @ --precise ` -where `` is the latest version supporting rustc {current_version}", +where `` is the latest version supporting rustc {rustc_version}", ) .unwrap(); } diff --git a/src/cargo/ops/cargo_install.rs b/src/cargo/ops/cargo_install.rs index ced0ea932b1..4e51ef30ea4 100644 --- a/src/cargo/ops/cargo_install.rs +++ b/src/cargo/ops/cargo_install.rs @@ -15,6 +15,7 @@ use crate::{drop_println, ops}; use anyhow::{bail, Context as _}; use cargo_util::paths; +use cargo_util_schemas::core::PartialVersion; use itertools::Itertools; use semver::VersionReq; use tempfile::Builder as TempFileBuilder; @@ -66,7 +67,7 @@ impl<'gctx> InstallablePackage<'gctx> { force: bool, no_track: bool, needs_update_if_source_is_index: bool, - current_rust_version: Option<&semver::Version>, + current_rust_version: Option<&PartialVersion>, ) -> CargoResult> { if let Some(name) = krate { if name == "." { @@ -625,15 +626,7 @@ pub fn install( let current_rust_version = if opts.honor_rust_version { let rustc = gctx.load_global_rustc(None)?; - - // Remove any pre-release identifiers for easier comparison - let current_version = &rustc.version; - let untagged_version = semver::Version::new( - current_version.major, - current_version.minor, - current_version.patch, - ); - Some(untagged_version) + Some(rustc.version.clone().into()) } else { None }; diff --git a/src/cargo/ops/common_for_install_and_uninstall.rs b/src/cargo/ops/common_for_install_and_uninstall.rs index 89705b3548b..c8d35ba6c56 100644 --- a/src/cargo/ops/common_for_install_and_uninstall.rs +++ b/src/cargo/ops/common_for_install_and_uninstall.rs @@ -8,6 +8,7 @@ use std::task::Poll; use anyhow::{bail, format_err, Context as _}; use cargo_util::paths; +use cargo_util_schemas::core::PartialVersion; use ops::FilterRule; use serde::{Deserialize, Serialize}; @@ -569,7 +570,7 @@ pub fn select_dep_pkg( dep: Dependency, gctx: &GlobalContext, needs_update: bool, - current_rust_version: Option<&semver::Version>, + current_rust_version: Option<&PartialVersion>, ) -> CargoResult where T: Source, @@ -596,8 +597,7 @@ where { Some(summary) => { if let (Some(current), Some(msrv)) = (current_rust_version, summary.rust_version()) { - let msrv_req = msrv.to_caret_req(); - if !msrv_req.matches(current) { + if !msrv.is_compatible_with(current) { let name = summary.name(); let ver = summary.version(); let extra = if dep.source_id().is_registry() { @@ -616,7 +616,7 @@ where .filter(|summary| { summary .rust_version() - .map(|msrv| msrv.to_caret_req().matches(current)) + .map(|msrv| msrv.is_compatible_with(current)) .unwrap_or(true) }) .max_by_key(|s| s.package_id()) @@ -689,7 +689,7 @@ pub fn select_pkg( dep: Option, mut list_all: F, gctx: &GlobalContext, - current_rust_version: Option<&semver::Version>, + current_rust_version: Option<&PartialVersion>, ) -> CargoResult where T: Source, diff --git a/src/cargo/ops/resolve.rs b/src/cargo/ops/resolve.rs index f9e036b2e05..71b386df3fe 100644 --- a/src/cargo/ops/resolve.rs +++ b/src/cargo/ops/resolve.rs @@ -73,6 +73,7 @@ use crate::util::cache_lock::CacheLockMode; use crate::util::errors::CargoResult; use crate::util::CanonicalUrl; use anyhow::Context as _; +use cargo_util_schemas::manifest::RustVersion; use std::collections::{HashMap, HashSet}; use tracing::{debug, trace}; @@ -318,7 +319,7 @@ pub fn resolve_with_previous<'gctx>( version_prefs.version_ordering(VersionOrdering::MinimumVersionsFirst) } if ws.gctx().cli_unstable().msrv_policy { - version_prefs.max_rust_version(ws.rust_version().cloned()); + version_prefs.max_rust_version(ws.rust_version().cloned().map(RustVersion::into_partial)); } // This is a set of PackageIds of `[patch]` entries, and some related locked PackageIds, for diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 1a01d59bd08..80910aecd36 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -9,7 +9,6 @@ use crate::AlreadyPrintedError; use anyhow::{anyhow, bail, Context as _}; use cargo_platform::Platform; use cargo_util::paths; -use cargo_util_schemas::core::PartialVersion; use cargo_util_schemas::manifest; use cargo_util_schemas::manifest::RustVersion; use itertools::Itertools; @@ -580,17 +579,15 @@ pub fn to_real_manifest( .parse() .with_context(|| "failed to parse the `edition` key")?; package.edition = Some(manifest::InheritableField::Value(edition.to_string())); - if let Some(rust_version) = &rust_version { - let req = rust_version.to_caret_req(); - if let Some(first_version) = edition.first_version() { - let unsupported = - semver::Version::new(first_version.major, first_version.minor - 1, 9999); - if req.matches(&unsupported) { + if let Some(pkg_msrv) = &rust_version { + if let Some(edition_msrv) = edition.first_version() { + let edition_msrv = RustVersion::try_from(edition_msrv).unwrap(); + if !edition_msrv.is_compatible_with(pkg_msrv.as_partial()) { bail!( "rust-version {} is older than first version ({}) required by \ the specified edition ({})", - rust_version, - first_version, + pkg_msrv, + edition_msrv, edition, ) } @@ -598,14 +595,14 @@ pub fn to_real_manifest( } edition } else { - let msrv_edition = if let Some(rust_version) = &rust_version { + let msrv_edition = if let Some(pkg_msrv) = &rust_version { Edition::ALL .iter() .filter(|e| { e.first_version() .map(|e| { - let e = PartialVersion::from(e); - e <= **rust_version + let e = RustVersion::try_from(e).unwrap(); + e.is_compatible_with(pkg_msrv.as_partial()) }) .unwrap_or_default() }) diff --git a/triagebot.toml b/triagebot.toml index ab134f19721..b659dee749d 100644 --- a/triagebot.toml +++ b/triagebot.toml @@ -171,7 +171,7 @@ trigger_files = ["src/cargo/core/compiler/lto.rs"] [autolabel."A-manifest"] trigger_files = [ - "crates/cargo-util-schemas/src/manifest.rs", + "crates/cargo-util-schemas/src/manifest/", "src/cargo/core/manifest.rs", "src/cargo/util/toml/mod.rs", "src/cargo/util/toml_mut/",