aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-sat-resolver
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-03 22:07:34 +0900
committernsfisis <nsfisis@gmail.com>2026-05-03 22:07:34 +0900
commit38706a6f0ceb773d473c4f5ddebf49e8e5ae46dc (patch)
treea38de4fbf27e6834eae3944bbd86ce53b13236cb /crates/mozart-sat-resolver
parent64f8bb0c1aa16d78c5edc3f3de5dd3ff6e5861de (diff)
downloadphp-mozart-38706a6f0ceb773d473c4f5ddebf49e8e5ae46dc.tar.gz
php-mozart-38706a6f0ceb773d473c4f5ddebf49e8e5ae46dc.tar.zst
php-mozart-38706a6f0ceb773d473c4f5ddebf49e8e5ae46dc.zip
fix(update): apply --minimal-changes via policy preferred versions
The previous implementation pinned every resolved package back to its locked version after the resolve, which discarded the new versions the solver had to pick when a root constraint moved off the lock (e.g. a require bumped from `1.*` to `2.*`). The lock effectively never moved, so transitive cascades from a forced root-level update were lost. Mirror Composer's `Installer::createPolicy(forUpdate=true, minimalUpdate=true)` instead: thread the lock's `name → normalized version` map through the policy as `preferred_versions`. The solver now picks the locked version as a tiebreaker when it still satisfies the active constraints, but moves freely when a constraint forces a different version. Drop the post-process hook entirely.
Diffstat (limited to 'crates/mozart-sat-resolver')
-rw-r--r--crates/mozart-sat-resolver/src/policy.rs41
1 files changed, 41 insertions, 0 deletions
diff --git a/crates/mozart-sat-resolver/src/policy.rs b/crates/mozart-sat-resolver/src/policy.rs
index a253c39..f45c4f5 100644
--- a/crates/mozart-sat-resolver/src/policy.rs
+++ b/crates/mozart-sat-resolver/src/policy.rs
@@ -10,6 +10,14 @@ pub struct DefaultPolicy {
pub prefer_stable: bool,
/// Whether to prefer lowest versions.
pub prefer_lowest: bool,
+ /// `name → normalized version` overrides used when more than one
+ /// candidate could satisfy a requirement: a literal pinned at the
+ /// preferred version wins outright over the usual highest/lowest pick.
+ /// Mirrors Composer's `DefaultPolicy::pruneToBestVersion` behavior under
+ /// `--minimal-changes`, where the lock's previously-installed versions
+ /// are passed in so the solver only moves a package when a constraint
+ /// actually forces a different version.
+ pub preferred_versions: Option<IndexMap<String, String>>,
}
impl DefaultPolicy {
@@ -17,6 +25,19 @@ impl DefaultPolicy {
DefaultPolicy {
prefer_stable,
prefer_lowest,
+ preferred_versions: None,
+ }
+ }
+
+ pub fn with_preferred(
+ prefer_stable: bool,
+ prefer_lowest: bool,
+ preferred_versions: IndexMap<String, String>,
+ ) -> Self {
+ DefaultPolicy {
+ prefer_stable,
+ prefer_lowest,
+ preferred_versions: Some(preferred_versions),
}
}
@@ -123,6 +144,26 @@ impl DefaultPolicy {
return vec![];
}
+ // Mirror Composer's `DefaultPolicy::pruneToBestVersion` short-circuit:
+ // when a preferred version is set for this package and one of the
+ // candidates matches it exactly, that wins over the regular
+ // highest/lowest pick. Falls through otherwise (e.g. the locked
+ // version no longer satisfies the constraint and was filtered out
+ // before reaching this method).
+ if let Some(ref preferred) = self.preferred_versions {
+ let name = pool.literal_to_package(literals[0]).name.clone();
+ if let Some(preferred_ver) = preferred.get(&name) {
+ let preferred_lits: Vec<Literal> = literals
+ .iter()
+ .filter(|&&lit| pool.literal_to_package(lit).version == *preferred_ver)
+ .copied()
+ .collect();
+ if !preferred_lits.is_empty() {
+ return preferred_lits;
+ }
+ }
+ }
+
// The first literal is the best after sorting
let best_version = &pool.literal_to_package(literals[0]).version;
literals