Skip to content

Commit

Permalink
Auto merge of #13516 - epage:msrv-add, r=ehuss
Browse files Browse the repository at this point in the history
feat(add): Fallback to `rustc -v` when no MSRV is set

### What does this PR try to resolve?

#10653 made version-requirement selection respect MSRV as part of #9930.
This updates the implementation for the now-approved RFC specifies that we should respect `rustc -v` if there is no MSRV.

The messages also get a little bit of polish.

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

Tests are added in separate commits for easier viewing of behavior changes.

### Additional information
  • Loading branch information
bors committed Mar 3, 2024
2 parents 352d2f3 + 55d3d65 commit 9e6288e
Show file tree
Hide file tree
Showing 28 changed files with 413 additions and 72 deletions.
142 changes: 78 additions & 64 deletions src/cargo/ops/cargo_add/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::str::FromStr;

use anyhow::Context as _;
use cargo_util::paths;
use cargo_util_schemas::core::PartialVersion;
use cargo_util_schemas::manifest::RustVersion;
use indexmap::IndexSet;
use itertools::Itertools;
Expand Down Expand Up @@ -615,52 +616,74 @@ fn get_latest_dependency(
})?;

if gctx.cli_unstable().msrv_policy && honor_rust_version {
fn parse_msrv(comp: &RustVersion) -> (u64, u64, u64) {
(comp.major, comp.minor.unwrap_or(0), comp.patch.unwrap_or(0))
}
let (req_msrv, is_msrv) = spec
.rust_version()
.cloned()
.map(|msrv| CargoResult::Ok((msrv.clone(), 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))
})?;

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(),
let msrvs = possibilities
.iter()
.map(|s| (s, s.rust_version()))
.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 = latest_compatible(&msrvs, &req_msrv).ok_or_else(|| {
let name = spec.name();
let dep_name = &dependency.name;
let latest_version = latest.version();
let latest_msrv = latest
.rust_version()
.expect("as `None` are compatible, we can't be here");
if is_msrv {
anyhow::format_err!(
"\
no version of crate `{dep_name}` can maintain {name}'s rust-version of {req_msrv}
help: pass `--ignore-rust-version` to select {dep_name}@{latest_version} which requires rustc {latest_msrv}"
)
})?;

if latest_msrv.version() < latest.version() {
} else {
anyhow::format_err!(
"\
no version of crate `{dep_name}` is compatible with rustc {req_msrv}
help: pass `--ignore-rust-version` to select {dep_name}@{latest_version} which requires rustc {latest_msrv}"
)
}
})?;

if latest_msrv.version() < latest.version() {
let latest_version = latest.version();
let latest_rust_version = latest.rust_version().unwrap();
let name = spec.name();
if is_msrv {
gctx.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(),
"\
ignoring {dependency}@{latest_version} (which requires rustc {latest_rust_version}) to maintain {name}'s rust-version of {req_msrv}",
))?;
} else {
gctx.shell().warn(format_args!(
"\
ignoring {dependency}@{latest_version} (which requires rustc {latest_rust_version}) as it is incompatible with rustc {req_msrv}",
))?;

latest = latest_msrv;
}

latest = latest_msrv;
}
}

Expand All @@ -673,29 +696,20 @@ fn get_latest_dependency(
}
}

fn rust_version_incompat_error(
dep: &str,
rust_version: &RustVersion,
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)
/// Of MSRV-compatible summaries, find the highest version
///
/// Assumptions:
/// - `msrvs` is sorted by version
fn latest_compatible<'s>(
msrvs: &[(&'s Summary, Option<&RustVersion>)],
req_msrv: &RustVersion,
) -> Option<&'s Summary> {
msrvs
.iter()
.filter(|(_, v)| v.as_ref().map(|msrv| req_msrv >= *msrv).unwrap_or(true))
.map(|(s, _)| s)
.last()
.copied()
}

fn select_package(
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 @@ -123,6 +123,10 @@ mod rust_version_ignore;
mod rust_version_incompatible;
mod rust_version_latest;
mod rust_version_older;
mod rustc_ignore;
mod rustc_incompatible;
mod rustc_latest;
mod rustc_older;
mod sorted_table_with_dotted_item;
mod target;
mod target_cfg;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions tests/testsuite/cargo_add/rust_version_older/stderr.term.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions tests/testsuite/cargo_add/rustc_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"
edition = "2015"
Empty file.
38 changes: 38 additions & 0 deletions tests/testsuite/cargo_add/rustc_ignore/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use cargo_test_support::compare::assert_ui;
use cargo_test_support::current_dir;
use cargo_test_support::file;
use cargo_test_support::prelude::*;
use cargo_test_support::str;
use cargo_test_support::Project;

#[cargo_test]
fn case() {
cargo_test_support::registry::init();
cargo_test_support::registry::Package::new("rust-version-user", "0.1.0")
.rust_version("1.30")
.publish();
cargo_test_support::registry::Package::new("rust-version-user", "0.1.1")
.rust_version("1.30")
.publish();
cargo_test_support::registry::Package::new("rust-version-user", "0.2.1")
.rust_version("1.2345")
.publish();

let project = Project::from_template(current_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()
.code(0)
.stdout_matches(str![""])
.stderr_matches(file!["stderr.term.svg"]);

assert_ui().subset_matches(current_dir!().join("out"), &project_root);
}
9 changes: 9 additions & 0 deletions tests/testsuite/cargo_add/rustc_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"
edition = "2015"

[dependencies]
rust-version-user = "0.2.1"
Empty file.
29 changes: 29 additions & 0 deletions tests/testsuite/cargo_add/rustc_ignore/stderr.term.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions tests/testsuite/cargo_add/rustc_incompatible/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"
edition = "2015"
Empty file.
31 changes: 31 additions & 0 deletions tests/testsuite/cargo_add/rustc_incompatible/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use cargo_test_support::compare::assert_ui;
use cargo_test_support::current_dir;
use cargo_test_support::file;
use cargo_test_support::prelude::*;
use cargo_test_support::str;
use cargo_test_support::Project;

#[cargo_test]
fn case() {
cargo_test_support::registry::init();
cargo_test_support::registry::Package::new("rust-version-user", "0.2.1")
.rust_version("1.2345")
.publish();

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

snapbox::cmd::Command::cargo_ui()
.arg("-Zmsrv-policy")
.arg("add")
.arg_line("rust-version-user")
.current_dir(cwd)
.masquerade_as_nightly_cargo(&["msrv-policy"])
.assert()
.failure()
.stdout_matches(str![""])
.stderr_matches(file!["stderr.term.svg"]);

assert_ui().subset_matches(current_dir!().join("out"), &project_root);
}
6 changes: 6 additions & 0 deletions tests/testsuite/cargo_add/rustc_incompatible/out/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"
edition = "2015"
Empty file.
32 changes: 32 additions & 0 deletions tests/testsuite/cargo_add/rustc_incompatible/stderr.term.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions tests/testsuite/cargo_add/rustc_latest/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"
edition = "2015"
Empty file.
Loading

0 comments on commit 9e6288e

Please sign in to comment.