Skip to content

Commit 145bec3

Browse files
committed
update: report error for non-matching package specs with --breaking
When using `cargo update --breaking <spec>`, package specifications that don't match any upgradeable dependency are now properly reported as errors instead of being silently ignored. The implementation tracks which specs match direct dependencies during the upgrade process. After processing all workspace members, it validates that each requested spec either: 1. Matched a direct registry dependency (and was processed), or 2. Exists in the lockfile but cannot be upgraded Specs that match neither category produce clear error messages: - "did not match any packages" for completely non-existent packages - "matched a package... but did not match any direct dependencies" for transitive/non-upgradeable packages, with a note explaining that --breaking can only upgrade direct dependencies Multiple errors are collected and reported together for better UX. This fixes the confusing behavior where users could specify packages that don't exist without receiving any feedback.
1 parent d4f36b2 commit 145bec3

File tree

2 files changed

+148
-19
lines changed

2 files changed

+148
-19
lines changed

src/cargo/ops/cargo_update.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::util::{CargoResult, VersionExt};
1717
use crate::util::{OptVersionReq, style};
1818
use anyhow::Context as _;
1919
use cargo_util_schemas::core::PartialVersion;
20-
use indexmap::IndexMap;
20+
use indexmap::{IndexMap, IndexSet};
2121
use itertools::Itertools;
2222
use semver::{Op, Version, VersionReq};
2323
use std::cmp::Ordering;
@@ -238,6 +238,8 @@ pub fn upgrade_manifests(
238238
let mut registry = ws.package_registry()?;
239239
registry.lock_patches();
240240

241+
let mut remaining_specs: IndexSet<_> = to_update.iter().cloned().collect();
242+
241243
for member in ws.members_mut().sorted() {
242244
debug!("upgrading manifest for `{}`", member.name());
243245

@@ -252,11 +254,58 @@ pub fn upgrade_manifests(
252254
&mut registry,
253255
&mut upgrades,
254256
&mut upgrade_messages,
257+
&mut remaining_specs,
255258
d,
256259
)
257260
})?;
258261
}
259262

263+
if !remaining_specs.is_empty() {
264+
let previous_resolve = ops::load_pkg_lockfile(ws)?;
265+
let plural = if remaining_specs.len() == 1 { "" } else { "s" };
266+
267+
let mut error_msg = format!(
268+
"package ID specification{plural} did not match any direct dependencies that could be upgraded"
269+
);
270+
271+
let mut transitive_specs = Vec::new();
272+
for spec in &remaining_specs {
273+
error_msg.push_str(&format!("\n {spec}"));
274+
275+
// Check if spec is in the lockfile (could be transitive)
276+
let in_lockfile = if let Some(ref resolve) = previous_resolve {
277+
spec.query(resolve.iter()).is_ok()
278+
} else {
279+
false
280+
};
281+
282+
// Check if spec matches any direct dependency in the workspace
283+
let matches_direct_dep = ws.members().any(|member| {
284+
member.dependencies().iter().any(|dep| {
285+
spec.name() == dep.package_name().as_str()
286+
&& dep.source_id().is_registry()
287+
&& spec.url().map_or(true, |url| url == dep.source_id().url())
288+
&& spec
289+
.version()
290+
.map_or(true, |v| dep.version_req().matches(&v))
291+
})
292+
});
293+
294+
// Track transitive specs for notes at the end
295+
if in_lockfile && !matches_direct_dep {
296+
transitive_specs.push(spec);
297+
}
298+
}
299+
300+
for spec in transitive_specs {
301+
error_msg.push_str(&format!(
302+
"\nnote: `{spec}` exists as a transitive dependency but those are not available for upgrading through `--breaking`"
303+
));
304+
}
305+
306+
anyhow::bail!("{error_msg}");
307+
}
308+
260309
Ok(upgrades)
261310
}
262311

@@ -266,6 +315,7 @@ fn upgrade_dependency(
266315
registry: &mut PackageRegistry<'_>,
267316
upgrades: &mut UpgradeMap,
268317
upgrade_messages: &mut HashSet<String>,
318+
remaining_specs: &mut IndexSet<PackageIdSpec>,
269319
dependency: Dependency,
270320
) -> CargoResult<Dependency> {
271321
let name = dependency.package_name();
@@ -367,6 +417,10 @@ fn upgrade_dependency(
367417

368418
upgrades.insert((name.to_string(), dependency.source_id()), latest.clone());
369419

420+
// Remove this spec from remaining_specs since we successfully upgraded it
421+
remaining_specs
422+
.retain(|spec| !(spec.name() == name.as_str() && dependency.source_id().is_registry()));
423+
370424
let req = OptVersionReq::Req(VersionReq::parse(&latest.to_string())?);
371425
let mut dep = dependency.clone();
372426
dep.set_version_req(req);

tests/testsuite/update.rs

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2161,19 +2161,30 @@ fn update_breaking_specific_packages_that_wont_update() {
21612161
Package::new("non-semver", "2.0.0").publish();
21622162
Package::new("transitive-incompatible", "2.0.0").publish();
21632163

2164-
// Transitive dependencies are silently ignored
2164+
// Test that transitive dependencies produce helpful errors
21652165
p.cargo("update -Zunstable-options --breaking transitive-compatible transitive-incompatible")
21662166
.masquerade_as_nightly_cargo(&["update-breaking"])
2167+
.with_status(101)
21672168
.with_stderr_data(str![[r#"
2169+
[ERROR] package ID specifications did not match any direct dependencies that could be upgraded
2170+
transitive-compatible
2171+
transitive-incompatible
2172+
[NOTE] `transitive-compatible` exists as a transitive dependency but those are not available for upgrading through `--breaking`
2173+
[NOTE] `transitive-incompatible` exists as a transitive dependency but those are not available for upgrading through `--breaking`
21682174
21692175
"#]])
21702176
.run();
21712177

2172-
// Renamed, non-semver, no-breaking-update dependencies are silently ignored
2178+
// Test that renamed, non-semver, no-breaking-update dependencies produce errors
21732179
p.cargo("update -Zunstable-options --breaking compatible renamed-from non-semver")
21742180
.masquerade_as_nightly_cargo(&["update-breaking"])
2181+
.with_status(101)
21752182
.with_stderr_data(str![[r#"
2176-
[UPDATING] `[..]` index
2183+
[UPDATING] `dummy-registry` index
2184+
[ERROR] package ID specifications did not match any direct dependencies that could be upgraded
2185+
compatible
2186+
renamed-from
2187+
non-semver
21772188
21782189
"#]])
21792190
.run();
@@ -2285,13 +2296,23 @@ Caused by:
22852296
// Spec version not matching our current dependencies
22862297
p.cargo("update -Zunstable-options --breaking incompatible@2.0.0")
22872298
.masquerade_as_nightly_cargo(&["update-breaking"])
2288-
.with_stderr_data(str![[r#""#]])
2299+
.with_status(101)
2300+
.with_stderr_data(str![[r#"
2301+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2302+
incompatible@2.0.0
2303+
2304+
"#]])
22892305
.run();
22902306

22912307
// Spec source not matching our current dependencies
22922308
p.cargo("update -Zunstable-options --breaking https://alternative.com#incompatible@1.0.0")
22932309
.masquerade_as_nightly_cargo(&["update-breaking"])
2294-
.with_stderr_data(str![[r#""#]])
2310+
.with_status(101)
2311+
.with_stderr_data(str![[r#"
2312+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2313+
https://alternative.com/#incompatible@1.0.0
2314+
2315+
"#]])
22952316
.run();
22962317

22972318
// Accepted spec
@@ -2322,21 +2343,34 @@ Caused by:
23222343
// Spec matches a dependency that will not be upgraded
23232344
p.cargo("update -Zunstable-options --breaking compatible@1.0.0")
23242345
.masquerade_as_nightly_cargo(&["update-breaking"])
2346+
.with_status(101)
23252347
.with_stderr_data(str![[r#"
23262348
[UPDATING] `[..]` index
2349+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2350+
compatible@1.0.0
23272351
23282352
"#]])
23292353
.run();
23302354

23312355
// Non-existing versions
23322356
p.cargo("update -Zunstable-options --breaking incompatible@9.0.0")
23332357
.masquerade_as_nightly_cargo(&["update-breaking"])
2334-
.with_stderr_data(str![[r#""#]])
2358+
.with_status(101)
2359+
.with_stderr_data(str![[r#"
2360+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2361+
incompatible@9.0.0
2362+
2363+
"#]])
23352364
.run();
23362365

23372366
p.cargo("update -Zunstable-options --breaking compatible@9.0.0")
23382367
.masquerade_as_nightly_cargo(&["update-breaking"])
2339-
.with_stderr_data(str![[r#""#]])
2368+
.with_status(101)
2369+
.with_stderr_data(str![[r#"
2370+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2371+
compatible@9.0.0
2372+
2373+
"#]])
23402374
.run();
23412375
}
23422376

@@ -2397,8 +2431,11 @@ fn update_breaking_spec_version_transitive() {
23972431
// But not the transitive one, because bar is not a workspace member
23982432
p.cargo("update -Zunstable-options --breaking dep@1.1")
23992433
.masquerade_as_nightly_cargo(&["update-breaking"])
2434+
.with_status(101)
24002435
.with_stderr_data(str![[r#"
24012436
[UPDATING] `[..]` index
2437+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2438+
dep@1.1
24022439
24032440
"#]])
24042441
.run();
@@ -2656,12 +2693,15 @@ fn update_breaking_pre_release_downgrade() {
26562693

26572694
// The purpose of this test is
26582695
// to demonstrate that `update --breaking` will not try to downgrade to the latest stable version (1.7.0),
2659-
// but will rather keep the latest pre-release (2.0.0-beta.21).
2696+
// but will error because the dependency uses an exact version (not caret).
26602697
Package::new("bar", "1.7.0").publish();
26612698
p.cargo("update -Zunstable-options --breaking bar")
26622699
.masquerade_as_nightly_cargo(&["update-breaking"])
2700+
.with_status(101)
26632701
.with_stderr_data(str![[r#"
26642702
[UPDATING] `dummy-registry` index
2703+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2704+
bar
26652705
26662706
"#]])
26672707
.run();
@@ -2690,21 +2730,27 @@ fn update_breaking_pre_release_upgrade() {
26902730

26912731
p.cargo("generate-lockfile").run();
26922732

2693-
// TODO: `2.0.0-beta.21` can be upgraded to `2.0.0-beta.22`
2733+
// `2.0.0-beta.21` cannot be upgraded with --breaking because it uses an exact version (not caret)
26942734
Package::new("bar", "2.0.0-beta.22").publish();
26952735
p.cargo("update -Zunstable-options --breaking bar")
26962736
.masquerade_as_nightly_cargo(&["update-breaking"])
2737+
.with_status(101)
26972738
.with_stderr_data(str![[r#"
26982739
[UPDATING] `dummy-registry` index
2740+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2741+
bar
26992742
27002743
"#]])
27012744
.run();
2702-
// TODO: `2.0.0-beta.21` can be upgraded to `2.0.0`
2745+
// `2.0.0-beta.21` cannot be upgraded to `2.0.0` with --breaking because it uses an exact version (not caret)
27032746
Package::new("bar", "2.0.0").publish();
27042747
p.cargo("update -Zunstable-options --breaking bar")
27052748
.masquerade_as_nightly_cargo(&["update-breaking"])
2749+
.with_status(101)
27062750
.with_stderr_data(str![[r#"
27072751
[UPDATING] `dummy-registry` index
2752+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2753+
bar
27082754
27092755
"#]])
27102756
.run();
@@ -2788,21 +2834,32 @@ fn update_breaking_missing_package_error() {
27882834
.add_dep(Dependency::new("transitive", "1.0.0").build())
27892835
.publish();
27902836

2791-
// This test demonstrates the current buggy behavior where invalid package
2792-
// specs are silently ignored instead of reporting an error. A subsequent
2793-
// commit will fix this behavior and update this test to verify proper
2794-
// error reporting.
2795-
2796-
// Non-existent package is silently ignored
2837+
// Non-existent package reports an error
27972838
p.cargo("update -Zunstable-options --breaking no_such_crate")
27982839
.masquerade_as_nightly_cargo(&["update-breaking"])
2840+
.with_status(101)
27992841
.with_stderr_data(str![[r#"
2842+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2843+
no_such_crate
28002844
28012845
"#]])
28022846
.run();
28032847

2804-
// Valid package processes, invalid package silently ignored
2848+
// Valid package processes, invalid package reports error
28052849
p.cargo("update -Zunstable-options --breaking bar no_such_crate")
2850+
.masquerade_as_nightly_cargo(&["update-breaking"])
2851+
.with_status(101)
2852+
.with_stderr_data(str![[r#"
2853+
[UPDATING] `dummy-registry` index
2854+
[UPGRADING] bar ^1.0 -> ^2.0
2855+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2856+
no_such_crate
2857+
2858+
"#]])
2859+
.run();
2860+
2861+
// Successfully upgrade bar to add transitive to lockfile
2862+
p.cargo("update -Zunstable-options --breaking bar")
28062863
.masquerade_as_nightly_cargo(&["update-breaking"])
28072864
.with_stderr_data(str![[r#"
28082865
[UPDATING] `dummy-registry` index
@@ -2814,10 +2871,28 @@ fn update_breaking_missing_package_error() {
28142871
"#]])
28152872
.run();
28162873

2817-
// Transitive dependency is silently ignored (produces no output)
2874+
// Transitive dependency reports helpful error
28182875
p.cargo("update -Zunstable-options --breaking transitive")
28192876
.masquerade_as_nightly_cargo(&["update-breaking"])
2877+
.with_status(101)
2878+
.with_stderr_data(str![[r#"
2879+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2880+
transitive
2881+
[NOTE] `transitive` exists as a transitive dependency but those are not available for upgrading through `--breaking`
2882+
2883+
"#]])
2884+
.run();
2885+
2886+
// Multiple error types reported together
2887+
p.cargo("update -Zunstable-options --breaking no_such_crate transitive another_missing")
2888+
.masquerade_as_nightly_cargo(&["update-breaking"])
2889+
.with_status(101)
28202890
.with_stderr_data(str![[r#"
2891+
[ERROR] package ID specifications did not match any direct dependencies that could be upgraded
2892+
no_such_crate
2893+
transitive
2894+
another_missing
2895+
[NOTE] `transitive` exists as a transitive dependency but those are not available for upgrading through `--breaking`
28212896
28222897
"#]])
28232898
.run();

0 commit comments

Comments
 (0)