Skip to content

Commit 9e6288e

Browse files
committed
Auto merge of #13516 - epage:msrv-add, r=ehuss
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
2 parents 352d2f3 + 55d3d65 commit 9e6288e

File tree

28 files changed

+413
-72
lines changed

28 files changed

+413
-72
lines changed

src/cargo/ops/cargo_add/mod.rs

Lines changed: 78 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::str::FromStr;
1111

1212
use anyhow::Context as _;
1313
use cargo_util::paths;
14+
use cargo_util_schemas::core::PartialVersion;
1415
use cargo_util_schemas::manifest::RustVersion;
1516
use indexmap::IndexSet;
1617
use itertools::Itertools;
@@ -615,52 +616,74 @@ fn get_latest_dependency(
615616
})?;
616617

617618
if gctx.cli_unstable().msrv_policy && honor_rust_version {
618-
fn parse_msrv(comp: &RustVersion) -> (u64, u64, u64) {
619-
(comp.major, comp.minor.unwrap_or(0), comp.patch.unwrap_or(0))
620-
}
619+
let (req_msrv, is_msrv) = spec
620+
.rust_version()
621+
.cloned()
622+
.map(|msrv| CargoResult::Ok((msrv.clone(), true)))
623+
.unwrap_or_else(|| {
624+
let rustc = gctx.load_global_rustc(None)?;
625+
626+
// Remove any pre-release identifiers for easier comparison
627+
let current_version = &rustc.version;
628+
let untagged_version = RustVersion::try_from(PartialVersion {
629+
major: current_version.major,
630+
minor: Some(current_version.minor),
631+
patch: Some(current_version.patch),
632+
pre: None,
633+
build: None,
634+
})
635+
.unwrap();
636+
Ok((untagged_version, false))
637+
})?;
621638

622-
if let Some(req_msrv) = spec.rust_version().map(parse_msrv) {
623-
let msrvs = possibilities
624-
.iter()
625-
.map(|s| (s, s.rust_version().map(parse_msrv)))
626-
.collect::<Vec<_>>();
627-
628-
// Find the latest version of the dep which has a compatible rust-version. To
629-
// determine whether or not one rust-version is compatible with another, we
630-
// compare the lowest possible versions they could represent, and treat
631-
// candidates without a rust-version as compatible by default.
632-
let (latest_msrv, _) = msrvs
633-
.iter()
634-
.filter(|(_, v)| v.map(|msrv| req_msrv >= msrv).unwrap_or(true))
635-
.last()
636-
.ok_or_else(|| {
637-
// Failing that, try to find the highest version with the lowest
638-
// rust-version to report to the user.
639-
let lowest_candidate = msrvs
640-
.iter()
641-
.min_set_by_key(|(_, v)| v)
642-
.iter()
643-
.map(|(s, _)| s)
644-
.max_by_key(|s| s.version());
645-
rust_version_incompat_error(
646-
&dependency.name,
647-
spec.rust_version().unwrap(),
648-
lowest_candidate.copied(),
639+
let msrvs = possibilities
640+
.iter()
641+
.map(|s| (s, s.rust_version()))
642+
.collect::<Vec<_>>();
643+
644+
// Find the latest version of the dep which has a compatible rust-version. To
645+
// determine whether or not one rust-version is compatible with another, we
646+
// compare the lowest possible versions they could represent, and treat
647+
// candidates without a rust-version as compatible by default.
648+
let latest_msrv = latest_compatible(&msrvs, &req_msrv).ok_or_else(|| {
649+
let name = spec.name();
650+
let dep_name = &dependency.name;
651+
let latest_version = latest.version();
652+
let latest_msrv = latest
653+
.rust_version()
654+
.expect("as `None` are compatible, we can't be here");
655+
if is_msrv {
656+
anyhow::format_err!(
657+
"\
658+
no version of crate `{dep_name}` can maintain {name}'s rust-version of {req_msrv}
659+
help: pass `--ignore-rust-version` to select {dep_name}@{latest_version} which requires rustc {latest_msrv}"
649660
)
650-
})?;
651-
652-
if latest_msrv.version() < latest.version() {
661+
} else {
662+
anyhow::format_err!(
663+
"\
664+
no version of crate `{dep_name}` is compatible with rustc {req_msrv}
665+
help: pass `--ignore-rust-version` to select {dep_name}@{latest_version} which requires rustc {latest_msrv}"
666+
)
667+
}
668+
})?;
669+
670+
if latest_msrv.version() < latest.version() {
671+
let latest_version = latest.version();
672+
let latest_rust_version = latest.rust_version().unwrap();
673+
let name = spec.name();
674+
if is_msrv {
653675
gctx.shell().warn(format_args!(
654-
"ignoring `{dependency}@{latest_version}` (which has a rust-version of \
655-
{latest_rust_version}) to satisfy this package's rust-version of \
656-
{rust_version} (use `--ignore-rust-version` to override)",
657-
latest_version = latest.version(),
658-
latest_rust_version = latest.rust_version().unwrap(),
659-
rust_version = spec.rust_version().unwrap(),
676+
"\
677+
ignoring {dependency}@{latest_version} (which requires rustc {latest_rust_version}) to maintain {name}'s rust-version of {req_msrv}",
678+
))?;
679+
} else {
680+
gctx.shell().warn(format_args!(
681+
"\
682+
ignoring {dependency}@{latest_version} (which requires rustc {latest_rust_version}) as it is incompatible with rustc {req_msrv}",
660683
))?;
661-
662-
latest = latest_msrv;
663684
}
685+
686+
latest = latest_msrv;
664687
}
665688
}
666689

@@ -673,29 +696,20 @@ fn get_latest_dependency(
673696
}
674697
}
675698

676-
fn rust_version_incompat_error(
677-
dep: &str,
678-
rust_version: &RustVersion,
679-
lowest_rust_version: Option<&Summary>,
680-
) -> anyhow::Error {
681-
let mut error_msg = format!(
682-
"could not find version of crate `{dep}` that satisfies this package's rust-version of \
683-
{rust_version}\n\
684-
help: use `--ignore-rust-version` to override this behavior"
685-
);
686-
687-
if let Some(lowest) = lowest_rust_version {
688-
// rust-version must be present for this candidate since it would have been selected as
689-
// compatible previously if it weren't.
690-
let version = lowest.version();
691-
let rust_version = lowest.rust_version().unwrap();
692-
error_msg.push_str(&format!(
693-
"\nnote: the lowest rust-version available for `{dep}` is {rust_version}, used in \
694-
version {version}"
695-
));
696-
}
697-
698-
anyhow::format_err!(error_msg)
699+
/// Of MSRV-compatible summaries, find the highest version
700+
///
701+
/// Assumptions:
702+
/// - `msrvs` is sorted by version
703+
fn latest_compatible<'s>(
704+
msrvs: &[(&'s Summary, Option<&RustVersion>)],
705+
req_msrv: &RustVersion,
706+
) -> Option<&'s Summary> {
707+
msrvs
708+
.iter()
709+
.filter(|(_, v)| v.as_ref().map(|msrv| req_msrv >= *msrv).unwrap_or(true))
710+
.map(|(s, _)| s)
711+
.last()
712+
.copied()
699713
}
700714

701715
fn select_package(

tests/testsuite/cargo_add/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ mod rust_version_ignore;
123123
mod rust_version_incompatible;
124124
mod rust_version_latest;
125125
mod rust_version_older;
126+
mod rustc_ignore;
127+
mod rustc_incompatible;
128+
mod rustc_latest;
129+
mod rustc_older;
126130
mod sorted_table_with_dotted_item;
127131
mod target;
128132
mod target_cfg;

tests/testsuite/cargo_add/rust_version_incompatible/stderr.term.svg

Lines changed: 4 additions & 6 deletions
Loading

tests/testsuite/cargo_add/rust_version_older/stderr.term.svg

Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[workspace]
2+
3+
[package]
4+
name = "cargo-list-test-fixture"
5+
version = "0.0.0"
6+
edition = "2015"

tests/testsuite/cargo_add/rustc_ignore/in/src/lib.rs

Whitespace-only changes.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::current_dir;
3+
use cargo_test_support::file;
4+
use cargo_test_support::prelude::*;
5+
use cargo_test_support::str;
6+
use cargo_test_support::Project;
7+
8+
#[cargo_test]
9+
fn case() {
10+
cargo_test_support::registry::init();
11+
cargo_test_support::registry::Package::new("rust-version-user", "0.1.0")
12+
.rust_version("1.30")
13+
.publish();
14+
cargo_test_support::registry::Package::new("rust-version-user", "0.1.1")
15+
.rust_version("1.30")
16+
.publish();
17+
cargo_test_support::registry::Package::new("rust-version-user", "0.2.1")
18+
.rust_version("1.2345")
19+
.publish();
20+
21+
let project = Project::from_template(current_dir!().join("in"));
22+
let project_root = project.root();
23+
let cwd = &project_root;
24+
25+
snapbox::cmd::Command::cargo_ui()
26+
.arg("-Zmsrv-policy")
27+
.arg("add")
28+
.arg("--ignore-rust-version")
29+
.arg_line("rust-version-user")
30+
.current_dir(cwd)
31+
.masquerade_as_nightly_cargo(&["msrv-policy"])
32+
.assert()
33+
.code(0)
34+
.stdout_matches(str![""])
35+
.stderr_matches(file!["stderr.term.svg"]);
36+
37+
assert_ui().subset_matches(current_dir!().join("out"), &project_root);
38+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[workspace]
2+
3+
[package]
4+
name = "cargo-list-test-fixture"
5+
version = "0.0.0"
6+
edition = "2015"
7+
8+
[dependencies]
9+
rust-version-user = "0.2.1"

tests/testsuite/cargo_add/rustc_ignore/out/src/lib.rs

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[workspace]
2+
3+
[package]
4+
name = "cargo-list-test-fixture"
5+
version = "0.0.0"
6+
edition = "2015"

tests/testsuite/cargo_add/rustc_incompatible/in/src/lib.rs

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::current_dir;
3+
use cargo_test_support::file;
4+
use cargo_test_support::prelude::*;
5+
use cargo_test_support::str;
6+
use cargo_test_support::Project;
7+
8+
#[cargo_test]
9+
fn case() {
10+
cargo_test_support::registry::init();
11+
cargo_test_support::registry::Package::new("rust-version-user", "0.2.1")
12+
.rust_version("1.2345")
13+
.publish();
14+
15+
let project = Project::from_template(current_dir!().join("in"));
16+
let project_root = project.root();
17+
let cwd = &project_root;
18+
19+
snapbox::cmd::Command::cargo_ui()
20+
.arg("-Zmsrv-policy")
21+
.arg("add")
22+
.arg_line("rust-version-user")
23+
.current_dir(cwd)
24+
.masquerade_as_nightly_cargo(&["msrv-policy"])
25+
.assert()
26+
.failure()
27+
.stdout_matches(str![""])
28+
.stderr_matches(file!["stderr.term.svg"]);
29+
30+
assert_ui().subset_matches(current_dir!().join("out"), &project_root);
31+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[workspace]
2+
3+
[package]
4+
name = "cargo-list-test-fixture"
5+
version = "0.0.0"
6+
edition = "2015"

tests/testsuite/cargo_add/rustc_incompatible/out/src/lib.rs

Whitespace-only changes.
Lines changed: 32 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[workspace]
2+
3+
[package]
4+
name = "cargo-list-test-fixture"
5+
version = "0.0.0"
6+
edition = "2015"

tests/testsuite/cargo_add/rustc_latest/in/src/lib.rs

Whitespace-only changes.

0 commit comments

Comments
 (0)