Skip to content

Commit

Permalink
Consider rust-version when selecting packages for cargo add
Browse files Browse the repository at this point in the history
When `-Zmsrv-policy` is enabled, try to select dependencies which satisfy the
target package's `rust-version` field (if present). If the selected version is
not also the latest, emit a warning to the user about this discrepancy.

Dependency versions without a `rust-version` are considered compatible by
default.

Implements #10653.
  • Loading branch information
cassaundra committed May 4, 2023
1 parent ac84010 commit aca793c
Showing 1 changed file with 92 additions and 15 deletions.
107 changes: 92 additions & 15 deletions src/cargo/ops/cargo_add/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
&manifest,
raw,
workspace,
&options.spec,
&options.section,
options.config,
&mut registry,
Expand Down Expand Up @@ -256,6 +257,7 @@ fn resolve_dependency(
manifest: &LocalManifest,
arg: &DepOp,
ws: &Workspace<'_>,
spec: &Package,
section: &DepTable,
config: &Config,
registry: &mut PackageRegistry<'_>,
Expand Down Expand Up @@ -368,7 +370,7 @@ fn resolve_dependency(
}
dependency = dependency.set_source(src);
} else {
let latest = get_latest_dependency(&dependency, false, config, registry)?;
let latest = get_latest_dependency(spec, &dependency, false, config, registry)?;

if dependency.name != latest.name {
config.shell().warn(format!(
Expand Down Expand Up @@ -518,6 +520,7 @@ fn get_existing_dependency(
}

fn get_latest_dependency(
spec: &Package,
dependency: &Dependency,
_flag_allow_prerelease: bool,
config: &Config,
Expand All @@ -529,27 +532,86 @@ fn get_latest_dependency(
unreachable!("registry dependencies required, found a workspace dependency");
}
MaybeWorkspace::Other(query) => {
let possibilities = loop {
let mut possibilities = loop {
match registry.query_vec(&query, QueryKind::Fuzzy) {
std::task::Poll::Ready(res) => {
break res?;
}
std::task::Poll::Pending => registry.block_until_ready()?,
}
};
let latest = possibilities
.iter()
.max_by_key(|s| {
// Fallback to a pre-release if no official release is available by sorting them as
// less.
let stable = s.version().pre.is_empty();
(stable, s.version())
})
.ok_or_else(|| {
anyhow::format_err!(
"the crate `{dependency}` could not be found in registry index."
)
})?;

possibilities.sort_by_key(|s| {
// Fallback to a pre-release if no official release is available by sorting them as
// less.
let stable = s.version().pre.is_empty();
(stable, s.version().clone())
});

let mut latest = possibilities.last().ok_or_else(|| {
anyhow::format_err!(
"the crate `{dependency}` could not be found in registry index."
)
})?;

if config.cli_unstable().msrv_policy {
if let Some((req_major, req_minor, req_patch)) =
spec.rust_version().map(parse_rust_version)
{
let rust_versions = possibilities
.iter()
.map(|s| {
let rust_version = s.rust_version().map(parse_rust_version);
(s, rust_version)
})
.collect::<Vec<_>>();

// Find the latest version of the dep which has a compatible rust-version,
// treating candidates without a rust-version as compatible.
let (latest_msrv, _) = rust_versions
.iter()
.filter(|(_, v)| {
v.map(|(major, minor, patch)| {
// Determine whether or not the rust-versions are compatible by
// comparing the lowest possible versions each of them could
// represent.
(req_major, req_minor, req_patch.unwrap_or(0))
>= (major, minor, patch.unwrap_or(0))
})
.unwrap_or(true)
})
.last()
.ok_or_else(|| {
let mut error_msg = format!(
"could not find version of crate `{dependency}` that satisfies \
this package's rust-version"
);

if let Some((lowest, _)) = rust_versions
.iter()
.filter(|(_, v)| v.is_some())
.min_by_key(|(_, v)| v)
{
let lowest_version = lowest.rust_version().unwrap();
error_msg.push_str(&format!(
"\nnote: the lowest rust-version available for `{dependency}` \
is {lowest_version}"
));
}

anyhow::anyhow!(error_msg)
})?;

if latest_msrv.version() < latest.version() {
config.shell().warn(format_args!(
"selecting older version of `{dependency}` to satisfy this package's \
rust-version"
))?;
latest = latest_msrv;
}
}
}

let mut dep = Dependency::from(latest);
if let Some(reg_name) = dependency.registry.as_deref() {
dep = dep.set_registry(reg_name);
Expand All @@ -559,6 +621,21 @@ fn get_latest_dependency(
}
}

fn parse_rust_version(rust_version: impl AsRef<str>) -> (u64, u64, Option<u64>) {
// HACK: `rust-version` 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 in the future this
// syntax is expanded, this code will need to be updated.

let version = semver::VersionReq::parse(rust_version.as_ref()).unwrap();
assert!(version.comparators.len() == 1);

let version = &version.comparators[0];
assert_eq!(version.op, semver::Op::Caret);
assert_eq!(version.pre, semver::Prerelease::EMPTY);

(version.major, version.minor.unwrap(), version.patch)
}

fn select_package(
dependency: &Dependency,
config: &Config,
Expand Down

0 comments on commit aca793c

Please sign in to comment.