Skip to content

Commit 3a4485d

Browse files
authored
update: silent failure on non-matching package specs with --breaking (#16276)
When using 'cargo update -Zunstable-options --breaking <spec>', package specifications that don't match any dependency in the workspace or lockfile are silently ignored instead of reporting an error. This leads to confusing behavior where users think they're updating a package that doesn't exist. Fix this by tracking which specs match dependencies during the upgrade process. After processing all workspace members, verify that each requested spec matched at least one direct dependency or exists in the lockfile as a transitive dependency. Report an error for any spec that matches neither. The fix preserves existing behavior for renamed dependencies and non-registry sources while ensuring proper error reporting for genuinely missing packages. Closes #16258
2 parents 3d20f19 + 145bec3 commit 3a4485d

File tree

2 files changed

+211
-10
lines changed

2 files changed

+211
-10
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: 156 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2161,10 +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-
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 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`
2174+
2175+
"#]])
2176+
.run();
2177+
2178+
// Test that renamed, non-semver, no-breaking-update dependencies produce errors
2179+
p.cargo("update -Zunstable-options --breaking compatible renamed-from non-semver")
2180+
.masquerade_as_nightly_cargo(&["update-breaking"])
2181+
.with_status(101)
2182+
.with_stderr_data(str![[r#"
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
21682188
21692189
"#]])
21702190
.run();
@@ -2276,13 +2296,23 @@ Caused by:
22762296
// Spec version not matching our current dependencies
22772297
p.cargo("update -Zunstable-options --breaking incompatible@2.0.0")
22782298
.masquerade_as_nightly_cargo(&["update-breaking"])
2279-
.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+
"#]])
22802305
.run();
22812306

22822307
// Spec source not matching our current dependencies
22832308
p.cargo("update -Zunstable-options --breaking https://alternative.com#incompatible@1.0.0")
22842309
.masquerade_as_nightly_cargo(&["update-breaking"])
2285-
.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+
"#]])
22862316
.run();
22872317

22882318
// Accepted spec
@@ -2313,21 +2343,34 @@ Caused by:
23132343
// Spec matches a dependency that will not be upgraded
23142344
p.cargo("update -Zunstable-options --breaking compatible@1.0.0")
23152345
.masquerade_as_nightly_cargo(&["update-breaking"])
2346+
.with_status(101)
23162347
.with_stderr_data(str![[r#"
23172348
[UPDATING] `[..]` index
2349+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2350+
compatible@1.0.0
23182351
23192352
"#]])
23202353
.run();
23212354

23222355
// Non-existing versions
23232356
p.cargo("update -Zunstable-options --breaking incompatible@9.0.0")
23242357
.masquerade_as_nightly_cargo(&["update-breaking"])
2325-
.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+
"#]])
23262364
.run();
23272365

23282366
p.cargo("update -Zunstable-options --breaking compatible@9.0.0")
23292367
.masquerade_as_nightly_cargo(&["update-breaking"])
2330-
.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+
"#]])
23312374
.run();
23322375
}
23332376

@@ -2388,8 +2431,11 @@ fn update_breaking_spec_version_transitive() {
23882431
// But not the transitive one, because bar is not a workspace member
23892432
p.cargo("update -Zunstable-options --breaking dep@1.1")
23902433
.masquerade_as_nightly_cargo(&["update-breaking"])
2434+
.with_status(101)
23912435
.with_stderr_data(str![[r#"
23922436
[UPDATING] `[..]` index
2437+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2438+
dep@1.1
23932439
23942440
"#]])
23952441
.run();
@@ -2647,12 +2693,15 @@ fn update_breaking_pre_release_downgrade() {
26472693

26482694
// The purpose of this test is
26492695
// to demonstrate that `update --breaking` will not try to downgrade to the latest stable version (1.7.0),
2650-
// 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).
26512697
Package::new("bar", "1.7.0").publish();
26522698
p.cargo("update -Zunstable-options --breaking bar")
26532699
.masquerade_as_nightly_cargo(&["update-breaking"])
2700+
.with_status(101)
26542701
.with_stderr_data(str![[r#"
26552702
[UPDATING] `dummy-registry` index
2703+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2704+
bar
26562705
26572706
"#]])
26582707
.run();
@@ -2681,21 +2730,27 @@ fn update_breaking_pre_release_upgrade() {
26812730

26822731
p.cargo("generate-lockfile").run();
26832732

2684-
// 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)
26852734
Package::new("bar", "2.0.0-beta.22").publish();
26862735
p.cargo("update -Zunstable-options --breaking bar")
26872736
.masquerade_as_nightly_cargo(&["update-breaking"])
2737+
.with_status(101)
26882738
.with_stderr_data(str![[r#"
26892739
[UPDATING] `dummy-registry` index
2740+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2741+
bar
26902742
26912743
"#]])
26922744
.run();
2693-
// 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)
26942746
Package::new("bar", "2.0.0").publish();
26952747
p.cargo("update -Zunstable-options --breaking bar")
26962748
.masquerade_as_nightly_cargo(&["update-breaking"])
2749+
.with_status(101)
26972750
.with_stderr_data(str![[r#"
26982751
[UPDATING] `dummy-registry` index
2752+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2753+
bar
26992754
27002755
"#]])
27012756
.run();
@@ -2750,3 +2805,95 @@ Caused by:
27502805
"#]])
27512806
.run();
27522807
}
2808+
2809+
#[cargo_test]
2810+
fn update_breaking_missing_package_error() {
2811+
Package::new("bar", "1.0.0").publish();
2812+
Package::new("transitive", "1.0.0").publish();
2813+
2814+
let p = project()
2815+
.file(
2816+
"Cargo.toml",
2817+
r#"
2818+
[package]
2819+
name = "foo"
2820+
version = "0.0.1"
2821+
edition = "2015"
2822+
authors = []
2823+
2824+
[dependencies]
2825+
bar = "1.0"
2826+
"#,
2827+
)
2828+
.file("src/lib.rs", "")
2829+
.build();
2830+
2831+
p.cargo("generate-lockfile").run();
2832+
2833+
Package::new("bar", "2.0.0")
2834+
.add_dep(Dependency::new("transitive", "1.0.0").build())
2835+
.publish();
2836+
2837+
// Non-existent package reports an error
2838+
p.cargo("update -Zunstable-options --breaking no_such_crate")
2839+
.masquerade_as_nightly_cargo(&["update-breaking"])
2840+
.with_status(101)
2841+
.with_stderr_data(str![[r#"
2842+
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
2843+
no_such_crate
2844+
2845+
"#]])
2846+
.run();
2847+
2848+
// Valid package processes, invalid package reports error
2849+
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")
2863+
.masquerade_as_nightly_cargo(&["update-breaking"])
2864+
.with_stderr_data(str![[r#"
2865+
[UPDATING] `dummy-registry` index
2866+
[UPGRADING] bar ^1.0 -> ^2.0
2867+
[LOCKING] 2 packages to latest compatible versions
2868+
[UPDATING] bar v1.0.0 -> v2.0.0
2869+
[ADDING] transitive v1.0.0
2870+
2871+
"#]])
2872+
.run();
2873+
2874+
// Transitive dependency reports helpful error
2875+
p.cargo("update -Zunstable-options --breaking transitive")
2876+
.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)
2890+
.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`
2896+
2897+
"#]])
2898+
.run();
2899+
}

0 commit comments

Comments
 (0)