diff --git a/src/bin/cargo/commands/install.rs b/src/bin/cargo/commands/install.rs index 143ae517cff..cb66ba100c1 100644 --- a/src/bin/cargo/commands/install.rs +++ b/src/bin/cargo/commands/install.rs @@ -6,7 +6,7 @@ use anyhow::format_err; use cargo::core::{GitReference, SourceId, Workspace}; use cargo::ops; use cargo::util::IntoUrl; -use cargo::util::VersionExt; +use cargo::util_semver::VersionExt; use cargo::CargoResult; use itertools::Itertools; use semver::VersionReq; diff --git a/src/cargo/core/package_id_spec.rs b/src/cargo/core/package_id_spec.rs index 457166b3710..d3f0abc465e 100644 --- a/src/cargo/core/package_id_spec.rs +++ b/src/cargo/core/package_id_spec.rs @@ -9,8 +9,8 @@ use url::Url; use crate::core::PackageId; use crate::util::edit_distance; use crate::util::errors::CargoResult; -use crate::util::PartialVersion; use crate::util::{validate_package_name, IntoUrl}; +use crate::util_semver::PartialVersion; /// Some or all of the data required to identify a package: /// diff --git a/src/cargo/core/resolver/errors.rs b/src/cargo/core/resolver/errors.rs index b57a7c3ebf4..15a006ffba0 100644 --- a/src/cargo/core/resolver/errors.rs +++ b/src/cargo/core/resolver/errors.rs @@ -4,7 +4,8 @@ use std::task::Poll; use crate::core::{Dependency, PackageId, Registry, Summary}; use crate::sources::source::QueryKind; use crate::util::edit_distance::edit_distance; -use crate::util::{Config, OptVersionReq, VersionExt}; +use crate::util::{Config, OptVersionReq}; +use crate::util_semver::VersionExt; use anyhow::Error; use super::context::Context; diff --git a/src/cargo/lib.rs b/src/cargo/lib.rs index ce1c899dff6..6947642c9ce 100644 --- a/src/cargo/lib.rs +++ b/src/cargo/lib.rs @@ -161,6 +161,7 @@ pub mod core; pub mod ops; pub mod sources; pub mod util; +pub mod util_semver; mod version; pub fn exit_with_error(err: CliError, shell: &mut Shell) -> ! { diff --git a/src/cargo/util/mod.rs b/src/cargo/util/mod.rs index 8c4d0339a20..fb4c4b39c1e 100644 --- a/src/cargo/util/mod.rs +++ b/src/cargo/util/mod.rs @@ -23,7 +23,7 @@ pub use self::progress::{Progress, ProgressStyle}; pub use self::queue::Queue; pub use self::restricted_names::validate_package_name; pub use self::rustc::Rustc; -pub use self::semver_ext::{OptVersionReq, PartialVersion, RustVersion, VersionExt}; +pub use self::semver_ext::{OptVersionReq, RustVersion}; pub use self::vcs::{existing_vcs_repo, FossilRepo, GitRepo, HgRepo, PijulRepo}; pub use self::workspace::{ add_path_args, path_args, print_available_benches, print_available_binaries, diff --git a/src/cargo/util/semver_ext.rs b/src/cargo/util/semver_ext.rs index 475abacc578..561cf140ed6 100644 --- a/src/cargo/util/semver_ext.rs +++ b/src/cargo/util/semver_ext.rs @@ -1,7 +1,11 @@ -use semver::{Comparator, Op, Version, VersionReq}; -use serde_untagged::UntaggedEnumVisitor; use std::fmt::{self, Display}; +use semver::{Op, Version, VersionReq}; +use serde_untagged::UntaggedEnumVisitor; + +use crate::util_semver::PartialVersion; +use crate::util_semver::VersionExt as _; + #[derive(PartialEq, Eq, Hash, Clone, Debug)] pub enum OptVersionReq { Any, @@ -12,30 +16,6 @@ pub enum OptVersionReq { UpdatePrecise(Version, VersionReq), } -pub trait VersionExt { - fn is_prerelease(&self) -> bool; - - fn to_exact_req(&self) -> VersionReq; -} - -impl VersionExt for Version { - fn is_prerelease(&self) -> bool { - !self.pre.is_empty() - } - - fn to_exact_req(&self) -> VersionReq { - VersionReq { - comparators: vec![Comparator { - op: Op::Exact, - major: self.major, - minor: Some(self.minor), - patch: Some(self.patch), - pre: self.pre.clone(), - }], - } - } -} - impl OptVersionReq { pub fn exact(version: &Version) -> Self { OptVersionReq::Req(version.to_exact_req()) @@ -190,170 +170,3 @@ impl Display for RustVersion { self.0.fmt(f) } } - -#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] -pub struct PartialVersion { - pub major: u64, - pub minor: Option, - pub patch: Option, - pub pre: Option, - pub build: Option, -} - -impl PartialVersion { - pub fn to_version(&self) -> Option { - Some(Version { - major: self.major, - minor: self.minor?, - patch: self.patch?, - pre: self.pre.clone().unwrap_or_default(), - build: self.build.clone().unwrap_or_default(), - }) - } - - pub fn to_caret_req(&self) -> VersionReq { - VersionReq { - comparators: vec![Comparator { - op: semver::Op::Caret, - major: self.major, - minor: self.minor, - patch: self.patch, - pre: self.pre.as_ref().cloned().unwrap_or_default(), - }], - } - } - - /// Check if this matches a version, including build metadata - /// - /// Build metadata does not affect version precedence but may be necessary for uniquely - /// identifying a package. - pub fn matches(&self, version: &Version) -> bool { - if !version.pre.is_empty() && self.pre.is_none() { - // Pre-release versions must be explicitly opted into, if for no other reason than to - // give us room to figure out and define the semantics - return false; - } - self.major == version.major - && self.minor.map(|f| f == version.minor).unwrap_or(true) - && self.patch.map(|f| f == version.patch).unwrap_or(true) - && self.pre.as_ref().map(|f| f == &version.pre).unwrap_or(true) - && self - .build - .as_ref() - .map(|f| f == &version.build) - .unwrap_or(true) - } -} - -impl From for PartialVersion { - fn from(ver: semver::Version) -> Self { - let pre = if ver.pre.is_empty() { - None - } else { - Some(ver.pre) - }; - let build = if ver.build.is_empty() { - None - } else { - Some(ver.build) - }; - Self { - major: ver.major, - minor: Some(ver.minor), - patch: Some(ver.patch), - pre, - build, - } - } -} - -impl std::str::FromStr for PartialVersion { - type Err = anyhow::Error; - - fn from_str(value: &str) -> Result { - if is_req(value) { - anyhow::bail!("unexpected version requirement, expected a version like \"1.32\"") - } - match semver::Version::parse(value) { - Ok(ver) => Ok(ver.into()), - Err(_) => { - // HACK: Leverage `VersionReq` for partial version parsing - let mut version_req = match semver::VersionReq::parse(value) { - Ok(req) => req, - Err(_) if value.contains('-') => { - anyhow::bail!( - "unexpected prerelease field, expected a version like \"1.32\"" - ) - } - Err(_) if value.contains('+') => { - anyhow::bail!("unexpected build field, expected a version like \"1.32\"") - } - Err(_) => anyhow::bail!("expected a version like \"1.32\""), - }; - assert_eq!(version_req.comparators.len(), 1, "guaranteed by is_req"); - let comp = version_req.comparators.pop().unwrap(); - assert_eq!(comp.op, semver::Op::Caret, "guaranteed by is_req"); - let pre = if comp.pre.is_empty() { - None - } else { - Some(comp.pre) - }; - Ok(Self { - major: comp.major, - minor: comp.minor, - patch: comp.patch, - pre, - build: None, - }) - } - } - } -} - -impl Display for PartialVersion { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let major = self.major; - write!(f, "{major}")?; - if let Some(minor) = self.minor { - write!(f, ".{minor}")?; - } - if let Some(patch) = self.patch { - write!(f, ".{patch}")?; - } - if let Some(pre) = self.pre.as_ref() { - write!(f, "-{pre}")?; - } - if let Some(build) = self.build.as_ref() { - write!(f, "+{build}")?; - } - Ok(()) - } -} - -impl serde::Serialize for PartialVersion { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.collect_str(self) - } -} - -impl<'de> serde::Deserialize<'de> for PartialVersion { - 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) - } -} - -fn is_req(value: &str) -> bool { - let Some(first) = value.chars().next() else { - return false; - }; - "<>=^~".contains(first) || value.contains('*') || value.contains(',') -} diff --git a/src/cargo/util_semver.rs b/src/cargo/util_semver.rs new file mode 100644 index 00000000000..a84c9ee582e --- /dev/null +++ b/src/cargo/util_semver.rs @@ -0,0 +1,195 @@ +use std::fmt::{self, Display}; + +use semver::{Comparator, Op, Version, VersionReq}; +use serde_untagged::UntaggedEnumVisitor; + +pub trait VersionExt { + fn is_prerelease(&self) -> bool; + + fn to_exact_req(&self) -> VersionReq; +} + +impl VersionExt for Version { + fn is_prerelease(&self) -> bool { + !self.pre.is_empty() + } + + fn to_exact_req(&self) -> VersionReq { + VersionReq { + comparators: vec![Comparator { + op: Op::Exact, + major: self.major, + minor: Some(self.minor), + patch: Some(self.patch), + pre: self.pre.clone(), + }], + } + } +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] +pub struct PartialVersion { + pub major: u64, + pub minor: Option, + pub patch: Option, + pub pre: Option, + pub build: Option, +} + +impl PartialVersion { + pub fn to_version(&self) -> Option { + Some(Version { + major: self.major, + minor: self.minor?, + patch: self.patch?, + pre: self.pre.clone().unwrap_or_default(), + build: self.build.clone().unwrap_or_default(), + }) + } + + pub fn to_caret_req(&self) -> VersionReq { + VersionReq { + comparators: vec![Comparator { + op: semver::Op::Caret, + major: self.major, + minor: self.minor, + patch: self.patch, + pre: self.pre.as_ref().cloned().unwrap_or_default(), + }], + } + } + + /// Check if this matches a version, including build metadata + /// + /// Build metadata does not affect version precedence but may be necessary for uniquely + /// identifying a package. + pub fn matches(&self, version: &Version) -> bool { + if !version.pre.is_empty() && self.pre.is_none() { + // Pre-release versions must be explicitly opted into, if for no other reason than to + // give us room to figure out and define the semantics + return false; + } + self.major == version.major + && self.minor.map(|f| f == version.minor).unwrap_or(true) + && self.patch.map(|f| f == version.patch).unwrap_or(true) + && self.pre.as_ref().map(|f| f == &version.pre).unwrap_or(true) + && self + .build + .as_ref() + .map(|f| f == &version.build) + .unwrap_or(true) + } +} + +impl From for PartialVersion { + fn from(ver: semver::Version) -> Self { + let pre = if ver.pre.is_empty() { + None + } else { + Some(ver.pre) + }; + let build = if ver.build.is_empty() { + None + } else { + Some(ver.build) + }; + Self { + major: ver.major, + minor: Some(ver.minor), + patch: Some(ver.patch), + pre, + build, + } + } +} + +impl std::str::FromStr for PartialVersion { + type Err = anyhow::Error; + + fn from_str(value: &str) -> Result { + if is_req(value) { + anyhow::bail!("unexpected version requirement, expected a version like \"1.32\"") + } + match semver::Version::parse(value) { + Ok(ver) => Ok(ver.into()), + Err(_) => { + // HACK: Leverage `VersionReq` for partial version parsing + let mut version_req = match semver::VersionReq::parse(value) { + Ok(req) => req, + Err(_) if value.contains('-') => { + anyhow::bail!( + "unexpected prerelease field, expected a version like \"1.32\"" + ) + } + Err(_) if value.contains('+') => { + anyhow::bail!("unexpected build field, expected a version like \"1.32\"") + } + Err(_) => anyhow::bail!("expected a version like \"1.32\""), + }; + assert_eq!(version_req.comparators.len(), 1, "guaranteed by is_req"); + let comp = version_req.comparators.pop().unwrap(); + assert_eq!(comp.op, semver::Op::Caret, "guaranteed by is_req"); + let pre = if comp.pre.is_empty() { + None + } else { + Some(comp.pre) + }; + Ok(Self { + major: comp.major, + minor: comp.minor, + patch: comp.patch, + pre, + build: None, + }) + } + } + } +} + +impl Display for PartialVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let major = self.major; + write!(f, "{major}")?; + if let Some(minor) = self.minor { + write!(f, ".{minor}")?; + } + if let Some(patch) = self.patch { + write!(f, ".{patch}")?; + } + if let Some(pre) = self.pre.as_ref() { + write!(f, "-{pre}")?; + } + if let Some(build) = self.build.as_ref() { + write!(f, "+{build}")?; + } + Ok(()) + } +} + +impl serde::Serialize for PartialVersion { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(self) + } +} + +impl<'de> serde::Deserialize<'de> for PartialVersion { + 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) + } +} + +fn is_req(value: &str) -> bool { + let Some(first) = value.chars().next() else { + return false; + }; + "<>=^~".contains(first) || value.contains('*') || value.contains(',') +}