Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion src/cargo/ops/cargo_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::util::{CargoResult, VersionExt};
use crate::util::{OptVersionReq, style};
use anyhow::Context as _;
use cargo_util_schemas::core::PartialVersion;
use indexmap::IndexMap;
use indexmap::{IndexMap, IndexSet};
use itertools::Itertools;
use semver::{Op, Version, VersionReq};
use std::cmp::Ordering;
Expand Down Expand Up @@ -238,6 +238,8 @@ pub fn upgrade_manifests(
let mut registry = ws.package_registry()?;
registry.lock_patches();

let mut remaining_specs: IndexSet<_> = to_update.iter().cloned().collect();

for member in ws.members_mut().sorted() {
debug!("upgrading manifest for `{}`", member.name());

Expand All @@ -252,11 +254,58 @@ pub fn upgrade_manifests(
&mut registry,
&mut upgrades,
&mut upgrade_messages,
&mut remaining_specs,
d,
)
})?;
}

if !remaining_specs.is_empty() {
let previous_resolve = ops::load_pkg_lockfile(ws)?;
let plural = if remaining_specs.len() == 1 { "" } else { "s" };

let mut error_msg = format!(
"package ID specification{plural} did not match any direct dependencies that could be upgraded"
);

let mut transitive_specs = Vec::new();
for spec in &remaining_specs {
error_msg.push_str(&format!("\n {spec}"));

// Check if spec is in the lockfile (could be transitive)
let in_lockfile = if let Some(ref resolve) = previous_resolve {
spec.query(resolve.iter()).is_ok()
} else {
false
};

// Check if spec matches any direct dependency in the workspace
let matches_direct_dep = ws.members().any(|member| {
member.dependencies().iter().any(|dep| {
spec.name() == dep.package_name().as_str()
&& dep.source_id().is_registry()
&& spec.url().map_or(true, |url| url == dep.source_id().url())
&& spec
.version()
.map_or(true, |v| dep.version_req().matches(&v))
})
});

// Track transitive specs for notes at the end
if in_lockfile && !matches_direct_dep {
transitive_specs.push(spec);
}
}

for spec in transitive_specs {
error_msg.push_str(&format!(
"\nnote: `{spec}` exists as a transitive dependency but those are not available for upgrading through `--breaking`"
));
}

anyhow::bail!("{error_msg}");
}

Ok(upgrades)
}

Expand All @@ -266,6 +315,7 @@ fn upgrade_dependency(
registry: &mut PackageRegistry<'_>,
upgrades: &mut UpgradeMap,
upgrade_messages: &mut HashSet<String>,
remaining_specs: &mut IndexSet<PackageIdSpec>,
dependency: Dependency,
) -> CargoResult<Dependency> {
let name = dependency.package_name();
Expand Down Expand Up @@ -367,6 +417,10 @@ fn upgrade_dependency(

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

// Remove this spec from remaining_specs since we successfully upgraded it
remaining_specs
.retain(|spec| !(spec.name() == name.as_str() && dependency.source_id().is_registry()));

let req = OptVersionReq::Req(VersionReq::parse(&latest.to_string())?);
let mut dep = dependency.clone();
dep.set_version_req(req);
Expand Down
165 changes: 156 additions & 9 deletions tests/testsuite/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2161,10 +2161,30 @@ fn update_breaking_specific_packages_that_wont_update() {
Package::new("non-semver", "2.0.0").publish();
Package::new("transitive-incompatible", "2.0.0").publish();

p.cargo("update -Zunstable-options --breaking compatible renamed-from non-semver transitive-compatible transitive-incompatible")
// Test that transitive dependencies produce helpful errors
p.cargo("update -Zunstable-options --breaking transitive-compatible transitive-incompatible")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `[..]` index
[ERROR] package ID specifications did not match any direct dependencies that could be upgraded
transitive-compatible
transitive-incompatible
[NOTE] `transitive-compatible` exists as a transitive dependency but those are not available for upgrading through `--breaking`
[NOTE] `transitive-incompatible` exists as a transitive dependency but those are not available for upgrading through `--breaking`

"#]])
.run();

// Test that renamed, non-semver, no-breaking-update dependencies produce errors
p.cargo("update -Zunstable-options --breaking compatible renamed-from non-semver")
Comment thread
epage marked this conversation as resolved.
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[ERROR] package ID specifications did not match any direct dependencies that could be upgraded
compatible
renamed-from
non-semver

"#]])
.run();
Expand Down Expand Up @@ -2276,13 +2296,23 @@ Caused by:
// Spec version not matching our current dependencies
p.cargo("update -Zunstable-options --breaking incompatible@2.0.0")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_stderr_data(str![[r#""#]])
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
incompatible@2.0.0

"#]])
.run();

// Spec source not matching our current dependencies
p.cargo("update -Zunstable-options --breaking https://alternative.com#incompatible@1.0.0")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_stderr_data(str![[r#""#]])
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
https://alternative.com/#incompatible@1.0.0

"#]])
.run();

// Accepted spec
Expand Down Expand Up @@ -2313,21 +2343,34 @@ Caused by:
// Spec matches a dependency that will not be upgraded
p.cargo("update -Zunstable-options --breaking compatible@1.0.0")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `[..]` index
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
compatible@1.0.0

"#]])
.run();

// Non-existing versions
p.cargo("update -Zunstable-options --breaking incompatible@9.0.0")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_stderr_data(str![[r#""#]])
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
incompatible@9.0.0

"#]])
.run();

p.cargo("update -Zunstable-options --breaking compatible@9.0.0")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_stderr_data(str![[r#""#]])
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
compatible@9.0.0

"#]])
.run();
}

Expand Down Expand Up @@ -2388,8 +2431,11 @@ fn update_breaking_spec_version_transitive() {
// But not the transitive one, because bar is not a workspace member
p.cargo("update -Zunstable-options --breaking dep@1.1")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `[..]` index
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
dep@1.1

"#]])
.run();
Expand Down Expand Up @@ -2647,12 +2693,15 @@ fn update_breaking_pre_release_downgrade() {

// The purpose of this test is
// to demonstrate that `update --breaking` will not try to downgrade to the latest stable version (1.7.0),
// but will rather keep the latest pre-release (2.0.0-beta.21).
// but will error because the dependency uses an exact version (not caret).
Package::new("bar", "1.7.0").publish();
p.cargo("update -Zunstable-options --breaking bar")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
bar

"#]])
.run();
Expand Down Expand Up @@ -2681,21 +2730,27 @@ fn update_breaking_pre_release_upgrade() {

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

// TODO: `2.0.0-beta.21` can be upgraded to `2.0.0-beta.22`
// `2.0.0-beta.21` cannot be upgraded with --breaking because it uses an exact version (not caret)
Package::new("bar", "2.0.0-beta.22").publish();
p.cargo("update -Zunstable-options --breaking bar")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
bar

"#]])
.run();
// TODO: `2.0.0-beta.21` can be upgraded to `2.0.0`
// `2.0.0-beta.21` cannot be upgraded to `2.0.0` with --breaking because it uses an exact version (not caret)
Package::new("bar", "2.0.0").publish();
p.cargo("update -Zunstable-options --breaking bar")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
bar

"#]])
.run();
Expand Down Expand Up @@ -2750,3 +2805,95 @@ Caused by:
"#]])
.run();
}

#[cargo_test]
fn update_breaking_missing_package_error() {
Comment thread
epage marked this conversation as resolved.
Package::new("bar", "1.0.0").publish();
Package::new("transitive", "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();

Package::new("bar", "2.0.0")
.add_dep(Dependency::new("transitive", "1.0.0").build())
.publish();

// Non-existent package reports an error
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 did not match any direct dependencies that could be upgraded
no_such_crate

"#]])
.run();

// Valid package processes, invalid package reports error
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
[UPGRADING] bar ^1.0 -> ^2.0
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
no_such_crate

"#]])
.run();

// Successfully upgrade bar to add transitive to lockfile
p.cargo("update -Zunstable-options --breaking bar")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[UPGRADING] bar ^1.0 -> ^2.0
[LOCKING] 2 packages to latest compatible versions
[UPDATING] bar v1.0.0 -> v2.0.0
[ADDING] transitive v1.0.0

"#]])
.run();

// Transitive dependency reports helpful error
p.cargo("update -Zunstable-options --breaking transitive")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] package ID specification did not match any direct dependencies that could be upgraded
transitive
[NOTE] `transitive` exists as a transitive dependency but those are not available for upgrading through `--breaking`

"#]])
.run();

// Multiple error types reported together
p.cargo("update -Zunstable-options --breaking no_such_crate transitive another_missing")
.masquerade_as_nightly_cargo(&["update-breaking"])
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] package ID specifications did not match any direct dependencies that could be upgraded
no_such_crate
transitive
another_missing
[NOTE] `transitive` exists as a transitive dependency but those are not available for upgrading through `--breaking`

"#]])
.run();
}