Skip to content

Commit cfde2ac

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 e619bf0 commit cfde2ac

File tree

2 files changed

+95
-10
lines changed

2 files changed

+95
-10
lines changed

src/cargo/ops/cargo_update.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ pub fn upgrade_manifests(
238238
let mut registry = ws.package_registry()?;
239239
registry.lock_patches();
240240

241+
let mut matched_specs = HashSet::new();
242+
241243
for member in ws.members_mut().sorted() {
242244
debug!("upgrading manifest for `{}`", member.name());
243245

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

263+
if !to_update.is_empty() {
264+
let mut errors = Vec::new();
265+
let previous_resolve = ops::load_pkg_lockfile(ws)?;
266+
267+
for spec in &to_update {
268+
if !matched_specs.contains(spec) {
269+
let matches_lockfile = if let Some(ref resolve) = previous_resolve {
270+
spec.query(resolve.iter()).is_ok()
271+
} else {
272+
false
273+
};
274+
275+
if matches_lockfile {
276+
errors.push(format!(
277+
"package ID specification `{}` matched a package in the dependency graph \
278+
but did not match any direct dependencies that could be upgraded.\n\
279+
Note: `--breaking` can only upgrade direct dependencies of workspace members.",
280+
spec
281+
));
282+
} else {
283+
errors.push(format!(
284+
"package ID specification `{}` did not match any packages",
285+
spec
286+
));
287+
}
288+
}
289+
}
290+
291+
if !errors.is_empty() {
292+
anyhow::bail!("{}", errors.join("\n\n"));
293+
}
294+
}
295+
260296
Ok(upgrades)
261297
}
262298

@@ -266,11 +302,18 @@ fn upgrade_dependency(
266302
registry: &mut PackageRegistry<'_>,
267303
upgrades: &mut UpgradeMap,
268304
upgrade_messages: &mut HashSet<String>,
305+
matched_specs: &mut HashSet<PackageIdSpec>,
269306
dependency: Dependency,
270307
) -> CargoResult<Dependency> {
271308
let name = dependency.package_name();
272309
let renamed_to = dependency.name_in_toml();
273310

311+
for spec in to_update.iter() {
312+
if spec.name() == name.as_str() && dependency.source_id().is_registry() {
313+
matched_specs.insert(spec.clone());
314+
}
315+
}
316+
274317
if name != renamed_to {
275318
trace!("skipping dependency renamed from `{name}` to `{renamed_to}`");
276319
return Ok(dependency);

tests/testsuite/update.rs

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2161,10 +2161,25 @@ 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-
p.cargo("update -Zunstable-options --breaking compatible renamed-from non-semver transitive-compatible transitive-incompatible")
2164+
// Test that transitive dependencies produce helpful errors
2165+
p.cargo("update -Zunstable-options --breaking transitive-compatible transitive-incompatible")
21652166
.masquerade_as_nightly_cargo(&["update-breaking"])
2167+
.with_status(101)
21662168
.with_stderr_data(str![[r#"
2167-
[UPDATING] `[..]` index
2169+
[ERROR] package ID specification `transitive-compatible` matched a package in the dependency graph but did not match any direct dependencies that could be upgraded.
2170+
Note: `--breaking` can only upgrade direct dependencies of workspace members.
2171+
2172+
package ID specification `transitive-incompatible` matched a package in the dependency graph but did not match any direct dependencies that could be upgraded.
2173+
Note: `--breaking` can only upgrade direct dependencies of workspace members.
2174+
2175+
"#]])
2176+
.run();
2177+
2178+
// Test that renamed, non-semver, no-breaking-update dependencies are silently skipped (no errors)
2179+
p.cargo("update -Zunstable-options --breaking compatible renamed-from non-semver")
2180+
.masquerade_as_nightly_cargo(&["update-breaking"])
2181+
.with_stderr_data(str![[r#"
2182+
[UPDATING] `dummy-registry` index
21682183
21692184
"#]])
21702185
.run();
@@ -2779,21 +2794,30 @@ fn update_breaking_missing_package_error() {
27792794
.add_dep(Dependency::new("transitive", "1.0.0").build())
27802795
.publish();
27812796

2782-
// This test demonstrates the current buggy behavior where invalid package
2783-
// specs are silently ignored instead of reporting an error. A subsequent
2784-
// commit will fix this behavior and update this test to verify proper
2785-
// error reporting.
2786-
2787-
// Non-existent package is silently ignored
2797+
// Non-existent package reports an error
27882798
p.cargo("update -Zunstable-options --breaking no_such_crate")
27892799
.masquerade_as_nightly_cargo(&["update-breaking"])
2800+
.with_status(101)
27902801
.with_stderr_data(str![[r#"
2802+
[ERROR] package ID specification `no_such_crate` did not match any packages
27912803
27922804
"#]])
27932805
.run();
27942806

2795-
// Valid package processes, invalid package silently ignored
2807+
// Valid package processes, invalid package reports error
27962808
p.cargo("update -Zunstable-options --breaking bar no_such_crate")
2809+
.masquerade_as_nightly_cargo(&["update-breaking"])
2810+
.with_status(101)
2811+
.with_stderr_data(str![[r#"
2812+
[UPDATING] `dummy-registry` index
2813+
[UPGRADING] bar ^1.0 -> ^2.0
2814+
[ERROR] package ID specification `no_such_crate` did not match any packages
2815+
2816+
"#]])
2817+
.run();
2818+
2819+
// Successfully upgrade bar to add transitive to lockfile
2820+
p.cargo("update -Zunstable-options --breaking bar")
27972821
.masquerade_as_nightly_cargo(&["update-breaking"])
27982822
.with_stderr_data(str![[r#"
27992823
[UPDATING] `dummy-registry` index
@@ -2805,10 +2829,28 @@ fn update_breaking_missing_package_error() {
28052829
"#]])
28062830
.run();
28072831

2808-
// Transitive dependency is silently ignored (produces no output)
2832+
// Transitive dependency reports helpful error
28092833
p.cargo("update -Zunstable-options --breaking transitive")
28102834
.masquerade_as_nightly_cargo(&["update-breaking"])
2835+
.with_status(101)
28112836
.with_stderr_data(str![[r#"
2837+
[ERROR] package ID specification `transitive` matched a package in the dependency graph but did not match any direct dependencies that could be upgraded.
2838+
Note: `--breaking` can only upgrade direct dependencies of workspace members.
2839+
2840+
"#]])
2841+
.run();
2842+
2843+
// Multiple error types reported together
2844+
p.cargo("update -Zunstable-options --breaking no_such_crate transitive another_missing")
2845+
.masquerade_as_nightly_cargo(&["update-breaking"])
2846+
.with_status(101)
2847+
.with_stderr_data(str![[r#"
2848+
[ERROR] package ID specification `no_such_crate` did not match any packages
2849+
2850+
package ID specification `transitive` matched a package in the dependency graph but did not match any direct dependencies that could be upgraded.
2851+
Note: `--breaking` can only upgrade direct dependencies of workspace members.
2852+
2853+
package ID specification `another_missing` did not match any packages
28122854
28132855
"#]])
28142856
.run();

0 commit comments

Comments
 (0)