Skip to content

Commit

Permalink
Auto merge of #12614 - epage:partial, r=weihanglo
Browse files Browse the repository at this point in the history
feat(pkgid): Allow incomplete versions when unambigious

### What does this PR try to resolve?

This was proposed in #12425 to help sand off some of the rough edges around `cargo update` for its wider use it would be getting.  Its easy to accidentally get duplicate copies packages in a repo and a pain to have to specify the full version when `cargo update -p foo@1` is sufficient to describe it.

Other effects
- profile overrides also supports this since we already allow a spec to match multiple items
- `cargo clean -p foo@...` already ignored the version, so now we also parse and ignore the partial version
- `cargo tree --prune` will now accept partial versions and will match all of them

Parts not effected:
- Replacements
  - Two of the cases were found and we treat it as if the version isn't present which will error, so I think that is correct

### How should we test and review this PR?

This extracts `PartialVersion` from `RustVersion` where `RustVersion` is a more specialized variant, not allowing prerelease or build.

This works by adopting `PartialVersion` into `PackageIdSpec`.  For `PackageIdSpec::query`, this will "just work".

### Additional information
bors committed Sep 16, 2023
2 parents d5336f8 + 82f9bd3 commit da498c8
Showing 22 changed files with 420 additions and 128 deletions.
10 changes: 5 additions & 5 deletions crates/resolver-tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ use cargo::core::Resolve;
use cargo::core::{Dependency, PackageId, Registry, Summary};
use cargo::core::{GitReference, SourceId};
use cargo::sources::source::QueryKind;
use cargo::util::{CargoResult, Config, Graph, IntoUrl, PartialVersion};
use cargo::util::{CargoResult, Config, Graph, IntoUrl, RustVersion};

use proptest::collection::{btree_map, vec};
use proptest::prelude::*;
@@ -185,7 +185,7 @@ pub fn resolve_with_config_raw(
deps,
&BTreeMap::new(),
None::<&String>,
None::<PartialVersion>,
None::<RustVersion>,
)
.unwrap();
let opts = ResolveOpts::everything();
@@ -588,7 +588,7 @@ pub fn pkg_dep<T: ToPkgId>(name: T, dep: Vec<Dependency>) -> Summary {
dep,
&BTreeMap::new(),
link,
None::<PartialVersion>,
None::<RustVersion>,
)
.unwrap()
}
@@ -616,7 +616,7 @@ pub fn pkg_loc(name: &str, loc: &str) -> Summary {
Vec::new(),
&BTreeMap::new(),
link,
None::<PartialVersion>,
None::<RustVersion>,
)
.unwrap()
}
@@ -630,7 +630,7 @@ pub fn remove_dep(sum: &Summary, ind: usize) -> Summary {
deps,
&BTreeMap::new(),
sum.links().map(|a| a.as_str()),
None::<PartialVersion>,
None::<RustVersion>,
)
.unwrap()
}
2 changes: 1 addition & 1 deletion src/bin/cargo/commands/remove.rs
Original file line number Diff line number Diff line change
@@ -288,7 +288,7 @@ fn spec_has_match(
}

let version_matches = match (spec.version(), dep.version()) {
(Some(v), Some(vq)) => semver::VersionReq::parse(vq)?.matches(v),
(Some(v), Some(vq)) => semver::VersionReq::parse(vq)?.matches(&v),
(Some(_), None) => false,
(None, None | Some(_)) => true,
};
12 changes: 6 additions & 6 deletions src/cargo/core/manifest.rs
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ use crate::core::{Edition, Feature, Features, WorkspaceConfig};
use crate::util::errors::*;
use crate::util::interning::InternedString;
use crate::util::toml::{TomlManifest, TomlProfiles};
use crate::util::{short_hash, Config, Filesystem, PartialVersion};
use crate::util::{short_hash, Config, Filesystem, RustVersion};

pub enum EitherManifest {
Real(Manifest),
@@ -58,7 +58,7 @@ pub struct Manifest {
original: Rc<TomlManifest>,
unstable_features: Features,
edition: Edition,
rust_version: Option<PartialVersion>,
rust_version: Option<RustVersion>,
im_a_teapot: Option<bool>,
default_run: Option<String>,
metabuild: Option<Vec<String>>,
@@ -112,7 +112,7 @@ pub struct ManifestMetadata {
pub documentation: Option<String>, // URL
pub badges: BTreeMap<String, BTreeMap<String, String>>,
pub links: Option<String>,
pub rust_version: Option<PartialVersion>,
pub rust_version: Option<RustVersion>,
}

#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
@@ -401,7 +401,7 @@ impl Manifest {
workspace: WorkspaceConfig,
unstable_features: Features,
edition: Edition,
rust_version: Option<PartialVersion>,
rust_version: Option<RustVersion>,
im_a_teapot: Option<bool>,
default_run: Option<String>,
original: Rc<TomlManifest>,
@@ -570,8 +570,8 @@ impl Manifest {
self.edition
}

pub fn rust_version(&self) -> Option<PartialVersion> {
self.rust_version
pub fn rust_version(&self) -> Option<&RustVersion> {
self.rust_version.as_ref()
}

pub fn custom_metadata(&self) -> Option<&toml::Value> {
8 changes: 4 additions & 4 deletions src/cargo/core/package.rs
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ use crate::util::network::http::http_handle_and_timeout;
use crate::util::network::http::HttpTimeout;
use crate::util::network::retry::{Retry, RetryResult};
use crate::util::network::sleep::SleepTracker;
use crate::util::PartialVersion;
use crate::util::RustVersion;
use crate::util::{self, internal, Config, Progress, ProgressStyle};

pub const MANIFEST_PREAMBLE: &str = "\
@@ -104,7 +104,7 @@ pub struct SerializedPackage {
#[serde(skip_serializing_if = "Option::is_none")]
metabuild: Option<Vec<String>>,
default_run: Option<String>,
rust_version: Option<PartialVersion>,
rust_version: Option<RustVersion>,
}

impl Package {
@@ -178,7 +178,7 @@ impl Package {
self.targets().iter().any(|target| target.proc_macro())
}
/// Gets the package's minimum Rust version.
pub fn rust_version(&self) -> Option<PartialVersion> {
pub fn rust_version(&self) -> Option<&RustVersion> {
self.manifest().rust_version()
}

@@ -263,7 +263,7 @@ impl Package {
metabuild: self.manifest().metabuild().cloned(),
publish: self.publish().as_ref().cloned(),
default_run: self.manifest().default_run().map(|s| s.to_owned()),
rust_version: self.rust_version(),
rust_version: self.rust_version().cloned(),
}
}
}
67 changes: 50 additions & 17 deletions src/cargo/core/package_id_spec.rs
Original file line number Diff line number Diff line change
@@ -10,7 +10,8 @@ use crate::core::PackageId;
use crate::util::edit_distance;
use crate::util::errors::CargoResult;
use crate::util::interning::InternedString;
use crate::util::{validate_package_name, IntoUrl, ToSemver};
use crate::util::PartialVersion;
use crate::util::{validate_package_name, IntoUrl};

/// Some or all of the data required to identify a package:
///
@@ -24,7 +25,7 @@ use crate::util::{validate_package_name, IntoUrl, ToSemver};
#[derive(Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
pub struct PackageIdSpec {
name: InternedString,
version: Option<Version>,
version: Option<PartialVersion>,
url: Option<Url>,
}

@@ -70,7 +71,7 @@ impl PackageIdSpec {
let mut parts = spec.splitn(2, [':', '@']);
let name = parts.next().unwrap();
let version = match parts.next() {
Some(version) => Some(version.to_semver()?),
Some(version) => Some(version.parse::<PartialVersion>()?),
None => None,
};
validate_package_name(name, "pkgid", "")?;
@@ -94,12 +95,12 @@ impl PackageIdSpec {
spec.query(i)
}

/// Convert a `PackageId` to a `PackageIdSpec`, which will have both the `Version` and `Url`
/// Convert a `PackageId` to a `PackageIdSpec`, which will have both the `PartialVersion` and `Url`
/// fields filled in.
pub fn from_package_id(package_id: PackageId) -> PackageIdSpec {
PackageIdSpec {
name: package_id.name(),
version: Some(package_id.version().clone()),
version: Some(package_id.version().clone().into()),
url: Some(package_id.source_id().url().clone()),
}
}
@@ -125,14 +126,14 @@ impl PackageIdSpec {
match frag {
Some(fragment) => match fragment.split_once([':', '@']) {
Some((name, part)) => {
let version = part.to_semver()?;
let version = part.parse::<PartialVersion>()?;
(InternedString::new(name), Some(version))
}
None => {
if fragment.chars().next().unwrap().is_alphabetic() {
(InternedString::new(&fragment), None)
} else {
let version = fragment.to_semver()?;
let version = fragment.parse::<PartialVersion>()?;
(InternedString::new(path_name), Some(version))
}
}
@@ -151,7 +152,12 @@ impl PackageIdSpec {
self.name
}

pub fn version(&self) -> Option<&Version> {
/// Full `semver::Version`, if present
pub fn version(&self) -> Option<Version> {
self.version.as_ref().and_then(|v| v.version())
}

pub fn partial_version(&self) -> Option<&PartialVersion> {
self.version.as_ref()
}

@@ -170,7 +176,8 @@ impl PackageIdSpec {
}

if let Some(ref v) = self.version {
if v != package_id.version() {
let req = v.exact_req();
if !req.matches(package_id.version()) {
return false;
}
}
@@ -319,7 +326,6 @@ mod tests {
use super::PackageIdSpec;
use crate::core::{PackageId, SourceId};
use crate::util::interning::InternedString;
use crate::util::ToSemver;
use url::Url;

#[test]
@@ -344,16 +350,25 @@ mod tests {
"https://crates.io/foo#1.2.3",
PackageIdSpec {
name: InternedString::new("foo"),
version: Some("1.2.3".to_semver().unwrap()),
version: Some("1.2.3".parse().unwrap()),
url: Some(Url::parse("https://crates.io/foo").unwrap()),
},
"https://crates.io/foo#1.2.3",
);
ok(
"https://crates.io/foo#1.2",
PackageIdSpec {
name: InternedString::new("foo"),
version: Some("1.2".parse().unwrap()),
url: Some(Url::parse("https://crates.io/foo").unwrap()),
},
"https://crates.io/foo#1.2",
);
ok(
"https://crates.io/foo#bar:1.2.3",
PackageIdSpec {
name: InternedString::new("bar"),
version: Some("1.2.3".to_semver().unwrap()),
version: Some("1.2.3".parse().unwrap()),
url: Some(Url::parse("https://crates.io/foo").unwrap()),
},
"https://crates.io/foo#[email protected]",
@@ -362,11 +377,20 @@ mod tests {
"https://crates.io/foo#[email protected]",
PackageIdSpec {
name: InternedString::new("bar"),
version: Some("1.2.3".to_semver().unwrap()),
version: Some("1.2.3".parse().unwrap()),
url: Some(Url::parse("https://crates.io/foo").unwrap()),
},
"https://crates.io/foo#[email protected]",
);
ok(
"https://crates.io/foo#[email protected]",
PackageIdSpec {
name: InternedString::new("bar"),
version: Some("1.2".parse().unwrap()),
url: Some(Url::parse("https://crates.io/foo").unwrap()),
},
"https://crates.io/foo#[email protected]",
);
ok(
"foo",
PackageIdSpec {
@@ -380,7 +404,7 @@ mod tests {
"foo:1.2.3",
PackageIdSpec {
name: InternedString::new("foo"),
version: Some("1.2.3".to_semver().unwrap()),
version: Some("1.2.3".parse().unwrap()),
url: None,
},
"[email protected]",
@@ -389,21 +413,29 @@ mod tests {
"[email protected]",
PackageIdSpec {
name: InternedString::new("foo"),
version: Some("1.2.3".to_semver().unwrap()),
version: Some("1.2.3".parse().unwrap()),
url: None,
},
"[email protected]",
);
ok(
"[email protected]",
PackageIdSpec {
name: InternedString::new("foo"),
version: Some("1.2".parse().unwrap()),
url: None,
},
"[email protected]",
);
}

#[test]
fn bad_parsing() {
assert!(PackageIdSpec::parse("baz:").is_err());
assert!(PackageIdSpec::parse("baz:*").is_err());
assert!(PackageIdSpec::parse("baz:1.0").is_err());
assert!(PackageIdSpec::parse("baz@").is_err());
assert!(PackageIdSpec::parse("baz@*").is_err());
assert!(PackageIdSpec::parse("[email protected]").is_err());
assert!(PackageIdSpec::parse("baz@^1.0").is_err());
assert!(PackageIdSpec::parse("https://baz:1.0").is_err());
assert!(PackageIdSpec::parse("https://#baz:1.0").is_err());
}
@@ -421,5 +453,6 @@ mod tests {
assert!(!PackageIdSpec::parse("foo:1.2.2").unwrap().matches(foo));
assert!(PackageIdSpec::parse("[email protected]").unwrap().matches(foo));
assert!(!PackageIdSpec::parse("[email protected]").unwrap().matches(foo));
assert!(PackageIdSpec::parse("[email protected]").unwrap().matches(foo));
}
}
11 changes: 6 additions & 5 deletions src/cargo/core/resolver/dep_cache.rs
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ use crate::core::{Dependency, FeatureValue, PackageId, PackageIdSpec, Registry,
use crate::sources::source::QueryKind;
use crate::util::errors::CargoResult;
use crate::util::interning::InternedString;
use crate::util::PartialVersion;
use crate::util::RustVersion;

use anyhow::Context as _;
use std::collections::{BTreeSet, HashMap, HashSet};
@@ -36,7 +36,7 @@ pub struct RegistryQueryer<'a> {
/// versions first. That allows `cargo update -Z minimal-versions` which will
/// specify minimum dependency versions to be used.
minimal_versions: bool,
max_rust_version: Option<PartialVersion>,
max_rust_version: Option<RustVersion>,
/// a cache of `Candidate`s that fulfil a `Dependency` (and whether `first_minimal_version`)
registry_cache: HashMap<(Dependency, bool), Poll<Rc<Vec<Summary>>>>,
/// a cache of `Dependency`s that are required for a `Summary`
@@ -58,14 +58,14 @@ impl<'a> RegistryQueryer<'a> {
replacements: &'a [(PackageIdSpec, Dependency)],
version_prefs: &'a VersionPreferences,
minimal_versions: bool,
max_rust_version: Option<PartialVersion>,
max_rust_version: Option<&RustVersion>,
) -> Self {
RegistryQueryer {
registry,
replacements,
version_prefs,
minimal_versions,
max_rust_version,
max_rust_version: max_rust_version.cloned(),
registry_cache: HashMap::new(),
summary_cache: HashMap::new(),
used_replacements: HashMap::new(),
@@ -115,7 +115,8 @@ impl<'a> RegistryQueryer<'a> {

let mut ret = Vec::new();
let ready = self.registry.query(dep, QueryKind::Exact, &mut |s| {
if self.max_rust_version.is_none() || s.rust_version() <= self.max_rust_version {
if self.max_rust_version.is_none() || s.rust_version() <= self.max_rust_version.as_ref()
{
ret.push(s);
}
})?;
4 changes: 2 additions & 2 deletions src/cargo/core/resolver/mod.rs
Original file line number Diff line number Diff line change
@@ -71,7 +71,7 @@ use crate::util::config::Config;
use crate::util::errors::CargoResult;
use crate::util::network::PollExt;
use crate::util::profile;
use crate::util::PartialVersion;
use crate::util::RustVersion;

use self::context::Context;
use self::dep_cache::RegistryQueryer;
@@ -139,7 +139,7 @@ pub fn resolve(
version_prefs: &VersionPreferences,
config: Option<&Config>,
check_public_visible_dependencies: bool,
mut max_rust_version: Option<PartialVersion>,
mut max_rust_version: Option<&RustVersion>,
) -> CargoResult<Resolve> {
let _p = profile::start("resolving");
let minimal_versions = match config {
4 changes: 2 additions & 2 deletions src/cargo/core/resolver/version_prefs.rs
Original file line number Diff line number Diff line change
@@ -81,7 +81,7 @@ impl VersionPreferences {
mod test {
use super::*;
use crate::core::SourceId;
use crate::util::PartialVersion;
use crate::util::RustVersion;
use std::collections::BTreeMap;

fn pkgid(name: &str, version: &str) -> PackageId {
@@ -104,7 +104,7 @@ mod test {
Vec::new(),
&features,
None::<&String>,
None::<PartialVersion>,
None::<RustVersion>,
)
.unwrap()
}
10 changes: 5 additions & 5 deletions src/cargo/core/summary.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::core::{Dependency, PackageId, SourceId};
use crate::util::interning::InternedString;
use crate::util::CargoResult;
use crate::util::PartialVersion;
use crate::util::RustVersion;
use anyhow::bail;
use semver::Version;
use std::collections::{BTreeMap, HashMap, HashSet};
@@ -26,7 +26,7 @@ struct Inner {
features: Rc<FeatureMap>,
checksum: Option<String>,
links: Option<InternedString>,
rust_version: Option<PartialVersion>,
rust_version: Option<RustVersion>,
}

impl Summary {
@@ -35,7 +35,7 @@ impl Summary {
dependencies: Vec<Dependency>,
features: &BTreeMap<InternedString, Vec<InternedString>>,
links: Option<impl Into<InternedString>>,
rust_version: Option<PartialVersion>,
rust_version: Option<RustVersion>,
) -> CargoResult<Summary> {
// ****CAUTION**** If you change anything here that may raise a new
// error, be sure to coordinate that change with either the index
@@ -88,8 +88,8 @@ impl Summary {
self.inner.links
}

pub fn rust_version(&self) -> Option<PartialVersion> {
self.inner.rust_version
pub fn rust_version(&self) -> Option<&RustVersion> {
self.inner.rust_version.as_ref()
}

pub fn override_id(mut self, id: PackageId) -> Summary {
4 changes: 2 additions & 2 deletions src/cargo/core/workspace.rs
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ use crate::util::edit_distance;
use crate::util::errors::{CargoResult, ManifestError};
use crate::util::interning::InternedString;
use crate::util::toml::{read_manifest, InheritableFields, TomlDependency, TomlProfiles};
use crate::util::PartialVersion;
use crate::util::RustVersion;
use crate::util::{config::ConfigRelativePath, Config, Filesystem, IntoUrl};
use cargo_util::paths;
use cargo_util::paths::normalize_path;
@@ -598,7 +598,7 @@ impl<'cfg> Workspace<'cfg> {

/// Get the lowest-common denominator `package.rust-version` within the workspace, if specified
/// anywhere
pub fn rust_version(&self) -> Option<PartialVersion> {
pub fn rust_version(&self) -> Option<&RustVersion> {
self.members().filter_map(|pkg| pkg.rust_version()).min()
}

6 changes: 3 additions & 3 deletions src/cargo/ops/cargo_add/mod.rs
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ use crate::util::toml_mut::dependency::Source;
use crate::util::toml_mut::dependency::WorkspaceSource;
use crate::util::toml_mut::manifest::DepTable;
use crate::util::toml_mut::manifest::LocalManifest;
use crate::util::PartialVersion;
use crate::util::RustVersion;
use crate::CargoResult;
use crate::Config;
use crate_spec::CrateSpec;
@@ -564,7 +564,7 @@ fn get_latest_dependency(
})?;

if config.cli_unstable().msrv_policy && honor_rust_version {
fn parse_msrv(comp: PartialVersion) -> (u64, u64, u64) {
fn parse_msrv(comp: &RustVersion) -> (u64, u64, u64) {
(comp.major, comp.minor.unwrap_or(0), comp.patch.unwrap_or(0))
}

@@ -624,7 +624,7 @@ fn get_latest_dependency(

fn rust_version_incompat_error(
dep: &str,
rust_version: PartialVersion,
rust_version: &RustVersion,
lowest_rust_version: Option<&Summary>,
) -> anyhow::Error {
let mut error_msg = format!(
2 changes: 1 addition & 1 deletion src/cargo/ops/cargo_clean.rs
Original file line number Diff line number Diff line change
@@ -108,7 +108,7 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
for spec_str in opts.spec.iter() {
// Translate the spec to a Package.
let spec = PackageIdSpec::parse(spec_str)?;
if spec.version().is_some() {
if spec.partial_version().is_some() {
config.shell().warn(&format!(
"version qualifier in `-p {}` is ignored, \
cleaning all versions of `{}` found",
4 changes: 2 additions & 2 deletions src/cargo/ops/cargo_package.rs
Original file line number Diff line number Diff line change
@@ -423,7 +423,7 @@ fn build_lock(ws: &Workspace<'_>, orig_pkg: &Package) -> CargoResult<String> {
TomlManifest::to_real_manifest(&toml_manifest, false, source_id, package_root, config)?;
let new_pkg = Package::new(manifest, orig_pkg.manifest_path());

let max_rust_version = new_pkg.rust_version();
let max_rust_version = new_pkg.rust_version().cloned();

// Regenerate Cargo.lock using the old one as a guide.
let tmp_ws = Workspace::ephemeral(new_pkg, ws.config(), None, true)?;
@@ -437,7 +437,7 @@ fn build_lock(ws: &Workspace<'_>, orig_pkg: &Package) -> CargoResult<String> {
None,
&[],
true,
max_rust_version,
max_rust_version.as_ref(),
)?;
let pkg_set = ops::get_resolved_packages(&new_resolve, tmp_reg)?;

8 changes: 4 additions & 4 deletions src/cargo/ops/resolve.rs
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ use crate::core::{GitReference, PackageId, PackageIdSpec, PackageSet, SourceId,
use crate::ops;
use crate::sources::PathSource;
use crate::util::errors::CargoResult;
use crate::util::PartialVersion;
use crate::util::RustVersion;
use crate::util::{profile, CanonicalUrl};
use anyhow::Context as _;
use std::collections::{HashMap, HashSet};
@@ -133,7 +133,7 @@ pub fn resolve_ws_with_opts<'cfg>(
specs: &[PackageIdSpec],
has_dev_units: HasDevUnits,
force_all_targets: ForceAllTargets,
max_rust_version: Option<PartialVersion>,
max_rust_version: Option<&RustVersion>,
) -> CargoResult<WorkspaceResolve<'cfg>> {
let mut registry = PackageRegistry::new(ws.config())?;
let mut add_patches = true;
@@ -240,7 +240,7 @@ pub fn resolve_ws_with_opts<'cfg>(
fn resolve_with_registry<'cfg>(
ws: &Workspace<'cfg>,
registry: &mut PackageRegistry<'cfg>,
max_rust_version: Option<PartialVersion>,
max_rust_version: Option<&RustVersion>,
) -> CargoResult<Resolve> {
let prev = ops::load_pkg_lockfile(ws)?;
let mut resolve = resolve_with_previous(
@@ -285,7 +285,7 @@ pub fn resolve_with_previous<'cfg>(
to_avoid: Option<&HashSet<PackageId>>,
specs: &[PackageIdSpec],
register_patches: bool,
max_rust_version: Option<PartialVersion>,
max_rust_version: Option<&RustVersion>,
) -> CargoResult<Resolve> {
// We only want one Cargo at a time resolving a crate graph since this can
// involve a lot of frobbing of the global caches.
4 changes: 2 additions & 2 deletions src/cargo/sources/registry/index.rs
Original file line number Diff line number Diff line change
@@ -91,7 +91,7 @@ use crate::core::{PackageId, SourceId, Summary};
use crate::sources::registry::{LoadResponse, RegistryData};
use crate::util::interning::InternedString;
use crate::util::IntoUrl;
use crate::util::{internal, CargoResult, Config, Filesystem, OptVersionReq, PartialVersion};
use crate::util::{internal, CargoResult, Config, Filesystem, OptVersionReq, RustVersion};
use anyhow::bail;
use cargo_util::{paths, registry::make_dep_path};
use semver::Version;
@@ -358,7 +358,7 @@ pub struct IndexPackage<'a> {
///
/// Added in 2023 (see <https://github.com/rust-lang/crates.io/pull/6267>),
/// can be `None` if published before then or if not set in the manifest.
rust_version: Option<PartialVersion>,
rust_version: Option<RustVersion>,
/// The schema version for this entry.
///
/// If this is None, it defaults to version `1`. Entries with unknown
2 changes: 1 addition & 1 deletion src/cargo/util/mod.rs
Original file line number Diff line number Diff line change
@@ -22,7 +22,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, VersionExt, VersionReqExt};
pub use self::semver_ext::{OptVersionReq, PartialVersion, RustVersion, VersionExt, VersionReqExt};
pub use self::to_semver::ToSemver;
pub use self::vcs::{existing_vcs_repo, FossilRepo, GitRepo, HgRepo, PijulRepo};
pub use self::workspace::{
163 changes: 131 additions & 32 deletions src/cargo/util/semver_ext.rs
Original file line number Diff line number Diff line change
@@ -109,65 +109,158 @@ impl From<VersionReq> for OptVersionReq {
}
}

#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone, Debug)]
#[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 = anyhow::Error;

fn from_str(value: &str) -> Result<Self, Self::Err> {
let partial = value.parse::<PartialVersion>()?;
if partial.pre.is_some() {
anyhow::bail!("unexpected prerelease field, expected a version like \"1.32\"")
}
if partial.build.is_some() {
anyhow::bail!("unexpected prerelease field, expected a version like \"1.32\"")
}
Ok(Self(partial))
}
}

impl<'de> serde::Deserialize<'de> for RustVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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)
}
}

#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)]
pub struct PartialVersion {
pub major: u64,
pub minor: Option<u64>,
pub patch: Option<u64>,
pub pre: Option<semver::Prerelease>,
pub build: Option<semver::BuildMetadata>,
}

impl PartialVersion {
pub fn version(&self) -> Option<Version> {
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 caret_req(&self) -> VersionReq {
VersionReq {
comparators: vec![Comparator {
op: semver::Op::Caret,
major: self.major,
minor: self.minor,
patch: self.patch,
pre: Default::default(),
pre: self.pre.as_ref().cloned().unwrap_or_default(),
}],
}
}

pub fn exact_req(&self) -> VersionReq {
VersionReq {
comparators: vec![Comparator {
op: semver::Op::Exact,
major: self.major,
minor: self.minor,
patch: self.patch,
pre: self.pre.as_ref().cloned().unwrap_or_default(),
}],
}
}
}

impl From<semver::Version> 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<Self, Self::Err> {
// HACK: `PartialVersion` is a subset of the `VersionReq` syntax that only ever
// has one comparator with a required minor and optional patch, and uses no
// other features.
if is_req(value) {
anyhow::bail!("unexpected version requirement, expected a version like \"1.32\"")
}
let version_req = match semver::VersionReq::parse(value) {
// Exclude semver operators like `^` and pre-release identifiers
Ok(req) if value.chars().all(|c| c.is_ascii_digit() || c == '.') => req,
_ if value.contains('+') => {
anyhow::bail!("unexpected build field, expected a version like \"1.32\"")
}
_ if value.contains('-') => {
anyhow::bail!("unexpected prerelease field, 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, "guarenteed by is_req");
let comp = version_req.comparators.pop().unwrap();
assert_eq!(comp.op, semver::Op::Caret, "guarenteed 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,
})
}
_ => anyhow::bail!("expected a version like \"1.32\""),
};
assert_eq!(
version_req.comparators.len(),
1,
"guarenteed by character check"
);
let comp = &version_req.comparators[0];
assert_eq!(comp.op, semver::Op::Caret, "guarenteed by character check");
assert_eq!(
comp.pre,
semver::Prerelease::EMPTY,
"guarenteed by character check"
);
Ok(PartialVersion {
major: comp.major,
minor: comp.minor,
patch: comp.patch,
})
}
}
}

@@ -181,6 +274,12 @@ impl Display for PartialVersion {
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(())
}
}
31 changes: 16 additions & 15 deletions src/cargo/util/toml/mod.rs
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ use crate::util::errors::{CargoResult, ManifestError};
use crate::util::interning::InternedString;
use crate::util::{
self, config::ConfigRelativePath, validate_package_name, Config, IntoUrl, OptVersionReq,
PartialVersion,
RustVersion,
};

pub mod embedded;
@@ -1182,16 +1182,16 @@ impl<'de> de::Deserialize<'de> for MaybeWorkspaceString {
}
}

type MaybeWorkspacePartialVersion = MaybeWorkspace<PartialVersion, TomlWorkspaceField>;
impl<'de> de::Deserialize<'de> for MaybeWorkspacePartialVersion {
type MaybeWorkspaceRustVersion = MaybeWorkspace<RustVersion, TomlWorkspaceField>;
impl<'de> de::Deserialize<'de> for MaybeWorkspaceRustVersion {
fn deserialize<D>(d: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct Visitor;

impl<'de> de::Visitor<'de> for Visitor {
type Value = MaybeWorkspacePartialVersion;
type Value = MaybeWorkspaceRustVersion;

fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
f.write_str("a semver or workspace")
@@ -1201,8 +1201,8 @@ impl<'de> de::Deserialize<'de> for MaybeWorkspacePartialVersion {
where
E: de::Error,
{
let value = value.parse::<PartialVersion>().map_err(|e| E::custom(e))?;
Ok(MaybeWorkspacePartialVersion::Defined(value))
let value = value.parse::<RustVersion>().map_err(|e| E::custom(e))?;
Ok(MaybeWorkspaceRustVersion::Defined(value))
}

fn visit_map<V>(self, map: V) -> Result<Self::Value, V::Error>
@@ -1400,7 +1400,7 @@ impl WorkspaceInherit for TomlWorkspaceField {
#[serde(rename_all = "kebab-case")]
pub struct TomlPackage {
edition: Option<MaybeWorkspaceString>,
rust_version: Option<MaybeWorkspacePartialVersion>,
rust_version: Option<MaybeWorkspaceRustVersion>,
name: InternedString,
#[serde(deserialize_with = "version_trim_whitespace")]
version: MaybeWorkspaceSemverVersion,
@@ -1490,7 +1490,7 @@ pub struct InheritableFields {
exclude: Option<Vec<String>>,
include: Option<Vec<String>>,
#[serde(rename = "rust-version")]
rust_version: Option<PartialVersion>,
rust_version: Option<RustVersion>,
// We use skip here since it will never be present when deserializing
// and we don't want it present when serializing
#[serde(skip)]
@@ -1530,7 +1530,7 @@ impl InheritableFields {
("package.license", license -> String),
("package.publish", publish -> VecStringOrBool),
("package.repository", repository -> String),
("package.rust-version", rust_version -> PartialVersion),
("package.rust-version", rust_version -> RustVersion),
("package.version", version -> semver::Version),
}

@@ -1961,8 +1961,9 @@ impl TomlManifest {
}

let rust_version = if let Some(rust_version) = &package.rust_version {
let rust_version =
rust_version.resolve("rust_version", || inherit()?.rust_version())?;
let rust_version = rust_version
.clone()
.resolve("rust_version", || inherit()?.rust_version())?;
let req = rust_version.caret_req();
if let Some(first_version) = edition.first_version() {
let unsupported =
@@ -2244,7 +2245,7 @@ impl TomlManifest {
deps,
me.features.as_ref().unwrap_or(&empty_features),
package.links.as_deref(),
rust_version,
rust_version.clone(),
)?;

let metadata = ManifestMetadata {
@@ -2357,7 +2358,7 @@ impl TomlManifest {
.categories
.as_ref()
.map(|_| MaybeWorkspace::Defined(metadata.categories.clone()));
package.rust_version = rust_version.map(|rv| MaybeWorkspace::Defined(rv));
package.rust_version = rust_version.clone().map(|rv| MaybeWorkspace::Defined(rv));
package.exclude = package
.exclude
.as_ref()
@@ -2651,8 +2652,8 @@ impl TomlManifest {
replacement.unused_keys(),
&mut cx.warnings,
);
dep.set_version_req(OptVersionReq::exact(version))
.lock_version(version);
dep.set_version_req(OptVersionReq::exact(&version))
.lock_version(&version);
replace.push((spec, dep));
}
Ok(replace)
2 changes: 2 additions & 0 deletions src/doc/src/reference/pkgid-spec.md
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ The formal grammar for a Package Id Specification is:
spec := pkgname
| proto "://" hostname-and-path [ "#" ( pkgname | semver ) ]
pkgname := name [ ("@" | ":" ) semver ]
semver := digits [ "." digits [ "." digits [ "-" prerelease ] [ "+" build ]]]
proto := "http" | "git" | ...
```
@@ -40,6 +41,7 @@ The following are references to the `regex` package on `crates.io`:
| Spec | Name | Version |
|:------------------------------------------------------------|:-------:|:-------:|
| `regex` | `regex` | `*` |
| `regex@1.4` | `regex` | `1.4.*` |
| `regex@1.4.3` | `regex` | `1.4.3` |
| `https://github.com/rust-lang/crates.io-index#regex` | `regex` | `*` |
| `https://github.com/rust-lang/crates.io-index#regex@1.4.3` | `regex` | `1.4.3` |
122 changes: 115 additions & 7 deletions tests/testsuite/clean.rs
Original file line number Diff line number Diff line change
@@ -569,10 +569,118 @@ fn assert_all_clean(build_dir: &Path) {
}

#[cargo_test]
fn clean_spec_multiple() {
fn clean_spec_version() {
// clean -p foo where foo matches multiple versions
Package::new("bar", "1.0.0").publish();
Package::new("bar", "2.0.0").publish();
Package::new("bar", "0.1.0").publish();
Package::new("bar", "0.2.0").publish();

let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar1 = {version="0.1", package="bar"}
bar2 = {version="0.2", package="bar"}
"#,
)
.file("src/lib.rs", "")
.build();

p.cargo("build").run();

// Check suggestion for bad pkgid.
p.cargo("clean -p baz")
.with_status(101)
.with_stderr(
"\
error: package ID specification `baz` did not match any packages
<tab>Did you mean `bar`?
",
)
.run();

p.cargo("clean -p bar:0.1.0")
.with_stderr(
"warning: version qualifier in `-p bar:0.1.0` is ignored, \
cleaning all versions of `bar` found",
)
.run();
let mut walker = walkdir::WalkDir::new(p.build_dir())
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
let n = e.file_name().to_str().unwrap();
n.starts_with("bar") || n.starts_with("libbar")
});
if let Some(e) = walker.next() {
panic!("{:?} was not cleaned", e.path());
}
}

#[cargo_test]
fn clean_spec_partial_version() {
// clean -p foo where foo matches multiple versions
Package::new("bar", "0.1.0").publish();
Package::new("bar", "0.2.0").publish();

let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar1 = {version="0.1", package="bar"}
bar2 = {version="0.2", package="bar"}
"#,
)
.file("src/lib.rs", "")
.build();

p.cargo("build").run();

// Check suggestion for bad pkgid.
p.cargo("clean -p baz")
.with_status(101)
.with_stderr(
"\
error: package ID specification `baz` did not match any packages
<tab>Did you mean `bar`?
",
)
.run();

p.cargo("clean -p bar:0.1")
.with_stderr(
"warning: version qualifier in `-p bar:0.1` is ignored, \
cleaning all versions of `bar` found",
)
.run();
let mut walker = walkdir::WalkDir::new(p.build_dir())
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
let n = e.file_name().to_str().unwrap();
n.starts_with("bar") || n.starts_with("libbar")
});
if let Some(e) = walker.next() {
panic!("{:?} was not cleaned", e.path());
}
}

#[cargo_test]
fn clean_spec_partial_version_ambiguous() {
// clean -p foo where foo matches multiple versions
Package::new("bar", "0.1.0").publish();
Package::new("bar", "0.2.0").publish();

let p = project()
.file(
@@ -583,8 +691,8 @@ fn clean_spec_multiple() {
version = "0.1.0"
[dependencies]
bar1 = {version="1.0", package="bar"}
bar2 = {version="2.0", package="bar"}
bar1 = {version="0.1", package="bar"}
bar2 = {version="0.2", package="bar"}
"#,
)
.file("src/lib.rs", "")
@@ -604,9 +712,9 @@ error: package ID specification `baz` did not match any packages
)
.run();

p.cargo("clean -p bar:1.0.0")
p.cargo("clean -p bar:0")
.with_stderr(
"warning: version qualifier in `-p bar:1.0.0` is ignored, \
"warning: version qualifier in `-p bar:0` is ignored, \
cleaning all versions of `bar` found",
)
.run();
18 changes: 6 additions & 12 deletions tests/testsuite/pkgid.rs
Original file line number Diff line number Diff line change
@@ -151,25 +151,19 @@ fn multiple_versions() {
.with_status(101)
.with_stderr(
"\
error: invalid package ID specification: `two-ver@0`
<tab>Did you mean `two-ver`?
Caused by:
cannot parse '0' as a SemVer version
error: There are multiple `two-ver` packages in your project, and the specification `two-ver@0` is ambiguous.
Please re-run this command with `-p <spec>` where `<spec>` is one of the following:
two-ver@0.1.0
two-ver@0.2.0
",
)
.run();

// Incomplete version.
p.cargo("pkgid two-ver@0.2")
.with_status(101)
.with_stderr(
.with_stdout(
"\
error: invalid package ID specification: `two-ver@0.2`
Caused by:
cannot parse '0.2' as a SemVer version
https://github.com/rust-lang/crates.io-index#two-ver@0.2.0
",
)
.run();
54 changes: 54 additions & 0 deletions tests/testsuite/profile_overrides.rs
Original file line number Diff line number Diff line change
@@ -267,6 +267,60 @@ found package specs: bar, bar@0.5.0",
.run();
}

#[cargo_test]
fn profile_override_spec_with_version() {
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.0.1"
[dependencies]
bar = { path = "bar" }
[profile.dev.package."bar:0.5.0"]
codegen-units = 2
"#,
)
.file("src/lib.rs", "")
.file("bar/Cargo.toml", &basic_lib_manifest("bar"))
.file("bar/src/lib.rs", "")
.build();

p.cargo("check -v")
.with_stderr_contains("[RUNNING] `rustc [..]bar/src/lib.rs [..] -C codegen-units=2 [..]")
.run();
}

#[cargo_test]
fn profile_override_spec_with_partial_version() {
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.0.1"
[dependencies]
bar = { path = "bar" }
[profile.dev.package."bar:0.5"]
codegen-units = 2
"#,
)
.file("src/lib.rs", "")
.file("bar/Cargo.toml", &basic_lib_manifest("bar"))
.file("bar/src/lib.rs", "")
.build();

p.cargo("check -v")
.with_stderr_contains("[RUNNING] `rustc [..]bar/src/lib.rs [..] -C codegen-units=2 [..]")
.run();
}

#[cargo_test]
fn profile_override_spec() {
let p = project()

0 comments on commit da498c8

Please sign in to comment.