From 8a2f3b6bfe3026bad39d1574b8a77e14e7ce4fda Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 30 Jun 2026 14:16:56 +0200 Subject: [PATCH 1/6] chore: Extract dependency_matches_previous_id --- src/cargo/core/registry.rs | 49 +++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/cargo/core/registry.rs b/src/cargo/core/registry.rs index b3f5ca4ae3f..155cc05d333 100644 --- a/src/cargo/core/registry.rs +++ b/src/cargo/core/registry.rs @@ -869,30 +869,9 @@ fn lock( // Cases 1/2 are handled by `matches_id`, case 3 is handled specially, // and case 4 is handled by falling through to the logic below. if let Some((_, locked_deps)) = pair { - let locked = locked_deps.iter().find(|&&id| { - // If the dependency matches the package id exactly then we've - // found a match, this is the id the dependency was previously - // locked to. - if dep.matches_id(id) { - return true; - } - - // If the name/version doesn't match, then we definitely don't - // have a match whatsoever. Otherwise we need to check - // `[patch]`... - if !dep.matches_ignoring_source(id) { - return false; - } - - // ... so here we look up the dependency url in the patches - // map, and we see if `id` is contained in the list of patches - // for that url. If it is then this lock is still valid, - // otherwise the lock is no longer valid. - match patches.get(dep.source_id().canonical_url()) { - Some(list) => list.contains(&id), - None => false, - } - }); + let locked = locked_deps + .iter() + .find(|&&id| dependency_matches_previous_id(&dep, id, patches)); if let Some(&locked) = locked { trace!("\tfirst hit on {}", locked); @@ -929,6 +908,28 @@ fn lock( }) } +fn dependency_matches_previous_id( + dep: &Dependency, + id: PackageId, + patches: &HashMap>, +) -> bool { + // If the dependency matches the package id exactly then we've found the + // package this dependency previously resolved to. + if dep.matches_id(id) { + return true; + } + + // A dependency can also have resolved to a package from a different source + // through `[patch]`. + if !dep.matches_ignoring_source(id) { + return false; + } + + patches + .get(dep.source_id().canonical_url()) + .is_some_and(|available| available.contains(&id)) +} + /// A helper for selecting the summary, or generating a helpful error message. /// /// Returns a tuple that the first element is the summary selected. The second From f73ae090c23b0ffa8fda2b3bb741805d94009b87 Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 30 Jun 2026 14:26:19 +0200 Subject: [PATCH 2/6] chore: Migrate the locked map values to a LockedPackage type --- src/cargo/core/registry.rs | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/cargo/core/registry.rs b/src/cargo/core/registry.rs index 155cc05d333..696ca99309a 100644 --- a/src/cargo/core/registry.rs +++ b/src/cargo/core/registry.rs @@ -133,22 +133,23 @@ pub struct PackageRegistry<'gctx> { /// and is used to guide dependency resolution by altering summaries as they're /// queried from this source. /// -/// This map can be thought of as a glorified `Vec` where `MySummary` -/// has a `PackageId` for which package it represents as well as a list of -/// `PackageId` for the resolved dependencies. The hash map is otherwise -/// structured though for easy access throughout this registry. +/// This map can be thought of as a glorified `Vec`. The hash map +/// is otherwise structured for easy access throughout this registry. type LockedMap = HashMap< // The first level of key-ing done in this hash map is the source that // dependencies come from, identified by a `SourceId`. // The next level is keyed by the name of the package... (SourceId, InternedString), - // ... and the value here is a list of tuples. The first element of each - // tuple is a package which has the source/name used to get to this - // point. The second element of each tuple is the list of locked - // dependencies that the first element has. - Vec<(PackageId, Vec)>, + // ... and the value here is the list of matching locked packages. + Vec, >; +struct LockedPackage { + id: PackageId, + // The locked dependencies that this package has. + deps: Vec, +} + /// Kinds of sources a [`PackageRegistry`] has loaded. #[derive(PartialEq, Eq, Clone, Copy)] enum Kind { @@ -298,7 +299,7 @@ impl<'gctx> PackageRegistry<'gctx> { .locked .entry((id.source_id(), id.name())) .or_insert_with(Vec::new); - sub_vec.push((id, deps)); + sub_vec.push(LockedPackage { id, deps }); } /// Insert a `[patch]` section into this registry. @@ -828,13 +829,13 @@ fn lock( ) -> Summary { let pair = locked .get(&(summary.source_id(), summary.name())) - .and_then(|vec| vec.iter().find(|&&(id, _)| id == summary.package_id())); + .and_then(|vec| vec.iter().find(|locked| locked.id == summary.package_id())); trace!("locking summary of {}", summary.package_id()); // Lock the summary's ID if possible let summary = match pair { - Some((precise, _)) => summary.override_id(*precise), + Some(locked) => summary.override_id(locked.id), None => summary, }; summary.map_dependencies(|dep| { @@ -868,8 +869,9 @@ fn lock( // // Cases 1/2 are handled by `matches_id`, case 3 is handled specially, // and case 4 is handled by falling through to the logic below. - if let Some((_, locked_deps)) = pair { - let locked = locked_deps + if let Some(locked_package) = pair { + let locked = locked_package + .deps .iter() .find(|&&id| dependency_matches_previous_id(&dep, id, patches)); @@ -895,11 +897,11 @@ fn lock( // If anything does then we lock it to that and move on. let v = locked .get(&(dep.source_id(), dep.package_name())) - .and_then(|vec| vec.iter().find(|&&(id, _)| dep.matches_id(id))); - if let Some(&(id, _)) = v { - trace!("\tsecond hit on {}", id); + .and_then(|vec| vec.iter().find(|locked| dep.matches_id(locked.id))); + if let Some(locked) = v { + trace!("\tsecond hit on {}", locked.id); let mut dep = dep; - dep.lock_to(id); + dep.lock_to(locked.id); return dep; } From a576af5d4c59104f335c5e1ab520ec07c35d0c42 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 29 Jun 2026 22:39:26 +0200 Subject: [PATCH 3/6] test: Add regression test for unrelated dependencies being updated --- tests/testsuite/update.rs | 151 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/tests/testsuite/update.rs b/tests/testsuite/update.rs index 1e8584022ab..520df386430 100644 --- a/tests/testsuite/update.rs +++ b/tests/testsuite/update.rs @@ -3354,3 +3354,154 @@ fn update_breaking_missing_package_error() { "#]]) .run(); } + +/// Regression test for https://github.com/rust-lang/cargo/issues/5529: Don't change the version +/// of `shared` unnecessarily. +/// +/// ``` +/// foo +/// ├── shared =0.1.0 +/// └── c 1.0.0 +/// └── b 1.0.0 +/// └── shared >=0.1,<0.3 # Locked to 0.2.0 +/// ``` +/// +/// When `c` gets updated, the version of `shared` that `b` uses should not be updated, to avoid +/// churning the lockfile between different compatible versions. +#[cargo_test] +fn update_preserves_unrelated_dependency_choices() { + Package::new("shared", "0.1.0").publish(); + Package::new("shared", "0.2.0").publish(); + Package::new("shared", "0.3.0").publish(); + Package::new("b", "1.0.0") + .dep("shared", ">=0.1, <0.3") + .publish(); + Package::new("c", "1.0.0").dep("b", "=1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2015" + + [dependencies] + c = "1" + shared = "=0.1.0" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile") + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 4 packages to latest compatible versions +[ADDING] shared v0.1.0 (available: v0.3.0) + +"#]]) + .run(); + assert_e2e().eq( + p.read_lockfile(), + str![[r##" +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "b" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6815d5ddba1afab27e9a3e223d294b5165ca1c488635d7782d9fa067c9b69ddc" +dependencies = [ + "shared 0.2.0", +] + +[[package]] +name = "c" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "[..]" +dependencies = [ + "b", +] + +[[package]] +name = "foo" +version = "0.1.0" +dependencies = [ + "c", + "shared 0.1.0", +] + +[[package]] +name = "shared" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7229fd816f437ce23e3433bc64e3be727f45cb213facb4eaf6888b8814dfd472" + +[[package]] +name = "shared" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "450b41d98bc7d554b97852761912eb0ccc0148d20b1c3a8d8adee1eb8a8c4661" + +"##]], + ); + + Package::new("c", "1.0.1").dep("b", "=1.0.0").publish(); + + p.cargo("update -p c --precise 1.0.1") + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPDATING] c v1.0.0 -> v1.0.1 +[REMOVING] shared v0.2.0 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); + + assert_e2e().eq( + p.read_lockfile(), + str![[r##" +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "b" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6815d5ddba1afab27e9a3e223d294b5165ca1c488635d7782d9fa067c9b69ddc" +dependencies = [ + "shared", +] + +[[package]] +name = "c" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "[..]" +dependencies = [ + "b", +] + +[[package]] +name = "foo" +version = "0.1.0" +dependencies = [ + "c", + "shared", +] + +[[package]] +name = "shared" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7229fd816f437ce23e3433bc64e3be727f45cb213facb4eaf6888b8814dfd472" + +"##]], + ); +} From 824662c3fbcbc4e7024b6d74e55c3b1a8e9df195 Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 2 Jul 2026 12:05:15 +0200 Subject: [PATCH 4/6] test: Add regression test for updates conflicting with preferred versions --- tests/testsuite/update.rs | 200 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/tests/testsuite/update.rs b/tests/testsuite/update.rs index 520df386430..f63ee0c58fb 100644 --- a/tests/testsuite/update.rs +++ b/tests/testsuite/update.rs @@ -3505,3 +3505,203 @@ checksum = "7229fd816f437ce23e3433bc64e3be727f45cb213facb4eaf6888b8814dfd472" "##]], ); } + +/// Updating a package whose new versions conflict with the preferred version of an unrelated +/// dependency edge. +/// +/// ``` +/// foo +/// ├── b 1.0.0 +/// │ └── shared >=1, <3 # Locked to 1.1.0 +/// ├── bar 1.0.0 # The package being updated +/// │ └── shared 1 # Locked to 1.1.0 +/// └── d 1.0.0 +/// └── shared 2 # Locked to 2.0.0 +/// ``` +/// +/// The new versions of `bar` require `shared 1.5`, so updating `bar` also requires moving `b`'s +/// edge from `shared 1.1.0` to another compatible version (`1.5.0` or `2.0.0`). +#[cargo_test] +fn update_with_conflicting_preferred_dependency() { + Package::new("shared", "1.1.0").publish(); + Package::new("b", "1.0.0") + .dep("shared", ">=1, <3") + .publish(); + Package::new("bar", "1.0.0").dep("shared", "1").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2015" + + [dependencies] + b = "1" + bar = "1" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile") + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 3 packages to latest compatible versions + +"#]]) + .run(); + + // Later, `shared 2.0.0` is published and `d`, which requires it, is added to the project. + Package::new("shared", "2.0.0").publish(); + Package::new("d", "1.0.0").dep("shared", "2").publish(); + p.change_file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2015" + + [dependencies] + b = "1" + bar = "1" + d = "1" + "#, + ); + p.cargo("fetch").run(); + assert_e2e().eq( + p.read_lockfile(), + str![[r##" +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "b" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "949835dc399fa90ab724e71bcbf48e698b52cc0306382d824dabdc6a983499fd" +dependencies = [ + "shared 1.1.0", +] + +[[package]] +name = "bar" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b3a716d182c32149517cd5fc2df788c1b64f63a32586727a4578e263281b71a" +dependencies = [ + "shared 1.1.0", +] + +[[package]] +name = "d" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a6dee46f8e5b467aadbeedcb893996536e8d2aff74c93cc1bf109aecb2c0cd8" +dependencies = [ + "shared 2.0.0", +] + +[[package]] +name = "foo" +version = "0.1.0" +dependencies = [ + "b", + "bar", + "d", +] + +[[package]] +name = "shared" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30caf82c5fdb1c2f13f50fd6f4bf916d1203e96129dbbaa8f81f51499cc519a1" + +[[package]] +name = "shared" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7e5a3a91c4925f90efaa88a5a28b5b5883a51145b7382a8b7d6be77d5b2f62" + +"##]], + ); + + // Later still, `shared 1.5.0` is published, and new `bar` versions that require it. + Package::new("shared", "1.5.0").publish(); + for patch in 1..=5 { + Package::new("bar", &format!("1.0.{patch}")) + .dep("shared", "1.5") + .publish(); + } + + p.cargo("update bar") + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 2 packages to latest compatible versions +[UPDATING] bar v1.0.0 -> v1.0.5 +[UPDATING] shared v1.1.0 -> v1.5.0 + +"#]]) + .run(); + assert_e2e().eq( + p.read_lockfile(), + str![[r##" +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "b" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "949835dc399fa90ab724e71bcbf48e698b52cc0306382d824dabdc6a983499fd" +dependencies = [ + "shared 2.0.0", +] + +[[package]] +name = "bar" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "735ff842099ec0f08bc73dbde77731983353b6d2d8ddaace2857defd5e0ec731" +dependencies = [ + "shared 1.5.0", +] + +[[package]] +name = "d" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a6dee46f8e5b467aadbeedcb893996536e8d2aff74c93cc1bf109aecb2c0cd8" +dependencies = [ + "shared 2.0.0", +] + +[[package]] +name = "foo" +version = "0.1.0" +dependencies = [ + "b", + "bar", + "d", +] + +[[package]] +name = "shared" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5434694b9342a622d0c76663efc7f05a6fc3dd77f0513f64fd6ceac2e9a7bd8b" + +[[package]] +name = "shared" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7e5a3a91c4925f90efaa88a5a28b5b5883a51145b7382a8b7d6be77d5b2f62" + +"##]], + ); +} From 62bff8d86f67285cccb28a501071b34b83b0065e Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 29 Jun 2026 22:39:34 +0200 Subject: [PATCH 5/6] fix: Avoid churning unrelated dependency edges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When updating a dependency, its dependencies get recursively unlocked, while preferring lockfile versions. The edges however were not preserved, causing unrelated dependency edges to toggle between multiple compatible versions available in the lockfile (https://github.com/rust-lang/cargo/issues/5529). ``` foo ├── shared =0.1.0 └── c 1.0.0 └── b 1.0.0 └── shared >=0.1,<0.3 # Locked to 0.2.0 ``` In the example above, `cargo update -p c --precise 1.0.1` would change the version of `shared` that `b` uses to 0.1.0. This causes churn when updating dependency as packages with multiple versions in the lockfile and wide ranges, such as the `windows-*` crates, toggle back and forth through unrelated updates. This PR adds tracking of preferences for each package id, so `b 1.0.0` tracks a preference for `shared 0.2.0` specifically, which is set as version to try first. We introduce a third mode between locked and skipped dependencies in `register_previous_locks`: Packages such as `b 1.0.0` remain unlocked, but aren't skipped so that their dependency edges remain tracked. Fixes https://github.com/rust-lang/cargo/issues/5529 Note: The preference can also misdirect backtracking: when the new version of the package being updated conflicts with a preferred dependency edge, `cargo update ` keeps the old version instead of backtracking on the preference, see `update_with_conflicting_preferred_dependency`. --- src/cargo/core/dependency.rs | 17 +++++++++ src/cargo/core/registry.rs | 53 ++++++++++++++++++++++------ src/cargo/core/resolver/dep_cache.rs | 12 +++++++ src/cargo/ops/resolve.rs | 36 ++++++++++++------- tests/testsuite/update.rs | 31 +++++++++------- 5 files changed, 114 insertions(+), 35 deletions(-) diff --git a/src/cargo/core/dependency.rs b/src/cargo/core/dependency.rs index 29853420da9..9cc3b2540e2 100644 --- a/src/cargo/core/dependency.rs +++ b/src/cargo/core/dependency.rs @@ -48,6 +48,11 @@ struct Inner { // The presence of this information turns a dependency into an artifact dependency. artifact: Option, + /// A package this dependency previously resolved to. This version is + /// tried first, but doesn't lock the dependency in case transitive + /// update is required. + preferred_package_id: Option, + // This dependency should be used only for this platform. // `None` means *all platforms*. platform: Option, @@ -162,6 +167,7 @@ impl Dependency { platform: None, explicit_name_in_toml: None, artifact: None, + preferred_package_id: None, }), } } @@ -390,6 +396,17 @@ impl Dependency { self } + /// Prefer this package during resolution without preventing backtracking. + pub fn prefer_package_id(&mut self, id: PackageId) -> &mut Dependency { + Arc::make_mut(&mut self.inner).preferred_package_id = Some(id); + self + } + + /// Returns the package that should be tried first for this dependency. + pub fn preferred_package_id(&self) -> Option { + self.inner.preferred_package_id + } + /// Returns `true` if this is a "locked" dependency. Basically a locked /// dependency has an exact version req, but not vice versa. pub fn is_locked(&self) -> bool { diff --git a/src/cargo/core/registry.rs b/src/cargo/core/registry.rs index 696ca99309a..46e7d97f96a 100644 --- a/src/cargo/core/registry.rs +++ b/src/cargo/core/registry.rs @@ -146,8 +146,13 @@ type LockedMap = HashMap< struct LockedPackage { id: PackageId, - // The locked dependencies that this package has. + // If true, lock the package itself, if false, only use the previous + // dependency edges as preferences. + lock_package: bool, + // Dependencies that are fully locked. deps: Vec, + // Dependencies whose previous versions should be tried first. + preferred_deps: Vec, } /// Kinds of sources a [`PackageRegistry`] has loaded. @@ -290,16 +295,30 @@ impl<'gctx> PackageRegistry<'gctx> { /// Registers one "locked package" to the registry, for guiding the /// dependency resolution. See [`LockedMap`] for more. - pub fn register_lock(&mut self, id: PackageId, deps: Vec) { - trace!("register_lock: {}", id); + pub fn register_lock( + &mut self, + id: PackageId, + lock_package: bool, + deps: Vec, + preferred_deps: Vec, + ) { + trace!("register_lock: {} (lock package: {lock_package})", id); for dep in deps.iter() { trace!("\t-> {}", dep); } + for dep in preferred_deps.iter() { + trace!("\t~> {}", dep); + } let sub_vec = self .locked .entry((id.source_id(), id.name())) .or_insert_with(Vec::new); - sub_vec.push(LockedPackage { id, deps }); + sub_vec.push(LockedPackage { + id, + lock_package, + deps, + preferred_deps, + }); } /// Insert a `[patch]` section into this registry. @@ -835,10 +854,10 @@ fn lock( // Lock the summary's ID if possible let summary = match pair { - Some(locked) => summary.override_id(locked.id), - None => summary, + Some(locked) if locked.lock_package => summary.override_id(locked.id), + _ => summary, }; - summary.map_dependencies(|dep| { + summary.map_dependencies(|mut dep| { trace!( "\t{}/{}/{}", dep.package_name(), @@ -877,8 +896,6 @@ fn lock( if let Some(&locked) = locked { trace!("\tfirst hit on {}", locked); - let mut dep = dep; - // If we found a locked version where the sources match, then // we can `lock_to` to get an exact lock on this dependency. // Otherwise we got a lock via `[patch]` so we only lock the @@ -890,6 +907,19 @@ fn lock( } return dep; } + + // This dependency previously resolved to a package that was + // intentionally left unlocked. Prefer the previously locked + // version to avoid churn, but do not lock the version. + if let Some(&preferred) = locked_package + .preferred_deps + .iter() + .find(|&&id| dependency_matches_previous_id(&dep, id, patches)) + { + trace!("\tpreferring previously unlocked dependency {}", preferred); + dep.prefer_package_id(preferred); + return dep; + } } // If this dependency did not have a locked version, then we query @@ -897,7 +927,10 @@ fn lock( // If anything does then we lock it to that and move on. let v = locked .get(&(dep.source_id(), dep.package_name())) - .and_then(|vec| vec.iter().find(|locked| dep.matches_id(locked.id))); + .and_then(|vec| { + vec.iter() + .find(|locked| locked.lock_package && dep.matches_id(locked.id)) + }); if let Some(locked) = v { trace!("\tsecond hit on {}", locked.id); let mut dep = dep; diff --git a/src/cargo/core/resolver/dep_cache.rs b/src/cargo/core/resolver/dep_cache.rs index bc82a01a4c5..1a9c92a1b6d 100644 --- a/src/cargo/core/resolver/dep_cache.rs +++ b/src/cargo/core/resolver/dep_cache.rs @@ -194,6 +194,18 @@ impl<'a, T: Registry> RegistryQueryerAsync<'a, T> { self.version_prefs .sort_summaries(&mut summaries, *first_version); + // Explicit first-version modes take precedence over lockfile guidance. + if first_version.is_none() { + // Put a preference, if any, in first place. + if let Some(preferred) = dep.preferred_package_id() { + if let Some(index) = summaries + .iter() + .position(|summary| summary.package_id() == preferred) + { + summaries[..=index].rotate_right(1); + } + } + } Ok(Rc::new(summaries)) } } diff --git a/src/cargo/ops/resolve.rs b/src/cargo/ops/resolve.rs index c589390cce7..2fa8dd57fc0 100644 --- a/src/cargo/ops/resolve.rs +++ b/src/cargo/ops/resolve.rs @@ -751,21 +751,26 @@ fn register_previous_locks( } } - // Alright now that we've got our new, fresh, shiny, and refined `keep` - // function let's put it to action. Take a look at the previous lock file, - // filter everything by this callback, and then shove everything else into - // the registry as a locked dependency. - let keep = |id: &PackageId| keep(id) && !avoid_locking.contains(id); - + // Alright, now that we've got `avoid_locking` as refinement to `keep`. + // Take a look at the previous lock file, filter out what not to lock, + // reserve preferences, and then shove everything else into the registry + // as a locked dependency. registry.clear_lock(); { let _span = tracing::span!(tracing::Level::TRACE, "register_lock").entered(); + // Packages in the transitive update set are not locked themselves, + // but their previous dependency edges still guide candidate ordering. for node in resolve.iter().filter(keep) { - let deps = resolve - .deps_not_replaced(node) - .map(|p| p.0) - .filter(keep) - .collect::>(); + let lock_package = keep(&node) && !avoid_locking.contains(&node); + let mut dependencies = Vec::new(); + let mut preferred_dependencies = Vec::new(); + for (dependency, _) in resolve.deps_not_replaced(node) { + if keep(&dependency) && !avoid_locking.contains(&dependency) { + dependencies.push(dependency); + } else if keep(&dependency) { + preferred_dependencies.push(dependency); + } + } // In the v2 lockfile format and prior the `branch=master` dependency // directive was serialized the same way as the no-branch-listed @@ -778,10 +783,15 @@ fn register_previous_locks( // this point. All new lock files are encoded as v3-or-later, so this is // just compat for loading an old lock file successfully. if let Some(node) = master_branch_git_source(node, resolve) { - registry.register_lock(node, deps.clone()); + registry.register_lock( + node, + lock_package, + dependencies.clone(), + preferred_dependencies.clone(), + ); } - registry.register_lock(node, deps); + registry.register_lock(node, lock_package, dependencies, preferred_dependencies); } } diff --git a/tests/testsuite/update.rs b/tests/testsuite/update.rs index f63ee0c58fb..f8babcdde09 100644 --- a/tests/testsuite/update.rs +++ b/tests/testsuite/update.rs @@ -3457,7 +3457,6 @@ checksum = "450b41d98bc7d554b97852761912eb0ccc0148d20b1c3a8d8adee1eb8a8c4661" .with_stderr_data(str![[r#" [UPDATING] `dummy-registry` index [UPDATING] c v1.0.0 -> v1.0.1 -[REMOVING] shared v0.2.0 [NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest "#]]) @@ -3476,7 +3475,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6815d5ddba1afab27e9a3e223d294b5165ca1c488635d7782d9fa067c9b69ddc" dependencies = [ - "shared", + "shared 0.2.0", ] [[package]] @@ -3493,7 +3492,7 @@ name = "foo" version = "0.1.0" dependencies = [ "c", - "shared", + "shared 0.1.0", ] [[package]] @@ -3502,6 +3501,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7229fd816f437ce23e3433bc64e3be727f45cb213facb4eaf6888b8814dfd472" +[[package]] +name = "shared" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "450b41d98bc7d554b97852761912eb0ccc0148d20b1c3a8d8adee1eb8a8c4661" + "##]], ); } @@ -3638,12 +3643,14 @@ checksum = "5b7e5a3a91c4925f90efaa88a5a28b5b5883a51145b7382a8b7d6be77d5b2f62" .publish(); } + // FIXME: `bar` is not updated even though a resolution with `bar 1.0.5` exists: The + // preference for `shared 1.1.0` on `b`'s edge is activated first, and the resolver backtracks + // on `bar`'s new versions instead of on the preference. `--precise 1.0.5` still works. p.cargo("update bar") .with_stderr_data(str![[r#" [UPDATING] `dummy-registry` index -[LOCKING] 2 packages to latest compatible versions -[UPDATING] bar v1.0.0 -> v1.0.5 -[UPDATING] shared v1.1.0 -> v1.5.0 +[LOCKING] 0 packages to latest compatible versions +[NOTE] pass `--verbose` to see 2 unchanged dependencies behind latest "#]]) .run(); @@ -3660,16 +3667,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "949835dc399fa90ab724e71bcbf48e698b52cc0306382d824dabdc6a983499fd" dependencies = [ - "shared 2.0.0", + "shared 1.1.0", ] [[package]] name = "bar" -version = "1.0.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "735ff842099ec0f08bc73dbde77731983353b6d2d8ddaace2857defd5e0ec731" +checksum = "2b3a716d182c32149517cd5fc2df788c1b64f63a32586727a4578e263281b71a" dependencies = [ - "shared 1.5.0", + "shared 1.1.0", ] [[package]] @@ -3692,9 +3699,9 @@ dependencies = [ [[package]] name = "shared" -version = "1.5.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5434694b9342a622d0c76663efc7f05a6fc3dd77f0513f64fd6ceac2e9a7bd8b" +checksum = "30caf82c5fdb1c2f13f50fd6f4bf916d1203e96129dbbaa8f81f51499cc519a1" [[package]] name = "shared" From 0f27591a83f08283b421a1fadae1074bb38f36c0 Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 2 Jul 2026 12:34:22 +0200 Subject: [PATCH 6/6] chore: Move preferred version selection into VersionPreferences --- src/cargo/core/resolver/dep_cache.rs | 19 +++------ src/cargo/core/resolver/version_prefs.rs | 50 +++++++++++++++++------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/cargo/core/resolver/dep_cache.rs b/src/cargo/core/resolver/dep_cache.rs index 1a9c92a1b6d..6a188171f3d 100644 --- a/src/cargo/core/resolver/dep_cache.rs +++ b/src/cargo/core/resolver/dep_cache.rs @@ -192,20 +192,11 @@ impl<'a, T: Registry> RegistryQueryerAsync<'a, T> { } } - self.version_prefs - .sort_summaries(&mut summaries, *first_version); - // Explicit first-version modes take precedence over lockfile guidance. - if first_version.is_none() { - // Put a preference, if any, in first place. - if let Some(preferred) = dep.preferred_package_id() { - if let Some(index) = summaries - .iter() - .position(|summary| summary.package_id() == preferred) - { - summaries[..=index].rotate_right(1); - } - } - } + self.version_prefs.sort_summaries( + &mut summaries, + *first_version, + dep.preferred_package_id(), + ); Ok(Rc::new(summaries)) } } diff --git a/src/cargo/core/resolver/version_prefs.rs b/src/cargo/core/resolver/version_prefs.rs index 93077ffe40e..43a5f4cd306 100644 --- a/src/cargo/core/resolver/version_prefs.rs +++ b/src/cargo/core/resolver/version_prefs.rs @@ -95,9 +95,11 @@ impl VersionPreferences { /// Note: all summaries presumed to be for the same package. /// /// Sort order: - /// 1. Preferred packages - /// 2. Most compatible [`VersionPreferences::rust_versions`] - /// 3. `first_version`, falling back to [`VersionPreferences::version_ordering`] when `None` + /// 1. `previously_resolved`, the package the dependency resolved to in the previous + /// resolution, unless `first_version` is set + /// 2. Preferred packages + /// 3. Most compatible [`VersionPreferences::rust_versions`] + /// 4. `first_version`, falling back to [`VersionPreferences::version_ordering`] when `None` /// /// Filtering: /// - `publish_time` @@ -106,6 +108,7 @@ impl VersionPreferences { &self, summaries: &mut Vec, first_version: Option, + previously_resolved: Option, ) { if let Some(max_publish_time) = self.publish_time { summaries.retain(|s| { @@ -116,7 +119,22 @@ impl VersionPreferences { } }); } + // Explicit first-version modes take precedence over lockfile guidance. + let previously_resolved = if first_version.is_none() { + previously_resolved + } else { + None + }; summaries.sort_unstable_by(|a, b| { + // The version from the previous resolution, if any, sorts first. + let a_previously_resolved = Some(a.package_id()) == previously_resolved; + let b_previously_resolved = Some(b.package_id()) == previously_resolved; + match (a_previously_resolved, b_previously_resolved) { + (true, false) => return Ordering::Less, + (false, true) => return Ordering::Greater, + _ => {} + } + let prefer_a = self.should_prefer(&a.package_id()); let prefer_b = self.should_prefer(&b.package_id()); let previous_cmp = prefer_a.cmp(&prefer_b).reverse(); @@ -442,14 +460,14 @@ mod test { ]; vp.version_ordering(VersionOrdering::MaximumVersionsFirst); - vp.sort_summaries(&mut summaries, None); + vp.sort_summaries(&mut summaries, None, None); assert_eq!( describe(&summaries), "foo/1.2.3, foo/1.2.4, foo/1.1.0, foo/1.0.9".to_string() ); vp.version_ordering(VersionOrdering::MinimumVersionsFirst); - vp.sort_summaries(&mut summaries, None); + vp.sort_summaries(&mut summaries, None, None); assert_eq!( describe(&summaries), "foo/1.2.3, foo/1.0.9, foo/1.1.0, foo/1.2.4".to_string() @@ -469,14 +487,14 @@ mod test { ]; vp.version_ordering(VersionOrdering::MaximumVersionsFirst); - vp.sort_summaries(&mut summaries, None); + vp.sort_summaries(&mut summaries, None, None); assert_eq!( describe(&summaries), "foo/1.2.3, foo/1.2.4, foo/1.1.0, foo/1.0.9".to_string() ); vp.version_ordering(VersionOrdering::MinimumVersionsFirst); - vp.sort_summaries(&mut summaries, None); + vp.sort_summaries(&mut summaries, None, None); assert_eq!( describe(&summaries), "foo/1.2.3, foo/1.0.9, foo/1.1.0, foo/1.2.4".to_string() @@ -497,14 +515,14 @@ mod test { ]; vp.version_ordering(VersionOrdering::MaximumVersionsFirst); - vp.sort_summaries(&mut summaries, None); + vp.sort_summaries(&mut summaries, None, None); assert_eq!( describe(&summaries), "foo/1.2.3, foo/1.1.0, foo/1.2.4, foo/1.0.9".to_string() ); vp.version_ordering(VersionOrdering::MinimumVersionsFirst); - vp.sort_summaries(&mut summaries, None); + vp.sort_summaries(&mut summaries, None, None); assert_eq!( describe(&summaries), "foo/1.1.0, foo/1.2.3, foo/1.0.9, foo/1.2.4".to_string() @@ -527,7 +545,7 @@ mod test { ]; vp.version_ordering(VersionOrdering::MaximumVersionsFirst); - vp.sort_summaries(&mut summaries, None); + vp.sort_summaries(&mut summaries, None, None); assert_eq!( describe(&summaries), "foo/1.2.4, foo/1.2.2, foo/1.2.1, foo/1.2.0, foo/1.1.0, foo/1.0.9, foo/1.2.3" @@ -535,7 +553,7 @@ mod test { ); vp.version_ordering(VersionOrdering::MinimumVersionsFirst); - vp.sort_summaries(&mut summaries, None); + vp.sort_summaries(&mut summaries, None, None); assert_eq!( describe(&summaries), "foo/1.0.9, foo/1.1.0, foo/1.2.0, foo/1.2.1, foo/1.2.2, foo/1.2.4, foo/1.2.3" @@ -559,7 +577,7 @@ mod test { ]; vp.version_ordering(VersionOrdering::MaximumVersionsFirst); - vp.sort_summaries(&mut summaries, None); + vp.sort_summaries(&mut summaries, None, None); assert_eq!( describe(&summaries), "foo/1.2.4, foo/1.2.2, foo/1.2.0, foo/1.1.0, foo/1.0.9, foo/1.2.1, foo/1.2.3" @@ -567,7 +585,7 @@ mod test { ); vp.version_ordering(VersionOrdering::MinimumVersionsFirst); - vp.sort_summaries(&mut summaries, None); + vp.sort_summaries(&mut summaries, None, None); assert_eq!( describe(&summaries), "foo/1.0.9, foo/1.1.0, foo/1.2.0, foo/1.2.2, foo/1.2.4, foo/1.2.1, foo/1.2.3" @@ -580,7 +598,11 @@ mod test { let vp = VersionPreferences::default(); let mut summaries = vec![]; - vp.sort_summaries(&mut summaries, Some(VersionOrdering::MaximumVersionsFirst)); + vp.sort_summaries( + &mut summaries, + Some(VersionOrdering::MaximumVersionsFirst), + None, + ); assert_eq!(summaries, vec![]); }