diff --git a/src/cargo/ops/cargo_update.rs b/src/cargo/ops/cargo_update.rs index ccbbfa01bfd..f01898c002d 100644 --- a/src/cargo/ops/cargo_update.rs +++ b/src/cargo/ops/cargo_update.rs @@ -238,6 +238,9 @@ pub fn upgrade_manifests( let mut registry = ws.package_registry()?; registry.lock_patches(); + // Track which specs have been matched to detect non-existent packages + let mut matched_specs = HashSet::new(); + for member in ws.members_mut().sorted() { debug!("upgrading manifest for `{}`", member.name()); @@ -252,11 +255,38 @@ pub fn upgrade_manifests( &mut registry, &mut upgrades, &mut upgrade_messages, + &mut matched_specs, d, ) })?; } + // Check if any specs were not matched against direct dependencies + if !to_update.is_empty() { + // Load the lockfile to check for transitive dependencies + let previous_resolve = ops::load_pkg_lockfile(ws)?; + + for spec in &to_update { + if !matched_specs.contains(spec) { + // Spec didn't match any direct dependencies + // Check if it matches any package in the lockfile (including transitive deps) + let matches_lockfile = if let Some(ref resolve) = previous_resolve { + spec.query(resolve.iter()).is_ok() + } else { + false + }; + + if !matches_lockfile { + // Spec doesn't match any package at all + anyhow::bail!( + "package ID specification `{}` did not match any packages", + spec + ); + } + } + } + } + Ok(upgrades) } @@ -266,18 +296,30 @@ fn upgrade_dependency( registry: &mut PackageRegistry<'_>, upgrades: &mut UpgradeMap, upgrade_messages: &mut HashSet, + matched_specs: &mut HashSet, dependency: Dependency, ) -> CargoResult { let name = dependency.package_name(); let renamed_to = dependency.name_in_toml(); + if !to_update.is_empty() { + // Check if any spec matches this dependency by name + for spec in to_update.iter() { + if spec.name() == name.as_str() { + // Mark this spec as matched (exists in workspace) + matched_specs.insert(spec.clone()); + } + } + } + if name != renamed_to { trace!("skipping dependency renamed from `{name}` to `{renamed_to}`"); return Ok(dependency); } - if !to_update.is_empty() - && !to_update.iter().any(|spec| { + if !to_update.is_empty() { + // Check if this dependency should be upgraded based on the specs + let should_upgrade = to_update.iter().any(|spec| { spec.name() == name.as_str() && dependency.source_id().is_registry() && spec @@ -286,10 +328,12 @@ fn upgrade_dependency( && spec .version() .map_or(true, |v| dependency.version_req().matches(&v)) - }) - { - trace!("skipping dependency `{name}` not selected for upgrading"); - return Ok(dependency); + }); + + if !should_upgrade { + trace!("skipping dependency `{name}` not selected for upgrading"); + return Ok(dependency); + } } if !dependency.source_id().is_registry() { diff --git a/tests/testsuite/update.rs b/tests/testsuite/update.rs index a8dc8721d92..f2551860a74 100644 --- a/tests/testsuite/update.rs +++ b/tests/testsuite/update.rs @@ -2750,3 +2750,48 @@ Caused by: "#]]) .run(); } + +#[cargo_test] +fn update_breaking_missing_package_error() { + Package::new("bar", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + bar = "1.0" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + // Test that --breaking reports an error for non-existent packages + p.cargo("update -Zunstable-options --breaking no_such_crate") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] package ID specification `no_such_crate` did not match any packages + +"#]]) + .run(); + + // Test with multiple packages, one valid and one invalid + p.cargo("update -Zunstable-options --breaking bar no_such_crate") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[ERROR] package ID specification `no_such_crate` did not match any packages + +"#]]) + .run(); +}