Skip to content

Commit

Permalink
Auto merge of #12078 - cassaundra:cargo-add-rust-version, r=epage
Browse files Browse the repository at this point in the history
Consider rust-version when selecting packages for cargo add

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.

One remaining question is whether we should go into more detail when explaining the discrepancy and ways to resolve it to the user. For example:

```
warning: selecting older version of `fancy-dep` to satisfy the minimum supported rust version
note: version 0.1.2 of `fancy-dep` has an MSRV of 1.72, which is greater than this package's MSRV of 1.69
```

Implements #10653.

r? `@epage`
  • Loading branch information
bors committed May 23, 2023
2 parents feb9bcf + 8df391b commit 64fb38c
Show file tree
Hide file tree
Showing 35 changed files with 383 additions and 15 deletions.
16 changes: 16 additions & 0 deletions src/bin/cargo/commands/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ The package will be removed from your features.")
Example uses:
- Depending on multiple versions of a crate
- Depend on crates with the same name from different registries"),
flag(
"ignore-rust-version",
"Ignore `rust-version` specification in packages (unstable)"
),
])
.arg_manifest_path()
.arg_package("Package to modify")
Expand Down Expand Up @@ -188,12 +192,24 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {

let dependencies = parse_dependencies(config, args)?;

let ignore_rust_version = args.flag("ignore-rust-version");
if ignore_rust_version && !config.cli_unstable().msrv_policy {
return Err(CliError::new(
anyhow::format_err!(
"`--ignore-rust-version` is unstable; pass `-Zmsrv-policy` to enable support for it"
),
101,
));
}
let honor_rust_version = !ignore_rust_version;

let options = AddOptions {
config,
spec,
dependencies,
section,
dry_run,
honor_rust_version,
};
add(&ws, &options)?;

Expand Down
130 changes: 115 additions & 15 deletions src/cargo/ops/cargo_add/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ pub struct AddOptions<'a> {
pub section: DepTable,
/// Act as if dependencies will be added
pub dry_run: bool,
/// Whether the minimum supported Rust version should be considered during resolution
pub honor_rust_version: bool,
}

/// Add dependencies to a manifest
Expand Down Expand Up @@ -86,7 +88,9 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
&manifest,
raw,
workspace,
&options.spec,
&options.section,
options.honor_rust_version,
options.config,
&mut registry,
)
Expand Down Expand Up @@ -256,7 +260,9 @@ fn resolve_dependency(
manifest: &LocalManifest,
arg: &DepOp,
ws: &Workspace<'_>,
spec: &Package,
section: &DepTable,
honor_rust_version: bool,
config: &Config,
registry: &mut PackageRegistry<'_>,
) -> CargoResult<DependencyUI> {
Expand Down Expand Up @@ -368,7 +374,14 @@ 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,
honor_rust_version,
config,
registry,
)?;

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

fn get_latest_dependency(
spec: &Package,
dependency: &Dependency,
_flag_allow_prerelease: bool,
honor_rust_version: bool,
config: &Config,
registry: &mut PackageRegistry<'_>,
) -> CargoResult<Dependency> {
Expand All @@ -529,27 +544,87 @@ 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 && honor_rust_version {
fn parse_msrv(rust_version: impl AsRef<str>) -> (u64, u64, 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_req = semver::VersionReq::parse(rust_version.as_ref()).unwrap();
assert!(version_req.comparators.len() == 1);
let comp = &version_req.comparators[0];
assert_eq!(comp.op, semver::Op::Caret);
assert_eq!(comp.pre, semver::Prerelease::EMPTY);
(comp.major, comp.minor.unwrap_or(0), comp.patch.unwrap_or(0))
}

if let Some(req_msrv) = spec.rust_version().map(parse_msrv) {
let msrvs = possibilities
.iter()
.map(|s| (s, s.rust_version().map(parse_msrv)))
.collect::<Vec<_>>();

// Find the latest version of the dep which has a compatible rust-version. To
// determine whether or not one rust-version is compatible with another, we
// compare the lowest possible versions they could represent, and treat
// candidates without a rust-version as compatible by default.
let (latest_msrv, _) = msrvs
.iter()
.filter(|(_, v)| v.map(|msrv| req_msrv >= msrv).unwrap_or(true))
.last()
.ok_or_else(|| {
// Failing that, try to find the highest version with the lowest
// rust-version to report to the user.
let lowest_candidate = msrvs
.iter()
.min_set_by_key(|(_, v)| v)
.iter()
.map(|(s, _)| s)
.max_by_key(|s| s.version());
rust_version_incompat_error(
&dependency.name,
spec.rust_version().unwrap(),
lowest_candidate.copied(),
)
})?;

if latest_msrv.version() < latest.version() {
config.shell().warn(format_args!(
"ignoring `{dependency}@{latest_version}` (which has a rust-version of \
{latest_rust_version}) to satisfy this package's rust-version of \
{rust_version} (use `--ignore-rust-version` to override)",
latest_version = latest.version(),
latest_rust_version = latest.rust_version().unwrap(),
rust_version = spec.rust_version().unwrap(),
))?;

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 +634,31 @@ fn get_latest_dependency(
}
}

fn rust_version_incompat_error(
dep: &str,
rust_version: &str,
lowest_rust_version: Option<&Summary>,
) -> anyhow::Error {
let mut error_msg = format!(
"could not find version of crate `{dep}` that satisfies this package's rust-version of \
{rust_version}\n\
help: use `--ignore-rust-version` to override this behavior"
);

if let Some(lowest) = lowest_rust_version {
// rust-version must be present for this candidate since it would have been selected as
// compatible previously if it weren't.
let version = lowest.version();
let rust_version = lowest.rust_version().unwrap();
error_msg.push_str(&format!(
"\nnote: the lowest rust-version available for `{dep}` is {rust_version}, used in \
version {version}"
));
}

anyhow::format_err!(error_msg)
}

fn select_package(
dependency: &Dependency,
config: &Config,
Expand Down
9 changes: 9 additions & 0 deletions src/doc/man/cargo-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ crates, the features for a specific crate may be enabled with
which enables all specified features.
{{/option}}

{{#option "`--ignore-rust-version`" }}
Ignore `rust-version` specification in packages.

This option is unstable and available only on the
[nightly channel](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html)
and requires the `-Z unstable-options` flag to enable.
See <https://github.com/rust-lang/cargo/issues/5579> for more information.
{{/option}}

{{/options}}


Expand Down
9 changes: 9 additions & 0 deletions src/doc/man/generated_txt/cargo-add.txt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ OPTIONS
be enabled with package-name/feature-name syntax. This flag may be
specified multiple times, which enables all specified features.

--ignore-rust-version
Ignore rust-version specification in packages.

This option is unstable and available only on the nightly channel
<https://doc.rust-lang.org/book/appendix-07-nightly-rust.html> and
requires the -Z unstable-options flag to enable. See
<https://github.com/rust-lang/cargo/issues/5579> for more
information.

Display Options
-v, --verbose
Use verbose output. May be specified twice for “very verbose”
Expand Down
8 changes: 8 additions & 0 deletions src/doc/src/commands/cargo-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ crates, the features for a specific crate may be enabled with
which enables all specified features.</dd>


<dt class="option-term" id="option-cargo-add---ignore-rust-version"><a class="option-anchor" href="#option-cargo-add---ignore-rust-version"></a><code>--ignore-rust-version</code></dt>
<dd class="option-desc">Ignore <code>rust-version</code> specification in packages.</p>
<p>This option is unstable and available only on the
<a href="https://doc.rust-lang.org/book/appendix-07-nightly-rust.html">nightly channel</a>
and requires the <code>-Z unstable-options</code> flag to enable.
See <a href="https://github.com/rust-lang/cargo/issues/5579">https://github.com/rust-lang/cargo/issues/5579</a> for more information.</dd>


</dl>


Expand Down
10 changes: 10 additions & 0 deletions src/etc/man/cargo-add.1
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,16 @@ crates, the features for a specific crate may be enabled with
\fBpackage\-name/feature\-name\fR syntax. This flag may be specified multiple times,
which enables all specified features.
.RE
.sp
\fB\-\-ignore\-rust\-version\fR
.RS 4
Ignore \fBrust\-version\fR specification in packages.
.sp
This option is unstable and available only on the
\fInightly channel\fR <https://doc.rust\-lang.org/book/appendix\-07\-nightly\-rust.html>
and requires the \fB\-Z unstable\-options\fR flag to enable.
See <https://github.com/rust\-lang/cargo/issues/5579> for more information.
.RE
.SS "Display Options"
.sp
\fB\-v\fR,
Expand Down
4 changes: 4 additions & 0 deletions tests/testsuite/cargo_add/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ mod quiet;
mod registry;
mod rename;
mod require_weak;
mod rust_version_ignore;
mod rust_version_incompatible;
mod rust_version_latest;
mod rust_version_older;
mod sorted_table_with_dotted_item;
mod target;
mod target_cfg;
Expand Down
6 changes: 6 additions & 0 deletions tests/testsuite/cargo_add/rust_version_ignore/in/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[workspace]

[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
rust-version = "1.68"
Empty file.
36 changes: 36 additions & 0 deletions tests/testsuite/cargo_add/rust_version_ignore/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use cargo_test_support::compare::assert_ui;
use cargo_test_support::prelude::*;
use cargo_test_support::Project;

use crate::cargo_add::init_registry;
use cargo_test_support::curr_dir;

#[cargo_test]
fn case() {
init_registry();

cargo_test_support::registry::Package::new("rust-version-user", "0.1.0")
.rust_version("1.66")
.publish();
cargo_test_support::registry::Package::new("rust-version-user", "0.2.1")
.rust_version("1.72")
.publish();

let project = Project::from_template(curr_dir!().join("in"));
let project_root = project.root();
let cwd = &project_root;

snapbox::cmd::Command::cargo_ui()
.arg("-Zmsrv-policy")
.arg("add")
.arg("--ignore-rust-version")
.arg_line("rust-version-user")
.current_dir(cwd)
.masquerade_as_nightly_cargo(&["msrv-policy"])
.assert()
.success()
.stdout_matches_path(curr_dir!().join("stdout.log"))
.stderr_matches_path(curr_dir!().join("stderr.log"));

assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
}
9 changes: 9 additions & 0 deletions tests/testsuite/cargo_add/rust_version_ignore/out/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[workspace]

[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
rust-version = "1.68"

[dependencies]
rust-version-user = "0.2.1"
Empty file.
2 changes: 2 additions & 0 deletions tests/testsuite/cargo_add/rust_version_ignore/stderr.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Updating `dummy-registry` index
Adding rust-version-user v0.2.1 to dependencies.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[workspace]

[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
rust-version = "1.56"
Empty file.
Loading

0 comments on commit 64fb38c

Please sign in to comment.