diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-03 12:02:08 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-03 12:02:08 +0900 |
| commit | ed77ff97d6c137ef58f0464b7a9b08bc2b875bd2 (patch) | |
| tree | 63322190bfb1de4ea0b425c31a568fa4fe58a211 /crates | |
| parent | ae1aa6540761e54a76b8f7984cf93cd3a0d011d0 (diff) | |
| download | php-mozart-ed77ff97d6c137ef58f0464b7a9b08bc2b875bd2.tar.gz php-mozart-ed77ff97d6c137ef58f0464b7a9b08bc2b875bd2.tar.zst php-mozart-ed77ff97d6c137ef58f0464b7a9b08bc2b875bd2.zip | |
fix(install): keep one cycle survivor as root for install ordering
Mozart's install-order topological sort marked every package required by
any other as non-root, so cycle members all fell out of the root set and
the cycle fallback emitted them in input (alphabetical) order. Composer
instead walks the sorted result map and removes each package's required
providers as it goes, skipping outer packages already removed — leaving
the highest-sort-key cycle member as a root and giving DFS a deterministic
entry point. Mirror that.
Unblocks the prefer_lowest_branches installer fixture.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/mozart/src/commands/install.rs | 22 | ||||
| -rw-r--r-- | crates/mozart/tests/installer.rs | 2 |
2 files changed, 16 insertions, 8 deletions
diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs index b38194e..e53b6d1 100644 --- a/crates/mozart/src/commands/install.rs +++ b/crates/mozart/src/commands/install.rs @@ -228,8 +228,10 @@ pub fn compute_operations<'a>( /// visit the package is re-pushed and its requires are pushed afterwards, /// so dep traversal order is the **reverse** of `getRequires` iteration. /// -/// Cycles fall back to input order (Composer rejects cycles earlier; this -/// branch should not normally fire). +/// Cycles produce a deterministic root: Composer's `getRootPackages` walks +/// the sorted map and removes each package's required providers as it goes, +/// skipping outer packages already removed. So among cycle members, the one +/// with the highest sort key survives as the entry point. fn topological_sort<'a>( packages: &[&'a lockfile::LockedPackage], ) -> Vec<&'a lockfile::LockedPackage> { @@ -253,18 +255,24 @@ fn topological_sort<'a>( } } - // Identify root packages: those not pulled in by any other package's - // requires (counting provides/replaces as a match). - let mut required_by_others: IndexSet<String> = IndexSet::new(); + // Mirror Composer's `getRootPackages`: walk in sorted order, removing + // each package's required providers from the candidate-roots set as we + // go. Skip outer packages that have already been removed — that ordering + // matters in cycles, where the first sorted member's requires strip the + // others from the set, leaving a single survivor. + let mut roots_set: IndexSet<String> = sorted.iter().map(|p| p.name.to_lowercase()).collect(); for pkg in &sorted { let pkg_lower = pkg.name.to_lowercase(); + if !roots_set.contains(&pkg_lower) { + continue; + } for dep in pkg.require.keys() { let dep_lower = dep.to_lowercase(); if let Some(matches) = resolves.get(&dep_lower) { for &m in matches { let m_lower = m.name.to_lowercase(); if m_lower != pkg_lower { - required_by_others.insert(m_lower); + roots_set.shift_remove(&m_lower); } } } @@ -273,7 +281,7 @@ fn topological_sort<'a>( let mut stack: Vec<&'a lockfile::LockedPackage> = sorted .iter() - .filter(|p| !required_by_others.contains(&p.name.to_lowercase())) + .filter(|p| roots_set.contains(&p.name.to_lowercase())) .copied() .collect(); diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index 3daaf1e..a0eaab0 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -308,7 +308,7 @@ installer_fixture!(partial_update_with_symlinked_path_repos); installer_fixture!(partial_update_without_lock); installer_fixture!(platform_ext_solver_problems); installer_fixture!(plugins_are_installed_first); -installer_fixture!(prefer_lowest_branches, ignore); +installer_fixture!(prefer_lowest_branches); installer_fixture!(problems_reduce_versions); installer_fixture!(provider_can_coexist_with_other_version_of_provided); installer_fixture!(provider_conflicts, ignore); |
