From 177b894d7d77a5297bee3b2487ef18a0cae7a596 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 3 May 2026 21:50:54 +0900 Subject: fix(resolver): expand root branch-alias and self.version replace links Two related parity gaps surfaced by the `circular-dependency` fixture: 1. The root's `extra.branch-alias` entry was never materialized in the pool, and root-level `replace`/`provide`/`conflict` constraints written as `self.version` were forwarded verbatim. Mirror Composer's `RootAliasPackage`: resolve `self.version` against the root's declared version for the base entry, then add an extra alias entry (carrying the base links plus a duplicate link per `self.version` original retagged at the alias's version) when the root's version matches an `extra.branch-alias` key. 2. `Pool::matches_package` returned on the first link to a target name even when its constraint did not match the query, hiding any later link to the same target. With the alias above, that masked the second `replace` link tagged at the alias version. Keep iterating when target matches but constraint does not, so a later link can still satisfy. --- crates/mozart-registry/src/lockfile.rs | 1 + crates/mozart-registry/src/resolver.rs | 104 ++++++++++++++++++++++++--------- 2 files changed, 77 insertions(+), 28 deletions(-) (limited to 'crates/mozart-registry/src') diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs index 5114fb7..701a6f7 100644 --- a/crates/mozart-registry/src/lockfile.rs +++ b/crates/mozart-registry/src/lockfile.rs @@ -1679,6 +1679,7 @@ mod tests { locked_package_names: IndexSet::new(), locked_packages: Vec::new(), block_abandoned: false, + root_branch_alias: None, }; let resolved = resolve(&resolve_request) diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs index f7feddd..66e923d 100644 --- a/crates/mozart-registry/src/resolver.rs +++ b/crates/mozart-registry/src/resolver.rs @@ -725,6 +725,14 @@ pub struct ResolveRequest { /// versions, so a root requirement that only matches abandoned candidates /// fails with the standard "could not be resolved" error. pub block_abandoned: bool, + /// Pretty form of the root's `extra.branch-alias` target when the root's + /// version matches a key in that map (e.g. `dev-master` → `2.0-dev`). + /// Mirrors Composer's `RootAliasPackage`: an extra alias entry is added + /// to the pool exposing the root under the numeric branch-alias version, + /// with `replace`/`provide`/`conflict` links extended to advertise the + /// alias's version for any link originally written as `self.version`. + /// `None` when the root carries no matching `branch-alias` entry. + pub root_branch_alias: Option, } /// Full data for a lock-pinned package, used in partial updates. Carried on @@ -906,42 +914,81 @@ pub async fn resolve(request: &ResolveRequest) -> Result, R Some(v) if !v.is_empty() => (v.to_string(), v.to_string()), _ => ("1.0.0+no-version-set".to_string(), "1.0.0.0".to_string()), }; - let root_input = PoolPackageInput { - name: root_name_lower.clone(), - version: root_normalized, - pretty_version: root_pretty, - requires: vec![], - replaces: request - .root_replace - .iter() - .map(|(target, constraint)| PoolLink { - target: target.to_lowercase(), - constraint: constraint.clone(), - source: root_name_lower.clone(), - }) - .collect(), - provides: request - .root_provide - .iter() - .map(|(target, constraint)| PoolLink { - target: target.to_lowercase(), - constraint: constraint.clone(), - source: root_name_lower.clone(), - }) - .collect(), - conflicts: request - .root_conflict - .iter() + // Resolve `self.version` against the root's normalized version when + // building base links. Mirrors Composer's `ArrayLoader::createLink`: + // a `self.version` constraint is parsed against the declaring package's + // pretty version (here, the root's). The base entry only carries this + // resolved form; any branch-alias entry below extends each base link + // with an extra link tagged at the alias's version, matching + // `AliasPackage::replaceSelfVersionDependencies`. + let make_base_links = |raw: &IndexMap| -> Vec { + raw.iter() .map(|(target, constraint)| PoolLink { target: target.to_lowercase(), - constraint: constraint.clone(), + constraint: if constraint.trim() == "self.version" { + root_normalized.clone() + } else { + constraint.clone() + }, source: root_name_lower.clone(), }) - .collect(), + .collect() + }; + let base_replaces = make_base_links(&request.root_replace); + let base_provides = make_base_links(&request.root_provide); + let base_conflicts = make_base_links(&request.root_conflict); + let root_input = PoolPackageInput { + name: root_name_lower.clone(), + version: root_normalized.clone(), + pretty_version: root_pretty.clone(), + requires: vec![], + replaces: base_replaces.clone(), + provides: base_provides.clone(), + conflicts: base_conflicts.clone(), is_fixed: true, is_alias_of: None, }; builder.add_package(root_input); + + // Materialize a branch-alias entry for the root when `extra.branch-alias` + // mapped this version to a numeric alias (e.g. dev-master → 2.0-dev). + // Mirrors Composer's `RootAliasPackage`: the alias copies the base's + // resolved replace/provide/conflict links and then ADDS one more link + // per `self.version` original, this time pinned at the alias's own + // version. So a transitive `provided/dependency 2.*` lookup can be + // satisfied through the alias even though the base resolved + // `self.version` to a non-matching dev version. + if let Some(alias_pretty) = &request.root_branch_alias + && let Some(alias_normalized) = normalize_branch_alias_target(alias_pretty) + { + let extra_self_version_links = |raw: &IndexMap| -> Vec { + raw.iter() + .filter(|(_, constraint)| constraint.trim() == "self.version") + .map(|(target, _)| PoolLink { + target: target.to_lowercase(), + constraint: alias_normalized.clone(), + source: root_name_lower.clone(), + }) + .collect() + }; + let mut alias_replaces = base_replaces.clone(); + alias_replaces.extend(extra_self_version_links(&request.root_replace)); + let mut alias_provides = base_provides.clone(); + alias_provides.extend(extra_self_version_links(&request.root_provide)); + let mut alias_conflicts = base_conflicts.clone(); + alias_conflicts.extend(extra_self_version_links(&request.root_conflict)); + builder.add_package(PoolPackageInput { + name: root_name_lower.clone(), + version: alias_normalized, + pretty_version: alias_pretty.clone(), + requires: vec![], + replaces: alias_replaces, + provides: alias_provides, + conflicts: alias_conflicts, + is_fixed: false, + is_alias_of: Some(root_normalized), + }); + } } // Add lock-pinned packages as pool entries (partial-update case). @@ -1738,6 +1785,7 @@ mod tests { locked_package_names: IndexSet::new(), locked_packages: Vec::new(), block_abandoned: false, + root_branch_alias: None, }; let result = resolve(&request).await; -- cgit v1.3.1