Skip to content

Commit 80f1a5d

Browse files
committed
Auto merge of #11688 - epage:minimal, r=Eh2406
feat(resolver): `-Zdirect-minimal-versions` This is an alternative to `-Zminimal-versions` as discussed in #5657. Problems with `-Zminimal-versions` includes - Requires the root most dependencies to verify it and we then percolate that up the stack. This requires a massive level of cooperation to accomplish and so far there have been mixed results with it to the point that cargo's unstable documentation discourages its use. - Users expect `cargo check -Zminimal-versions` to force resolving to minimal but it doesn't as the default maximal resolve is compatible and requires `cargo update -Zminimal-versions` - Different compatible versions might be selected, breaking interop between crates, changing feature unification, and breaking `-sys` crates without bad `links` `-Zdirect-minimal-versions` instead only applies this rule to your direct dependencies, allowing anyone in the stack to immediately adopt it, independent of everyone else. Special notes - Living up to the name and the existing design, this ignores yanked crates. This makes sense for `^1.1` version requirements but might look weird for `^1.1.1` version requirements as it could select `1.1.2`. - This will error if an indirect dependency requires a newer version. Your version requirement will need to capture what you use **and** all of you dependencies. An alternative design would have tried to merge the result of minimum versions for direct dependencies and maximum versions for indirect dependencies. This would have been complex and led to weird corner cases, making it harder to predict. I also suspect the value gained would be relatively low as you can't verify that version requirement in any other way. - This also means discrepancies between `dependencies` and `dev-dependencies` are errors - The error could be improved to call out that this was from minimal versions but I felt getting this out now and starting to collect feedback was more important. One advantage of this approach over `-Zminimal-versions` is that it removes most of the problems that [cargo-minimal-versions](https://github.com/taiki-e/cargo-minimal-versions) tried to workaround. As for the implementation, this might not be the most elegant solution but it works and we can always iterate and improve on it in the future. - We keep the state as a `bool` throughout but compensate for that by explicitly creating a variable to abstract away constants - The name changes depending on the context, from `direct_minimal_version` when dealing with the unstable flag to `first_minimal_version` when the concept of "direct" is lost to `first_version` when we split off the ordering concept into a separate variable - Packages that respect `direct_minimal_versions` are determined by whether they are the top-level `summaries` that get past into `resolve` ### What does this PR try to resolve? The primary use case is verifying version requirements to avoid depending on something newer than might be available in a dependent For this to help the MSRV use case, the crate author must directly depend on all indirect dependencies where their latest release has too new of an MSRV but at least they can do so with the `^` operator, rather than `<` and breaking the ecosystem. ### How should we test and review this PR? The first two commits add tests using `-Zminimal-versions`. The commit that adds `-Zdirect-minimal-versions` updates the tests, highlighting the differences in behavior. ### Additional information Potential areas of conversation for stablization - Flag name - Handling of yanked (pick first non-yanked, pick yanked, error) - Quality of error message - Should the package have a "memory" of this flag being set by writing it to the lockfile? Potential future work - Stablize this - Remove `-Zminimal-versions` - Update `cargo publish`s `--verify` step to use this. - The challenge is this won't be using the packaged `Cargo.lock` which probably should also be verified.
2 parents 64b0e79 + 1d153f1 commit 80f1a5d

File tree

8 files changed

+416
-44
lines changed

8 files changed

+416
-44
lines changed

crates/resolver-tests/tests/resolve.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,64 @@ proptest! {
100100
}
101101
}
102102

103+
/// NOTE: if you think this test has failed spuriously see the note at the top of this macro.
104+
#[test]
105+
fn prop_direct_minimum_version_error_implications(
106+
PrettyPrintRegistry(input) in registry_strategy(50, 20, 60)
107+
) {
108+
let mut config = Config::default().unwrap();
109+
config.nightly_features_allowed = true;
110+
config
111+
.configure(
112+
1,
113+
false,
114+
None,
115+
false,
116+
false,
117+
false,
118+
&None,
119+
&["direct-minimal-versions".to_string()],
120+
&[],
121+
)
122+
.unwrap();
123+
124+
let reg = registry(input.clone());
125+
// there is only a small chance that any one
126+
// crate will be interesting.
127+
// So we try some of the most complicated.
128+
for this in input.iter().rev().take(10) {
129+
// direct-minimal-versions reduces the number of available solutions, so we verify that
130+
// we do not come up with solutions maximal versions does not
131+
let res = resolve(
132+
vec![dep_req(&this.name(), &format!("={}", this.version()))],
133+
&reg,
134+
);
135+
136+
let mres = resolve_with_config(
137+
vec![dep_req(&this.name(), &format!("={}", this.version()))],
138+
&reg,
139+
&config,
140+
);
141+
142+
if res.is_err() {
143+
prop_assert!(
144+
mres.is_err(),
145+
"direct-minimal-versions should not have more solutions than the regular, maximal resolver but found one when resolving `{} = \"={}\"`",
146+
this.name(),
147+
this.version()
148+
)
149+
}
150+
if mres.is_ok() {
151+
prop_assert!(
152+
res.is_ok(),
153+
"direct-minimal-versions should not have more solutions than the regular, maximal resolver but found one when resolving `{} = \"={}\"`",
154+
this.name(),
155+
this.version()
156+
)
157+
}
158+
}
159+
}
160+
103161
/// NOTE: if you think this test has failed spuriously see the note at the top of this macro.
104162
#[test]
105163
fn prop_removing_a_dep_cant_break(

src/cargo/core/features.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,7 @@ unstable_cli_options!(
717717
dual_proc_macros: bool = ("Build proc-macros for both the host and the target"),
718718
features: Option<Vec<String>> = (HIDDEN),
719719
minimal_versions: bool = ("Resolve minimal dependency versions instead of maximum"),
720+
direct_minimal_versions: bool = ("Resolve minimal dependency versions instead of maximum (direct dependencies only)"),
720721
mtime_on_use: bool = ("Configure Cargo to update the mtime of used files"),
721722
no_index_update: bool = ("Do not update the registry index even if the cache is outdated"),
722723
panic_abort_tests: bool = ("Enable support to run tests with -Cpanic=abort"),
@@ -947,6 +948,7 @@ impl CliUnstable {
947948
"no-index-update" => self.no_index_update = parse_empty(k, v)?,
948949
"avoid-dev-deps" => self.avoid_dev_deps = parse_empty(k, v)?,
949950
"minimal-versions" => self.minimal_versions = parse_empty(k, v)?,
951+
"direct-minimal-versions" => self.direct_minimal_versions = parse_empty(k, v)?,
950952
"advanced-env" => self.advanced_env = parse_empty(k, v)?,
951953
"config-include" => self.config_include = parse_empty(k, v)?,
952954
"check-cfg" => {

src/cargo/core/resolver/dep_cache.rs

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,13 @@ pub struct RegistryQueryer<'a> {
3636
/// versions first. That allows `cargo update -Z minimal-versions` which will
3737
/// specify minimum dependency versions to be used.
3838
minimal_versions: bool,
39-
/// a cache of `Candidate`s that fulfil a `Dependency`
40-
registry_cache: HashMap<Dependency, Poll<Rc<Vec<Summary>>>>,
39+
/// a cache of `Candidate`s that fulfil a `Dependency` (and whether `first_minimal_version`)
40+
registry_cache: HashMap<(Dependency, bool), Poll<Rc<Vec<Summary>>>>,
4141
/// a cache of `Dependency`s that are required for a `Summary`
42+
///
43+
/// HACK: `first_minimal_version` is not kept in the cache key is it is 1:1 with
44+
/// `parent.is_none()` (the first element of the cache key) as it doesn't change through
45+
/// execution.
4246
summary_cache: HashMap<
4347
(Option<PackageId>, Summary, ResolveOpts),
4448
(Rc<(HashSet<InternedString>, Rc<Vec<DepInfo>>)>, bool),
@@ -96,8 +100,13 @@ impl<'a> RegistryQueryer<'a> {
96100
/// any candidates are returned which match an override then the override is
97101
/// applied by performing a second query for what the override should
98102
/// return.
99-
pub fn query(&mut self, dep: &Dependency) -> Poll<CargoResult<Rc<Vec<Summary>>>> {
100-
if let Some(out) = self.registry_cache.get(dep).cloned() {
103+
pub fn query(
104+
&mut self,
105+
dep: &Dependency,
106+
first_minimal_version: bool,
107+
) -> Poll<CargoResult<Rc<Vec<Summary>>>> {
108+
let registry_cache_key = (dep.clone(), first_minimal_version);
109+
if let Some(out) = self.registry_cache.get(&registry_cache_key).cloned() {
101110
return out.map(Result::Ok);
102111
}
103112

@@ -106,7 +115,8 @@ impl<'a> RegistryQueryer<'a> {
106115
ret.push(s);
107116
})?;
108117
if ready.is_pending() {
109-
self.registry_cache.insert(dep.clone(), Poll::Pending);
118+
self.registry_cache
119+
.insert((dep.clone(), first_minimal_version), Poll::Pending);
110120
return Poll::Pending;
111121
}
112122
for summary in ret.iter() {
@@ -128,7 +138,8 @@ impl<'a> RegistryQueryer<'a> {
128138
let mut summaries = match self.registry.query_vec(dep, QueryKind::Exact)? {
129139
Poll::Ready(s) => s.into_iter(),
130140
Poll::Pending => {
131-
self.registry_cache.insert(dep.clone(), Poll::Pending);
141+
self.registry_cache
142+
.insert((dep.clone(), first_minimal_version), Poll::Pending);
132143
return Poll::Pending;
133144
}
134145
};
@@ -192,18 +203,18 @@ impl<'a> RegistryQueryer<'a> {
192203

193204
// When we attempt versions for a package we'll want to do so in a sorted fashion to pick
194205
// the "best candidates" first. VersionPreferences implements this notion.
195-
self.version_prefs.sort_summaries(
196-
&mut ret,
197-
if self.minimal_versions {
198-
VersionOrdering::MinimumVersionsFirst
199-
} else {
200-
VersionOrdering::MaximumVersionsFirst
201-
},
202-
);
206+
let ordering = if first_minimal_version || self.minimal_versions {
207+
VersionOrdering::MinimumVersionsFirst
208+
} else {
209+
VersionOrdering::MaximumVersionsFirst
210+
};
211+
let first_version = first_minimal_version;
212+
self.version_prefs
213+
.sort_summaries(&mut ret, ordering, first_version);
203214

204215
let out = Poll::Ready(Rc::new(ret));
205216

206-
self.registry_cache.insert(dep.clone(), out.clone());
217+
self.registry_cache.insert(registry_cache_key, out.clone());
207218

208219
out.map(Result::Ok)
209220
}
@@ -218,6 +229,7 @@ impl<'a> RegistryQueryer<'a> {
218229
parent: Option<PackageId>,
219230
candidate: &Summary,
220231
opts: &ResolveOpts,
232+
first_minimal_version: bool,
221233
) -> ActivateResult<Rc<(HashSet<InternedString>, Rc<Vec<DepInfo>>)>> {
222234
// if we have calculated a result before, then we can just return it,
223235
// as it is a "pure" query of its arguments.
@@ -237,22 +249,24 @@ impl<'a> RegistryQueryer<'a> {
237249
let mut all_ready = true;
238250
let mut deps = deps
239251
.into_iter()
240-
.filter_map(|(dep, features)| match self.query(&dep) {
241-
Poll::Ready(Ok(candidates)) => Some(Ok((dep, candidates, features))),
242-
Poll::Pending => {
243-
all_ready = false;
244-
// we can ignore Pending deps, resolve will be repeatedly called
245-
// until there are none to ignore
246-
None
247-
}
248-
Poll::Ready(Err(e)) => Some(Err(e).with_context(|| {
249-
format!(
250-
"failed to get `{}` as a dependency of {}",
251-
dep.package_name(),
252-
describe_path_in_context(cx, &candidate.package_id()),
253-
)
254-
})),
255-
})
252+
.filter_map(
253+
|(dep, features)| match self.query(&dep, first_minimal_version) {
254+
Poll::Ready(Ok(candidates)) => Some(Ok((dep, candidates, features))),
255+
Poll::Pending => {
256+
all_ready = false;
257+
// we can ignore Pending deps, resolve will be repeatedly called
258+
// until there are none to ignore
259+
None
260+
}
261+
Poll::Ready(Err(e)) => Some(Err(e).with_context(|| {
262+
format!(
263+
"failed to get `{}` as a dependency of {}",
264+
dep.package_name(),
265+
describe_path_in_context(cx, &candidate.package_id()),
266+
)
267+
})),
268+
},
269+
)
256270
.collect::<CargoResult<Vec<DepInfo>>>()?;
257271

258272
// Attempt to resolve dependencies with fewer candidates before trying

src/cargo/core/resolver/mod.rs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,21 @@ pub fn resolve(
133133
Some(config) => config.cli_unstable().minimal_versions,
134134
None => false,
135135
};
136+
let direct_minimal_versions = match config {
137+
Some(config) => config.cli_unstable().direct_minimal_versions,
138+
None => false,
139+
};
136140
let mut registry =
137141
RegistryQueryer::new(registry, replacements, version_prefs, minimal_versions);
138142
let cx = loop {
139143
let cx = Context::new(check_public_visible_dependencies);
140-
let cx = activate_deps_loop(cx, &mut registry, summaries, config)?;
144+
let cx = activate_deps_loop(
145+
cx,
146+
&mut registry,
147+
summaries,
148+
direct_minimal_versions,
149+
config,
150+
)?;
141151
if registry.reset_pending() {
142152
break cx;
143153
} else {
@@ -189,6 +199,7 @@ fn activate_deps_loop(
189199
mut cx: Context,
190200
registry: &mut RegistryQueryer<'_>,
191201
summaries: &[(Summary, ResolveOpts)],
202+
direct_minimal_versions: bool,
192203
config: Option<&Config>,
193204
) -> CargoResult<Context> {
194205
let mut backtrack_stack = Vec::new();
@@ -201,7 +212,14 @@ fn activate_deps_loop(
201212
// Activate all the initial summaries to kick off some work.
202213
for &(ref summary, ref opts) in summaries {
203214
debug!("initial activation: {}", summary.package_id());
204-
let res = activate(&mut cx, registry, None, summary.clone(), opts);
215+
let res = activate(
216+
&mut cx,
217+
registry,
218+
None,
219+
summary.clone(),
220+
direct_minimal_versions,
221+
opts,
222+
);
205223
match res {
206224
Ok(Some((frame, _))) => remaining_deps.push(frame),
207225
Ok(None) => (),
@@ -399,7 +417,15 @@ fn activate_deps_loop(
399417
dep.package_name(),
400418
candidate.version()
401419
);
402-
let res = activate(&mut cx, registry, Some((&parent, &dep)), candidate, &opts);
420+
let direct_minimal_version = false; // this is an indirect dependency
421+
let res = activate(
422+
&mut cx,
423+
registry,
424+
Some((&parent, &dep)),
425+
candidate,
426+
direct_minimal_version,
427+
&opts,
428+
);
403429

404430
let successfully_activated = match res {
405431
// Success! We've now activated our `candidate` in our context
@@ -611,6 +637,7 @@ fn activate(
611637
registry: &mut RegistryQueryer<'_>,
612638
parent: Option<(&Summary, &Dependency)>,
613639
candidate: Summary,
640+
first_minimal_version: bool,
614641
opts: &ResolveOpts,
615642
) -> ActivateResult<Option<(DepsFrame, Duration)>> {
616643
let candidate_pid = candidate.package_id();
@@ -662,8 +689,13 @@ fn activate(
662689
};
663690

664691
let now = Instant::now();
665-
let (used_features, deps) =
666-
&*registry.build_deps(cx, parent.map(|p| p.0.package_id()), &candidate, opts)?;
692+
let (used_features, deps) = &*registry.build_deps(
693+
cx,
694+
parent.map(|p| p.0.package_id()),
695+
&candidate,
696+
opts,
697+
first_minimal_version,
698+
)?;
667699

668700
// Record what list of features is active for this package.
669701
if !used_features.is_empty() {
@@ -856,11 +888,14 @@ fn generalize_conflicting(
856888
})
857889
{
858890
for critical_parents_dep in critical_parents_deps.iter() {
891+
// We only want `first_minimal_version=true` for direct dependencies of workspace
892+
// members which isn't the case here as this has a `parent`
893+
let first_minimal_version = false;
859894
// A dep is equivalent to one of the things it can resolve to.
860895
// Thus, if all the things it can resolve to have already ben determined
861896
// to be conflicting, then we can just say that we conflict with the parent.
862897
if let Some(others) = registry
863-
.query(critical_parents_dep)
898+
.query(critical_parents_dep, first_minimal_version)
864899
.expect("an already used dep now error!?")
865900
.expect("an already used dep now pending!?")
866901
.iter()

src/cargo/core/resolver/version_prefs.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ impl VersionPreferences {
4242
/// Sort the given vector of summaries in-place, with all summaries presumed to be for
4343
/// the same package. Preferred versions appear first in the result, sorted by
4444
/// `version_ordering`, followed by non-preferred versions sorted the same way.
45-
pub fn sort_summaries(&self, summaries: &mut Vec<Summary>, version_ordering: VersionOrdering) {
45+
pub fn sort_summaries(
46+
&self,
47+
summaries: &mut Vec<Summary>,
48+
version_ordering: VersionOrdering,
49+
first_version: bool,
50+
) {
4651
let should_prefer = |pkg_id: &PackageId| {
4752
self.try_to_use.contains(pkg_id)
4853
|| self
@@ -66,6 +71,9 @@ impl VersionPreferences {
6671
_ => previous_cmp,
6772
}
6873
});
74+
if first_version {
75+
let _ = summaries.split_off(1);
76+
}
6977
}
7078
}
7179

@@ -115,13 +123,13 @@ mod test {
115123
summ("foo", "1.0.9"),
116124
];
117125

118-
vp.sort_summaries(&mut summaries, VersionOrdering::MaximumVersionsFirst);
126+
vp.sort_summaries(&mut summaries, VersionOrdering::MaximumVersionsFirst, false);
119127
assert_eq!(
120128
describe(&summaries),
121129
"foo/1.2.3, foo/1.2.4, foo/1.1.0, foo/1.0.9".to_string()
122130
);
123131

124-
vp.sort_summaries(&mut summaries, VersionOrdering::MinimumVersionsFirst);
132+
vp.sort_summaries(&mut summaries, VersionOrdering::MinimumVersionsFirst, false);
125133
assert_eq!(
126134
describe(&summaries),
127135
"foo/1.2.3, foo/1.0.9, foo/1.1.0, foo/1.2.4".to_string()
@@ -140,13 +148,13 @@ mod test {
140148
summ("foo", "1.0.9"),
141149
];
142150

143-
vp.sort_summaries(&mut summaries, VersionOrdering::MaximumVersionsFirst);
151+
vp.sort_summaries(&mut summaries, VersionOrdering::MaximumVersionsFirst, false);
144152
assert_eq!(
145153
describe(&summaries),
146154
"foo/1.2.3, foo/1.2.4, foo/1.1.0, foo/1.0.9".to_string()
147155
);
148156

149-
vp.sort_summaries(&mut summaries, VersionOrdering::MinimumVersionsFirst);
157+
vp.sort_summaries(&mut summaries, VersionOrdering::MinimumVersionsFirst, false);
150158
assert_eq!(
151159
describe(&summaries),
152160
"foo/1.2.3, foo/1.0.9, foo/1.1.0, foo/1.2.4".to_string()
@@ -166,13 +174,13 @@ mod test {
166174
summ("foo", "1.0.9"),
167175
];
168176

169-
vp.sort_summaries(&mut summaries, VersionOrdering::MaximumVersionsFirst);
177+
vp.sort_summaries(&mut summaries, VersionOrdering::MaximumVersionsFirst, false);
170178
assert_eq!(
171179
describe(&summaries),
172180
"foo/1.2.3, foo/1.1.0, foo/1.2.4, foo/1.0.9".to_string()
173181
);
174182

175-
vp.sort_summaries(&mut summaries, VersionOrdering::MinimumVersionsFirst);
183+
vp.sort_summaries(&mut summaries, VersionOrdering::MinimumVersionsFirst, false);
176184
assert_eq!(
177185
describe(&summaries),
178186
"foo/1.1.0, foo/1.2.3, foo/1.0.9, foo/1.2.4".to_string()

0 commit comments

Comments
 (0)