Skip to content

Commit 64fb38c

Browse files
committedMay 23, 2023
Auto merge of #12078 - cassaundra:cargo-add-rust-version, r=epage
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`
2 parents feb9bcf + 8df391b commit 64fb38c

File tree

35 files changed

+383
-15
lines changed

35 files changed

+383
-15
lines changed
 

‎src/bin/cargo/commands/add.rs

+16
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ The package will be removed from your features.")
7272
Example uses:
7373
- Depending on multiple versions of a crate
7474
- Depend on crates with the same name from different registries"),
75+
flag(
76+
"ignore-rust-version",
77+
"Ignore `rust-version` specification in packages (unstable)"
78+
),
7579
])
7680
.arg_manifest_path()
7781
.arg_package("Package to modify")
@@ -188,12 +192,24 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
188192

189193
let dependencies = parse_dependencies(config, args)?;
190194

195+
let ignore_rust_version = args.flag("ignore-rust-version");
196+
if ignore_rust_version && !config.cli_unstable().msrv_policy {
197+
return Err(CliError::new(
198+
anyhow::format_err!(
199+
"`--ignore-rust-version` is unstable; pass `-Zmsrv-policy` to enable support for it"
200+
),
201+
101,
202+
));
203+
}
204+
let honor_rust_version = !ignore_rust_version;
205+
191206
let options = AddOptions {
192207
config,
193208
spec,
194209
dependencies,
195210
section,
196211
dry_run,
212+
honor_rust_version,
197213
};
198214
add(&ws, &options)?;
199215

‎src/cargo/ops/cargo_add/mod.rs

+115-15
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ pub struct AddOptions<'a> {
5151
pub section: DepTable,
5252
/// Act as if dependencies will be added
5353
pub dry_run: bool,
54+
/// Whether the minimum supported Rust version should be considered during resolution
55+
pub honor_rust_version: bool,
5456
}
5557

5658
/// Add dependencies to a manifest
@@ -86,7 +88,9 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
8688
&manifest,
8789
raw,
8890
workspace,
91+
&options.spec,
8992
&options.section,
93+
options.honor_rust_version,
9094
options.config,
9195
&mut registry,
9296
)
@@ -256,7 +260,9 @@ fn resolve_dependency(
256260
manifest: &LocalManifest,
257261
arg: &DepOp,
258262
ws: &Workspace<'_>,
263+
spec: &Package,
259264
section: &DepTable,
265+
honor_rust_version: bool,
260266
config: &Config,
261267
registry: &mut PackageRegistry<'_>,
262268
) -> CargoResult<DependencyUI> {
@@ -368,7 +374,14 @@ fn resolve_dependency(
368374
}
369375
dependency = dependency.set_source(src);
370376
} else {
371-
let latest = get_latest_dependency(&dependency, false, config, registry)?;
377+
let latest = get_latest_dependency(
378+
spec,
379+
&dependency,
380+
false,
381+
honor_rust_version,
382+
config,
383+
registry,
384+
)?;
372385

373386
if dependency.name != latest.name {
374387
config.shell().warn(format!(
@@ -518,8 +531,10 @@ fn get_existing_dependency(
518531
}
519532

520533
fn get_latest_dependency(
534+
spec: &Package,
521535
dependency: &Dependency,
522536
_flag_allow_prerelease: bool,
537+
honor_rust_version: bool,
523538
config: &Config,
524539
registry: &mut PackageRegistry<'_>,
525540
) -> CargoResult<Dependency> {
@@ -529,27 +544,87 @@ fn get_latest_dependency(
529544
unreachable!("registry dependencies required, found a workspace dependency");
530545
}
531546
MaybeWorkspace::Other(query) => {
532-
let possibilities = loop {
547+
let mut possibilities = loop {
533548
match registry.query_vec(&query, QueryKind::Fuzzy) {
534549
std::task::Poll::Ready(res) => {
535550
break res?;
536551
}
537552
std::task::Poll::Pending => registry.block_until_ready()?,
538553
}
539554
};
540-
let latest = possibilities
541-
.iter()
542-
.max_by_key(|s| {
543-
// Fallback to a pre-release if no official release is available by sorting them as
544-
// less.
545-
let stable = s.version().pre.is_empty();
546-
(stable, s.version())
547-
})
548-
.ok_or_else(|| {
549-
anyhow::format_err!(
550-
"the crate `{dependency}` could not be found in registry index."
551-
)
552-
})?;
555+
556+
possibilities.sort_by_key(|s| {
557+
// Fallback to a pre-release if no official release is available by sorting them as
558+
// less.
559+
let stable = s.version().pre.is_empty();
560+
(stable, s.version().clone())
561+
});
562+
563+
let mut latest = possibilities.last().ok_or_else(|| {
564+
anyhow::format_err!(
565+
"the crate `{dependency}` could not be found in registry index."
566+
)
567+
})?;
568+
569+
if config.cli_unstable().msrv_policy && honor_rust_version {
570+
fn parse_msrv(rust_version: impl AsRef<str>) -> (u64, u64, u64) {
571+
// HACK: `rust-version` is a subset of the `VersionReq` syntax that only ever
572+
// has one comparator with a required minor and optional patch, and uses no
573+
// other features. If in the future this syntax is expanded, this code will need
574+
// to be updated.
575+
let version_req = semver::VersionReq::parse(rust_version.as_ref()).unwrap();
576+
assert!(version_req.comparators.len() == 1);
577+
let comp = &version_req.comparators[0];
578+
assert_eq!(comp.op, semver::Op::Caret);
579+
assert_eq!(comp.pre, semver::Prerelease::EMPTY);
580+
(comp.major, comp.minor.unwrap_or(0), comp.patch.unwrap_or(0))
581+
}
582+
583+
if let Some(req_msrv) = spec.rust_version().map(parse_msrv) {
584+
let msrvs = possibilities
585+
.iter()
586+
.map(|s| (s, s.rust_version().map(parse_msrv)))
587+
.collect::<Vec<_>>();
588+
589+
// Find the latest version of the dep which has a compatible rust-version. To
590+
// determine whether or not one rust-version is compatible with another, we
591+
// compare the lowest possible versions they could represent, and treat
592+
// candidates without a rust-version as compatible by default.
593+
let (latest_msrv, _) = msrvs
594+
.iter()
595+
.filter(|(_, v)| v.map(|msrv| req_msrv >= msrv).unwrap_or(true))
596+
.last()
597+
.ok_or_else(|| {
598+
// Failing that, try to find the highest version with the lowest
599+
// rust-version to report to the user.
600+
let lowest_candidate = msrvs
601+
.iter()
602+
.min_set_by_key(|(_, v)| v)
603+
.iter()
604+
.map(|(s, _)| s)
605+
.max_by_key(|s| s.version());
606+
rust_version_incompat_error(
607+
&dependency.name,
608+
spec.rust_version().unwrap(),
609+
lowest_candidate.copied(),
610+
)
611+
})?;
612+
613+
if latest_msrv.version() < latest.version() {
614+
config.shell().warn(format_args!(
615+
"ignoring `{dependency}@{latest_version}` (which has a rust-version of \
616+
{latest_rust_version}) to satisfy this package's rust-version of \
617+
{rust_version} (use `--ignore-rust-version` to override)",
618+
latest_version = latest.version(),
619+
latest_rust_version = latest.rust_version().unwrap(),
620+
rust_version = spec.rust_version().unwrap(),
621+
))?;
622+
623+
latest = latest_msrv;
624+
}
625+
}
626+
}
627+
553628
let mut dep = Dependency::from(latest);
554629
if let Some(reg_name) = dependency.registry.as_deref() {
555630
dep = dep.set_registry(reg_name);
@@ -559,6 +634,31 @@ fn get_latest_dependency(
559634
}
560635
}
561636

637+
fn rust_version_incompat_error(
638+
dep: &str,
639+
rust_version: &str,
640+
lowest_rust_version: Option<&Summary>,
641+
) -> anyhow::Error {
642+
let mut error_msg = format!(
643+
"could not find version of crate `{dep}` that satisfies this package's rust-version of \
644+
{rust_version}\n\
645+
help: use `--ignore-rust-version` to override this behavior"
646+
);
647+
648+
if let Some(lowest) = lowest_rust_version {
649+
// rust-version must be present for this candidate since it would have been selected as
650+
// compatible previously if it weren't.
651+
let version = lowest.version();
652+
let rust_version = lowest.rust_version().unwrap();
653+
error_msg.push_str(&format!(
654+
"\nnote: the lowest rust-version available for `{dep}` is {rust_version}, used in \
655+
version {version}"
656+
));
657+
}
658+
659+
anyhow::format_err!(error_msg)
660+
}
661+
562662
fn select_package(
563663
dependency: &Dependency,
564664
config: &Config,

‎src/doc/man/cargo-add.md

+9
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ crates, the features for a specific crate may be enabled with
123123
which enables all specified features.
124124
{{/option}}
125125

126+
{{#option "`--ignore-rust-version`" }}
127+
Ignore `rust-version` specification in packages.
128+
129+
This option is unstable and available only on the
130+
[nightly channel](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html)
131+
and requires the `-Z unstable-options` flag to enable.
132+
See <https://github.com/rust-lang/cargo/issues/5579> for more information.
133+
{{/option}}
134+
126135
{{/options}}
127136

128137

‎src/doc/man/generated_txt/cargo-add.txt

+9
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,15 @@ OPTIONS
111111
be enabled with package-name/feature-name syntax. This flag may be
112112
specified multiple times, which enables all specified features.
113113

114+
--ignore-rust-version
115+
Ignore rust-version specification in packages.
116+
117+
This option is unstable and available only on the nightly channel
118+
<https://doc.rust-lang.org/book/appendix-07-nightly-rust.html> and
119+
requires the -Z unstable-options flag to enable. See
120+
<https://github.com/rust-lang/cargo/issues/5579> for more
121+
information.
122+
114123
Display Options
115124
-v, --verbose
116125
Use verbose output. May be specified twice for “very verbose”

‎src/doc/src/commands/cargo-add.md

+8
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ crates, the features for a specific crate may be enabled with
129129
which enables all specified features.</dd>
130130

131131

132+
<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>
133+
<dd class="option-desc">Ignore <code>rust-version</code> specification in packages.</p>
134+
<p>This option is unstable and available only on the
135+
<a href="https://doc.rust-lang.org/book/appendix-07-nightly-rust.html">nightly channel</a>
136+
and requires the <code>-Z unstable-options</code> flag to enable.
137+
See <a href="https://github.com/rust-lang/cargo/issues/5579">https://github.com/rust-lang/cargo/issues/5579</a> for more information.</dd>
138+
139+
132140
</dl>
133141

134142

‎src/etc/man/cargo-add.1

+10
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,16 @@ crates, the features for a specific crate may be enabled with
140140
\fBpackage\-name/feature\-name\fR syntax. This flag may be specified multiple times,
141141
which enables all specified features.
142142
.RE
143+
.sp
144+
\fB\-\-ignore\-rust\-version\fR
145+
.RS 4
146+
Ignore \fBrust\-version\fR specification in packages.
147+
.sp
148+
This option is unstable and available only on the
149+
\fInightly channel\fR <https://doc.rust\-lang.org/book/appendix\-07\-nightly\-rust.html>
150+
and requires the \fB\-Z unstable\-options\fR flag to enable.
151+
See <https://github.com/rust\-lang/cargo/issues/5579> for more information.
152+
.RE
143153
.SS "Display Options"
144154
.sp
145155
\fB\-v\fR,

‎tests/testsuite/cargo_add/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ mod quiet;
102102
mod registry;
103103
mod rename;
104104
mod require_weak;
105+
mod rust_version_ignore;
106+
mod rust_version_incompatible;
107+
mod rust_version_latest;
108+
mod rust_version_older;
105109
mod sorted_table_with_dotted_item;
106110
mod target;
107111
mod target_cfg;
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+
rust-version = "1.68"

‎tests/testsuite/cargo_add/rust_version_ignore/in/src/lib.rs

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::prelude::*;
3+
use cargo_test_support::Project;
4+
5+
use crate::cargo_add::init_registry;
6+
use cargo_test_support::curr_dir;
7+
8+
#[cargo_test]
9+
fn case() {
10+
init_registry();
11+
12+
cargo_test_support::registry::Package::new("rust-version-user", "0.1.0")
13+
.rust_version("1.66")
14+
.publish();
15+
cargo_test_support::registry::Package::new("rust-version-user", "0.2.1")
16+
.rust_version("1.72")
17+
.publish();
18+
19+
let project = Project::from_template(curr_dir!().join("in"));
20+
let project_root = project.root();
21+
let cwd = &project_root;
22+
23+
snapbox::cmd::Command::cargo_ui()
24+
.arg("-Zmsrv-policy")
25+
.arg("add")
26+
.arg("--ignore-rust-version")
27+
.arg_line("rust-version-user")
28+
.current_dir(cwd)
29+
.masquerade_as_nightly_cargo(&["msrv-policy"])
30+
.assert()
31+
.success()
32+
.stdout_matches_path(curr_dir!().join("stdout.log"))
33+
.stderr_matches_path(curr_dir!().join("stderr.log"));
34+
35+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
36+
}
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+
rust-version = "1.68"
7+
8+
[dependencies]
9+
rust-version-user = "0.2.1"

‎tests/testsuite/cargo_add/rust_version_ignore/out/src/lib.rs

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Updating `dummy-registry` index
2+
Adding rust-version-user v0.2.1 to dependencies.

‎tests/testsuite/cargo_add/rust_version_ignore/stdout.log

Whitespace-only changes.
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+
rust-version = "1.56"

‎tests/testsuite/cargo_add/rust_version_incompatible/in/src/lib.rs

Whitespace-only changes.
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::prelude::*;
3+
use cargo_test_support::Project;
4+
5+
use crate::cargo_add::init_registry;
6+
use cargo_test_support::curr_dir;
7+
8+
#[cargo_test]
9+
fn case() {
10+
init_registry();
11+
12+
cargo_test_support::registry::Package::new("rust-version-user", "0.1.0")
13+
.rust_version("1.66")
14+
.publish();
15+
cargo_test_support::registry::Package::new("rust-version-user", "0.1.1")
16+
.rust_version("1.66")
17+
.publish();
18+
cargo_test_support::registry::Package::new("rust-version-user", "0.2.1")
19+
.rust_version("1.72")
20+
.publish();
21+
22+
let project = Project::from_template(curr_dir!().join("in"));
23+
let project_root = project.root();
24+
let cwd = &project_root;
25+
26+
snapbox::cmd::Command::cargo_ui()
27+
.arg("-Zmsrv-policy")
28+
.arg("add")
29+
.arg_line("rust-version-user")
30+
.current_dir(cwd)
31+
.masquerade_as_nightly_cargo(&["msrv-policy"])
32+
.assert()
33+
.failure()
34+
.stdout_matches_path(curr_dir!().join("stdout.log"))
35+
.stderr_matches_path(curr_dir!().join("stderr.log"));
36+
37+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
38+
}
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+
rust-version = "1.56"

‎tests/testsuite/cargo_add/rust_version_incompatible/out/src/lib.rs

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Updating `dummy-registry` index
2+
error: could not find version of crate `rust-version-user` that satisfies this package's rust-version of 1.56
3+
help: use `--ignore-rust-version` to override this behavior
4+
note: the lowest rust-version available for `rust-version-user` is 1.66, used in version 0.1.1

‎tests/testsuite/cargo_add/rust_version_incompatible/stdout.log

Whitespace-only changes.
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+
rust-version = "1.72"

‎tests/testsuite/cargo_add/rust_version_latest/in/src/lib.rs

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::prelude::*;
3+
use cargo_test_support::Project;
4+
5+
use crate::cargo_add::init_registry;
6+
use cargo_test_support::curr_dir;
7+
8+
#[cargo_test]
9+
fn case() {
10+
init_registry();
11+
12+
cargo_test_support::registry::Package::new("rust-version-user", "0.1.0")
13+
.rust_version("1.66")
14+
.publish();
15+
cargo_test_support::registry::Package::new("rust-version-user", "0.2.1")
16+
.rust_version("1.72")
17+
.publish();
18+
19+
let project = Project::from_template(curr_dir!().join("in"));
20+
let project_root = project.root();
21+
let cwd = &project_root;
22+
23+
snapbox::cmd::Command::cargo_ui()
24+
.arg("-Zmsrv-policy")
25+
.arg("add")
26+
.arg_line("rust-version-user")
27+
.current_dir(cwd)
28+
.masquerade_as_nightly_cargo(&["msrv-policy"])
29+
.assert()
30+
.success()
31+
.stdout_matches_path(curr_dir!().join("stdout.log"))
32+
.stderr_matches_path(curr_dir!().join("stderr.log"));
33+
34+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
35+
}
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+
rust-version = "1.72"
7+
8+
[dependencies]
9+
rust-version-user = "0.2.1"

‎tests/testsuite/cargo_add/rust_version_latest/out/src/lib.rs

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Updating `dummy-registry` index
2+
Adding rust-version-user v0.2.1 to dependencies.

‎tests/testsuite/cargo_add/rust_version_latest/stdout.log

Whitespace-only changes.
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+
rust-version = "1.70"

‎tests/testsuite/cargo_add/rust_version_older/in/src/lib.rs

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::prelude::*;
3+
use cargo_test_support::Project;
4+
5+
use crate::cargo_add::init_registry;
6+
use cargo_test_support::curr_dir;
7+
8+
#[cargo_test]
9+
fn case() {
10+
init_registry();
11+
12+
cargo_test_support::registry::Package::new("rust-version-user", "0.1.0")
13+
.rust_version("1.66")
14+
.publish();
15+
cargo_test_support::registry::Package::new("rust-version-user", "0.2.1")
16+
.rust_version("1.72")
17+
.publish();
18+
19+
let project = Project::from_template(curr_dir!().join("in"));
20+
let project_root = project.root();
21+
let cwd = &project_root;
22+
23+
snapbox::cmd::Command::cargo_ui()
24+
.arg("-Zmsrv-policy")
25+
.arg("add")
26+
.arg_line("rust-version-user")
27+
.current_dir(cwd)
28+
.masquerade_as_nightly_cargo(&["msrv-policy"])
29+
.assert()
30+
.success()
31+
.stdout_matches_path(curr_dir!().join("stdout.log"))
32+
.stderr_matches_path(curr_dir!().join("stderr.log"));
33+
34+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
35+
}
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+
rust-version = "1.70"
7+
8+
[dependencies]
9+
rust-version-user = "0.1.0"

‎tests/testsuite/cargo_add/rust_version_older/out/src/lib.rs

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Updating `dummy-registry` index
2+
warning: ignoring `rust-version-user@0.2.1` (which has a rust-version of 1.72) to satisfy this package's rust-version of 1.70 (use `--ignore-rust-version` to override)
3+
Adding rust-version-user v0.1.0 to dependencies.

‎tests/testsuite/cargo_add/rust_version_older/stdout.log

Whitespace-only changes.

0 commit comments

Comments
 (0)
Please sign in to comment.