diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-03 13:14:00 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-03 13:14:00 +0900 |
| commit | 3c0527aa63574f17c9f372b6187d5690e0cbaff0 (patch) | |
| tree | 8159e09b685f7faf2870970d465b9698093b8973 /crates/mozart/src/commands | |
| parent | c53dfc52f6449c8d5ca0b160a2a25f99790711f2 (diff) | |
| download | php-mozart-3c0527aa63574f17c9f372b6187d5690e0cbaff0.tar.gz php-mozart-3c0527aa63574f17c9f372b6187d5690e0cbaff0.tar.zst php-mozart-3c0527aa63574f17c9f372b6187d5690e0cbaff0.zip | |
fix(resolver): apply root "X as Y" aliases via pool second pass
Mirrors Composer's `RootPackageLoader::extractAliases` +
`PoolBuilder::loadPackage` flow: strip the `as` clause from each root
require so the SAT side sees only the LEFT-hand constraint, and after
every package is loaded run a second pass that materializes an alias
entry for any input matching `(name, version_normalized)`. Locked-only
packages in a partial update are excluded via a new
`ResolveRequest::locked_package_names` so they don't pick up the alias
(`propagateUpdate=false` in Composer).
Two adjacent fixes uncovered while making `install_aliased_alias`
green:
- `Version::cmp` treated unnamed wildcard branches (`1.0.x-dev`,
`is_dev_branch=true && name=None`) as below every numeric version.
They are semantically the same as the four-segment `*-dev` form
Composer's `normalizeBranch` emits, so let only *named* branches
take the shortcut.
- `Constraint::Exact` / `NotEqual` used the derived `==`, which
compared `is_dev_branch` field-by-field and missed the
wildcard/numeric equivalence. Switch to `cmp` so both forms count
as equal.
- `Pool::matches_package` now falls back to parsing `pretty_version`
when the `version` parse doesn't match the constraint, so a
`dev-master` query lines up with a pool entry stored as the
internal `9999999.x.x.x-dev` expansion.
Net effect on installer fixtures: `install_aliased_alias` newly
green, plus `aliased_priority`, `aliased_priority_conflicting`, and
`install_dev_using_dist` come along for the ride.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart/src/commands')
| -rw-r--r-- | crates/mozart/src/commands/create_project.rs | 1 | ||||
| -rw-r--r-- | crates/mozart/src/commands/remove.rs | 6 | ||||
| -rw-r--r-- | crates/mozart/src/commands/require.rs | 5 | ||||
| -rw-r--r-- | crates/mozart/src/commands/update.rs | 37 |
4 files changed, 48 insertions, 1 deletions
diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs index c0faa76..215eb20 100644 --- a/crates/mozart/src/commands/create_project.rs +++ b/crates/mozart/src/commands/create_project.rs @@ -440,6 +440,7 @@ pub async fn execute( .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(), + locked_package_names: indexmap::IndexSet::new(), }; console.info("Resolving dependencies..."); diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs index 08f7cc6..15b3586 100644 --- a/crates/mozart/src/commands/remove.rs +++ b/crates/mozart/src/commands/remove.rs @@ -274,6 +274,7 @@ pub async fn execute( .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(), + locked_package_names: indexmap::IndexSet::new(), }; // Print header messages @@ -550,6 +551,7 @@ async fn remove_unused( .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(), + locked_package_names: indexmap::IndexSet::new(), }; console.info("Resolving dependencies to detect unused packages..."); @@ -859,7 +861,7 @@ mod tests { #[tokio::test] #[ignore] async fn test_remove_full_e2e() { - use indexmap::IndexMap; + use indexmap::{IndexMap, IndexSet}; use mozart_registry::lockfile::{LockFileGenerationRequest, generate_lock_file}; use mozart_registry::resolver::{ResolveRequest, resolve}; use tempfile::tempdir; @@ -902,6 +904,7 @@ mod tests { root_provide: IndexMap::new(), root_replace: IndexMap::new(), root_conflict: IndexMap::new(), + locked_package_names: IndexSet::new(), }; let resolved = resolve(&request) .await @@ -957,6 +960,7 @@ mod tests { root_provide: IndexMap::new(), root_replace: IndexMap::new(), root_conflict: IndexMap::new(), + locked_package_names: IndexSet::new(), }; let resolved2 = resolve(&request2) .await diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs index 97d6b02..6a2917b 100644 --- a/crates/mozart/src/commands/require.rs +++ b/crates/mozart/src/commands/require.rs @@ -662,6 +662,7 @@ pub async fn execute( .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(), + locked_package_names: indexmap::IndexSet::new(), }; // Print header messages @@ -1029,6 +1030,7 @@ mod tests { #[tokio::test] #[ignore] async fn test_require_full_e2e() { + use indexmap::IndexSet; use mozart_core::package::RawPackageData; use mozart_registry::lockfile::{LockFileGenerationRequest, generate_lock_file}; @@ -1061,6 +1063,7 @@ mod tests { root_provide: IndexMap::new(), root_replace: IndexMap::new(), root_conflict: IndexMap::new(), + locked_package_names: IndexSet::new(), }; let resolved = resolver::resolve(&request) @@ -1094,6 +1097,7 @@ mod tests { #[tokio::test] #[ignore] async fn test_require_no_install_writes_lock_only() { + use indexmap::IndexSet; use mozart_core::package::RawPackageData; use tempfile::tempdir; @@ -1133,6 +1137,7 @@ mod tests { root_provide: IndexMap::new(), root_replace: IndexMap::new(), root_conflict: IndexMap::new(), + locked_package_names: IndexSet::new(), }; let resolved = resolver::resolve(&request) diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index 130d7e3..6d314dc 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -843,6 +843,41 @@ pub async fn run( .filter(|p| !matches!(p.to_lowercase().as_str(), "lock" | "nothing" | "mirrors")) .collect(); + // For partial updates (specific package names given), eagerly read the + // lock file to collect names that stay pinned across this resolve. + // The resolver uses this set to skip materializing root `as` aliases + // for those packages — Composer's `PoolBuilder::loadPackage` only + // applies a root alias when the package's update is being propagated, + // so a locked-only package keeps its locked version unaliased. + // + // Only the *names* are needed — the full lock is re-read below for + // change reporting and `apply_partial_update` post-processing. Reading + // it twice is fine: it's a small JSON file. Errors here fall back to + // an empty set (treat as full update); the later read surfaces the + // failure to the user. + let locked_package_names: IndexSet<String> = if !raw_packages.is_empty() && lock_path.exists() { + match lockfile::LockFile::read_from_file(&lock_path) { + Ok(l) => { + let updated: IndexSet<String> = + raw_packages.iter().map(|s| s.to_lowercase()).collect(); + l.packages + .iter() + .map(|p| p.name.to_lowercase()) + .chain( + l.packages_dev + .iter() + .flatten() + .map(|p| p.name.to_lowercase()), + ) + .filter(|n| !updated.contains(n)) + .collect() + } + Err(_) => IndexSet::new(), + } + } else { + IndexSet::new() + }; + // Step 5: Build the resolve request from composer.json // Filter out platform packages from require list for the resolver (they're handled separately) let require: Vec<(String, String)> = composer_json @@ -912,6 +947,7 @@ pub async fn run( .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(), + locked_package_names, }; // Step 6: Print header and run resolver @@ -2023,6 +2059,7 @@ mod tests { root_provide: IndexMap::new(), root_replace: IndexMap::new(), root_conflict: IndexMap::new(), + locked_package_names: IndexSet::new(), }; let resolved = resolve(&request).await.expect("Resolution should succeed"); |
