Skip to content

Commit 5e44da4

Browse files
committed
Auto merge of #14569 - epage:msrv-max, r=weihanglo
fix(resolve): Improve multi-MSRV workspaces ### What does this PR try to resolve? We do this by resolving for a package version that is compatible with the most number of MSRVs within a workspace. If a version requirement is just right, every package will get the highest MSRV-compatible dependency. If its too high, packages will get MSRV-incompatible dependency versions. This will happen regardless of what we do due to the nature of `"fallback"`. If its too low, packages with higher MSRVs will get older-than-necessary dependency versions. This is similar to the "some with and without MSRV" workspaces. When locking dependencies, we do report to users when newer MSRV/SemVer compatible dependencies are available to help guide them to upgrading if desired. Fixes #14414 ### How should we test and review this PR? Is this the right behavior? - This feature is unstable and letting people try it is one way to determine that - A concern was raised within the Cargo team about new users with workspace member MSRVs all set to latest having someone ask them to lower an MSRV and them dealing with staler-than-required dependencies - At this point, there seems to be agreement on #14414 being a problem, the resolver magically fixing this is unlikely to happen for the foreseeable future, and this does fix it with the potential for user confusion. From my understanding of those conversations, they are mostly in the area of UX, like with #14543. Rather than blocking on that discussion, this moves forward with the implementation. ### Additional information
2 parents e94fde9 + 94db932 commit 5e44da4

File tree

8 files changed

+118
-53
lines changed

8 files changed

+118
-53
lines changed

src/cargo/core/resolver/version_prefs.rs

+54-20
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub struct VersionPreferences {
2121
try_to_use: HashSet<PackageId>,
2222
prefer_patch_deps: HashMap<InternedString, HashSet<Dependency>>,
2323
version_ordering: VersionOrdering,
24-
max_rust_version: Option<PartialVersion>,
24+
rust_versions: Vec<PartialVersion>,
2525
}
2626

2727
#[derive(Copy, Clone, Default, PartialEq, Eq, Hash, Debug)]
@@ -49,8 +49,8 @@ impl VersionPreferences {
4949
self.version_ordering = ordering;
5050
}
5151

52-
pub fn max_rust_version(&mut self, ver: Option<PartialVersion>) {
53-
self.max_rust_version = ver;
52+
pub fn rust_versions(&mut self, vers: Vec<PartialVersion>) {
53+
self.rust_versions = vers;
5454
}
5555

5656
/// Sort (and filter) the given vector of summaries in-place
@@ -59,7 +59,7 @@ impl VersionPreferences {
5959
///
6060
/// Sort order:
6161
/// 1. Preferred packages
62-
/// 2. [`VersionPreferences::max_rust_version`]
62+
/// 2. Most compatible [`VersionPreferences::rust_versions`]
6363
/// 3. `first_version`, falling back to [`VersionPreferences::version_ordering`] when `None`
6464
///
6565
/// Filtering:
@@ -85,20 +85,11 @@ impl VersionPreferences {
8585
return previous_cmp;
8686
}
8787

88-
if let Some(max_rust_version) = &self.max_rust_version {
89-
let a_is_compat = a
90-
.rust_version()
91-
.map(|a| a.is_compatible_with(max_rust_version))
92-
.unwrap_or(true);
93-
let b_is_compat = b
94-
.rust_version()
95-
.map(|b| b.is_compatible_with(max_rust_version))
96-
.unwrap_or(true);
97-
match (a_is_compat, b_is_compat) {
98-
(true, true) => {} // fallback
99-
(false, false) => {} // fallback
100-
(true, false) => return Ordering::Less,
101-
(false, true) => return Ordering::Greater,
88+
if !self.rust_versions.is_empty() {
89+
let a_compat_count = self.msrv_compat_count(a);
90+
let b_compat_count = self.msrv_compat_count(b);
91+
if b_compat_count != a_compat_count {
92+
return b_compat_count.cmp(&a_compat_count);
10293
}
10394
}
10495

@@ -112,6 +103,17 @@ impl VersionPreferences {
112103
let _ = summaries.split_off(1);
113104
}
114105
}
106+
107+
fn msrv_compat_count(&self, summary: &Summary) -> usize {
108+
let Some(rust_version) = summary.rust_version() else {
109+
return self.rust_versions.len();
110+
};
111+
112+
self.rust_versions
113+
.iter()
114+
.filter(|max| rust_version.is_compatible_with(max))
115+
.count()
116+
}
115117
}
116118

117119
#[cfg(test)]
@@ -236,9 +238,9 @@ mod test {
236238
}
237239

238240
#[test]
239-
fn test_max_rust_version() {
241+
fn test_single_rust_version() {
240242
let mut vp = VersionPreferences::default();
241-
vp.max_rust_version(Some("1.50".parse().unwrap()));
243+
vp.rust_versions(vec!["1.50".parse().unwrap()]);
242244

243245
let mut summaries = vec![
244246
summ("foo", "1.2.4", None),
@@ -267,6 +269,38 @@ mod test {
267269
);
268270
}
269271

272+
#[test]
273+
fn test_multiple_rust_versions() {
274+
let mut vp = VersionPreferences::default();
275+
vp.rust_versions(vec!["1.45".parse().unwrap(), "1.55".parse().unwrap()]);
276+
277+
let mut summaries = vec![
278+
summ("foo", "1.2.4", None),
279+
summ("foo", "1.2.3", Some("1.60")),
280+
summ("foo", "1.2.2", None),
281+
summ("foo", "1.2.1", Some("1.50")),
282+
summ("foo", "1.2.0", None),
283+
summ("foo", "1.1.0", Some("1.40")),
284+
summ("foo", "1.0.9", None),
285+
];
286+
287+
vp.version_ordering(VersionOrdering::MaximumVersionsFirst);
288+
vp.sort_summaries(&mut summaries, None);
289+
assert_eq!(
290+
describe(&summaries),
291+
"foo/1.2.4, foo/1.2.2, foo/1.2.0, foo/1.1.0, foo/1.0.9, foo/1.2.1, foo/1.2.3"
292+
.to_string()
293+
);
294+
295+
vp.version_ordering(VersionOrdering::MinimumVersionsFirst);
296+
vp.sort_summaries(&mut summaries, None);
297+
assert_eq!(
298+
describe(&summaries),
299+
"foo/1.0.9, foo/1.1.0, foo/1.2.0, foo/1.2.2, foo/1.2.4, foo/1.2.1, foo/1.2.3"
300+
.to_string()
301+
);
302+
}
303+
270304
#[test]
271305
fn test_empty_summaries() {
272306
let vp = VersionPreferences::default();

src/cargo/core/workspace.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ impl<'gctx> Workspace<'gctx> {
664664

665665
/// Get the lowest-common denominator `package.rust-version` within the workspace, if specified
666666
/// anywhere
667-
pub fn rust_version(&self) -> Option<&RustVersion> {
667+
pub fn lowest_rust_version(&self) -> Option<&RustVersion> {
668668
self.members().filter_map(|pkg| pkg.rust_version()).min()
669669
}
670670

src/cargo/ops/cargo_update.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,7 @@ fn required_rust_version(ws: &Workspace<'_>) -> Option<PartialVersion> {
739739
return None;
740740
}
741741

742-
if let Some(ver) = ws.rust_version() {
742+
if let Some(ver) = ws.lowest_rust_version() {
743743
Some(ver.clone().into_partial())
744744
} else {
745745
let rustc = ws.gctx().load_global_rustc(Some(ws)).ok()?;

src/cargo/ops/lockfile.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ pub fn write_pkg_lockfile(ws: &Workspace<'_>, resolve: &mut Resolve) -> CargoRes
7171
// out lock file updates as they're otherwise already updated, and changes
7272
// which don't touch dependencies won't seemingly spuriously update the lock
7373
// file.
74-
let default_version = ResolveVersion::with_rust_version(ws.rust_version());
74+
let default_version = ResolveVersion::with_rust_version(ws.lowest_rust_version());
7575
let current_version = resolve.version();
7676
let next_lockfile_bump = ws.gctx().cli_unstable().next_lockfile_bump;
7777
tracing::debug!("lockfile - current: {current_version:?}, default: {default_version:?}");

src/cargo/ops/registry/info/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,6 @@ fn try_get_msrv_from_nearest_manifest_or_ws(
217217
let rust_version = nearest_package.and_then(|p| p.rust_version().map(|v| v.as_partial()));
218218
// If the nearest manifest does not have a specific Rust version, try to get it from the workspace.
219219
rust_version
220-
.or_else(|| ws.and_then(|ws| ws.rust_version().map(|v| v.as_partial())))
220+
.or_else(|| ws.and_then(|ws| ws.lowest_rust_version().map(|v| v.as_partial())))
221221
.cloned()
222222
}

src/cargo/ops/resolve.rs

+11-8
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ use crate::util::errors::CargoResult;
7979
use crate::util::CanonicalUrl;
8080
use anyhow::Context as _;
8181
use cargo_util::paths;
82+
use cargo_util_schemas::core::PartialVersion;
8283
use std::collections::{HashMap, HashSet};
8384
use tracing::{debug, trace};
8485

@@ -357,14 +358,16 @@ pub fn resolve_with_previous<'gctx>(
357358
version_prefs.version_ordering(VersionOrdering::MinimumVersionsFirst)
358359
}
359360
if ws.resolve_honors_rust_version() {
360-
let rust_version = if let Some(ver) = ws.rust_version() {
361-
ver.clone().into_partial()
362-
} else {
361+
let mut rust_versions: Vec<_> = ws
362+
.members()
363+
.filter_map(|p| p.rust_version().map(|rv| rv.as_partial().clone()))
364+
.collect();
365+
if rust_versions.is_empty() {
363366
let rustc = ws.gctx().load_global_rustc(Some(ws))?;
364-
let rustc_version = rustc.version.clone().into();
365-
rustc_version
366-
};
367-
version_prefs.max_rust_version(Some(rust_version));
367+
let rust_version: PartialVersion = rustc.version.clone().into();
368+
rust_versions.push(rust_version);
369+
}
370+
version_prefs.rust_versions(rust_versions);
368371
}
369372

370373
let avoid_patch_ids = if register_patches {
@@ -422,7 +425,7 @@ pub fn resolve_with_previous<'gctx>(
422425
&replace,
423426
registry,
424427
&version_prefs,
425-
ResolveVersion::with_rust_version(ws.rust_version()),
428+
ResolveVersion::with_rust_version(ws.lowest_rust_version()),
426429
Some(ws.gctx()),
427430
)?;
428431

src/doc/src/reference/unstable.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,9 @@ This was stabilized in 1.79 in [#13608](https://github.com/rust-lang/cargo/pull/
348348
- `package.edition = "2024"` (only in workspace root)
349349

350350
The resolver will prefer dependencies with a `package.rust-version` that is the same or older than your project's MSRV.
351-
Your project's MSRV is determined by taking the lowest `package.rust-version` set among your workspace members.
351+
As the resolver is unable to determine which workspace members will eventually
352+
depend on a package when it is being selected, we prioritize versions based on
353+
how many workspace member MSRVs they are compatible with.
352354
If there is no MSRV set then your toolchain version will be used, allowing it to pick up the toolchain version from pinned in rustup (e.g. `rust-toolchain.toml`).
353355

354356
#### `resolver.incompatible-rust-versions`

tests/testsuite/rust_version.rs

+46-20
Original file line numberDiff line numberDiff line change
@@ -409,22 +409,36 @@ foo v0.0.1 ([ROOT]/foo)
409409

410410
#[cargo_test]
411411
fn resolve_with_multiple_rust_versions() {
412-
Package::new("only-newer", "1.6.0")
412+
Package::new(&format!("shared-only-newer"), "1.65.0")
413413
.rust_version("1.65.0")
414414
.file("src/lib.rs", "fn other_stuff() {}")
415415
.publish();
416-
Package::new("newer-and-older", "1.5.0")
417-
.rust_version("1.45.0")
418-
.file("src/lib.rs", "fn other_stuff() {}")
419-
.publish();
420-
Package::new("newer-and-older", "1.5.1")
421-
.rust_version("1.55.0")
416+
for ver in ["1.45.0", "1.55.0", "1.65.0"] {
417+
Package::new(&format!("shared-newer-and-older"), ver)
418+
.rust_version(ver)
419+
.file("src/lib.rs", "fn other_stuff() {}")
420+
.publish();
421+
}
422+
Package::new(&format!("lower-only-newer"), "1.65.0")
423+
.rust_version("1.65.0")
422424
.file("src/lib.rs", "fn other_stuff() {}")
423425
.publish();
424-
Package::new("newer-and-older", "1.6.0")
426+
for ver in ["1.45.0", "1.55.0"] {
427+
Package::new(&format!("lower-newer-and-older"), ver)
428+
.rust_version(ver)
429+
.file("src/lib.rs", "fn other_stuff() {}")
430+
.publish();
431+
}
432+
Package::new(&format!("higher-only-newer"), "1.65.0")
425433
.rust_version("1.65.0")
426434
.file("src/lib.rs", "fn other_stuff() {}")
427435
.publish();
436+
for ver in ["1.55.0", "1.65.0"] {
437+
Package::new(&format!("higher-newer-and-older"), ver)
438+
.rust_version(ver)
439+
.file("src/lib.rs", "fn other_stuff() {}")
440+
.publish();
441+
}
428442

429443
let p = project()
430444
.file(
@@ -441,8 +455,10 @@ fn resolve_with_multiple_rust_versions() {
441455
rust-version = "1.60.0"
442456
443457
[dependencies]
444-
only-newer = "1.0.0"
445-
newer-and-older = "1.0.0"
458+
higher-only-newer = "1"
459+
higher-newer-and-older = "1"
460+
shared-only-newer = "1"
461+
shared-newer-and-older = "1"
446462
"#,
447463
)
448464
.file("src/main.rs", "fn main() {}")
@@ -457,8 +473,10 @@ fn resolve_with_multiple_rust_versions() {
457473
rust-version = "1.50.0"
458474
459475
[dependencies]
460-
only-newer = "1.0.0"
461-
newer-and-older = "1.0.0"
476+
lower-only-newer = "1"
477+
lower-newer-and-older = "1"
478+
shared-only-newer = "1"
479+
shared-newer-and-older = "1"
462480
"#,
463481
)
464482
.file("lower/src/main.rs", "fn main() {}")
@@ -470,15 +488,17 @@ fn resolve_with_multiple_rust_versions() {
470488
.masquerade_as_nightly_cargo(&["msrv-policy"])
471489
.with_stderr_data(str![[r#"
472490
[UPDATING] `dummy-registry` index
473-
[LOCKING] 2 packages to latest compatible versions
491+
[LOCKING] 6 packages to latest compatible versions
474492
475493
"#]])
476494
.run();
477495
p.cargo("tree")
478496
.with_stdout_data(str![[r#"
479497
higher v0.0.1 ([ROOT]/foo)
480-
├── newer-and-older v1.6.0
481-
└── only-newer v1.6.0
498+
├── higher-newer-and-older v1.65.0
499+
├── higher-only-newer v1.65.0
500+
├── shared-newer-and-older v1.65.0
501+
└── shared-only-newer v1.65.0
482502
483503
"#]])
484504
.run();
@@ -489,17 +509,23 @@ higher v0.0.1 ([ROOT]/foo)
489509
.masquerade_as_nightly_cargo(&["msrv-policy"])
490510
.with_stderr_data(str![[r#"
491511
[UPDATING] `dummy-registry` index
492-
[LOCKING] 2 packages to latest Rust 1.50.0 compatible versions
493-
[ADDING] newer-and-older v1.5.0 (available: v1.6.0, requires Rust 1.65.0)
494-
[ADDING] only-newer v1.6.0 (requires Rust 1.65.0)
512+
[LOCKING] 6 packages to latest Rust 1.50.0 compatible versions
513+
[ADDING] higher-newer-and-older v1.55.0 (available: v1.65.0, requires Rust 1.65.0)
514+
[ADDING] higher-only-newer v1.65.0 (requires Rust 1.65.0)
515+
[ADDING] lower-newer-and-older v1.45.0 (available: v1.55.0, requires Rust 1.55.0)
516+
[ADDING] lower-only-newer v1.65.0 (requires Rust 1.65.0)
517+
[ADDING] shared-newer-and-older v1.45.0 (available: v1.65.0, requires Rust 1.65.0)
518+
[ADDING] shared-only-newer v1.65.0 (requires Rust 1.65.0)
495519
496520
"#]])
497521
.run();
498522
p.cargo("tree")
499523
.with_stdout_data(str![[r#"
500524
higher v0.0.1 ([ROOT]/foo)
501-
├── newer-and-older v1.5.0
502-
└── only-newer v1.6.0
525+
├── higher-newer-and-older v1.55.0
526+
├── higher-only-newer v1.65.0
527+
├── shared-newer-and-older v1.45.0
528+
└── shared-only-newer v1.65.0
503529
504530
"#]])
505531
.run();

0 commit comments

Comments
 (0)