diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-02 22:21:25 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-02 22:21:25 +0900 |
| commit | 8da98493daf5013585e07ec98ca6960a42924edf (patch) | |
| tree | be57603fec29a4bf1e5f546b1ba2e14778595cb3 /crates/mozart-sat-resolver/src | |
| parent | 804b5b9a2a7759af24e41408c82dfc60c6092cf3 (diff) | |
| download | php-mozart-8da98493daf5013585e07ec98ca6960a42924edf.tar.gz php-mozart-8da98493daf5013585e07ec98ca6960a42924edf.tar.zst php-mozart-8da98493daf5013585e07ec98ca6960a42924edf.zip | |
feat(resolver): add branch-alias support across the resolution pipeline
Plumb Composer's `extra.branch-alias` mechanism end-to-end so a dev
branch (e.g. `dev-foobar`) can be installed alongside its numeric alias
(e.g. `3.2.x-dev`) and resolve constraints written against the alias
target.
Concretely:
- `mozart-semver`: stop treating pure-numeric `-dev` as a wildcard
branch — `3.2.9999999.9999999-dev` (the form `normalizeBranch` emits)
now parses as a classical version with `is_dev_branch=false`, so
constraints like `3.2.*` match it.
- `mozart-registry/composer_repo`: load `type: composer` repositories
from `file://` URLs (legacy embedded `packages.json`).
- `mozart-registry/resolver`: emit pool entries in pairs for dev
branches with `extra.branch-alias`, link them via `is_alias_of`, and
apply `@dev`/`@beta` etc. stability suffix flags from root requires.
- `mozart-sat-resolver`: alias rules (`PackageAlias` /
`PackageInverseAlias`) so alias and target install together; alias
packages skipped from same-name conflict indexing.
- `mozart-sat-resolver/policy`: `DefaultPolicy` now honors
`prefer_stable` via Composer's stability-tier comparison.
- `mozart-registry/lockfile`: split resolved set into real packages vs.
alias entries; populate the `aliases[]` block.
- `mozart-registry/installer_executor`: new `MarkAliasInstalled`
operation; `format_full_pretty_version` mirroring
`BasePackage::getFullPrettyVersion` (appends source ref[0..7] for
dev/git packages).
- Test harness rewrites fixture-relative `file://` URLs to absolute
paths.
Newly green fixtures: `install_branch_alias_composer_repo`,
`alias_solver_problems`, `alias_solver_problems2`,
`conflict_with_all_dependencies_option_dont_recommend_to_use_it`,
`unbounded_conflict_does_not_match_default_branch_with_branch_alias`,
`unbounded_conflict_does_not_match_default_branch_with_numeric_branch`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-sat-resolver/src')
| -rw-r--r-- | crates/mozart-sat-resolver/src/policy.rs | 46 | ||||
| -rw-r--r-- | crates/mozart-sat-resolver/src/pool.rs | 42 | ||||
| -rw-r--r-- | crates/mozart-sat-resolver/src/pool_builder.rs | 3 | ||||
| -rw-r--r-- | crates/mozart-sat-resolver/src/problem.rs | 1 | ||||
| -rw-r--r-- | crates/mozart-sat-resolver/src/rule_set_generator.rs | 36 | ||||
| -rw-r--r-- | crates/mozart-sat-resolver/src/solver.rs | 1 | ||||
| -rw-r--r-- | crates/mozart-sat-resolver/src/transaction.rs | 2 |
7 files changed, 128 insertions, 3 deletions
diff --git a/crates/mozart-sat-resolver/src/policy.rs b/crates/mozart-sat-resolver/src/policy.rs index aa63be7..a66719f 100644 --- a/crates/mozart-sat-resolver/src/policy.rs +++ b/crates/mozart-sat-resolver/src/policy.rs @@ -64,8 +64,20 @@ impl DefaultPolicy { let pkg_a = pool.literal_to_package(a); let pkg_b = pool.literal_to_package(b); - // If same name, prefer higher version (or lower if prefer_lowest) + // If same name, apply Composer's policy ordering. Mirrors + // `DefaultPolicy::versionCompare`: when `prefer_stable` is on and + // the two candidates have different stabilities, the more-stable + // one wins outright — `prefer_lowest` only kicks in within the same + // stability tier. Otherwise sort by version (asc for prefer_lowest, + // desc otherwise). if pkg_a.name == pkg_b.name { + if self.prefer_stable { + let stab_a = stability_priority(&pkg_a.version); + let stab_b = stability_priority(&pkg_b.version); + if stab_a != stab_b { + return stab_a.cmp(&stab_b); + } + } let cmp = self.compare_versions(&pkg_a.version, &pkg_b.version); return if self.prefer_lowest { cmp @@ -111,6 +123,37 @@ impl Default for DefaultPolicy { } } +/// Map a normalized version string to Composer's stability priority +/// (`BasePackage::STABILITIES`). Lower = more stable. Stable=0, RC=5, beta=10, +/// alpha=15, dev=20. Mirrors `DefaultPolicy::versionCompare`'s comparison +/// when `prefer_stable` is set. +fn stability_priority(version: &str) -> u8 { + let Ok(v) = mozart_semver::Version::parse(version) else { + return 0; + }; + if v.is_dev_branch { + return 20; + } + match v.pre_release.as_deref() { + None => 0, + Some(pre) => { + let lower = pre.to_lowercase(); + if lower.starts_with("dev") { + 20 + } else if lower.starts_with("alpha") || lower == "a" { + 15 + } else if lower.starts_with("beta") || lower == "b" { + 10 + } else if lower.starts_with("rc") { + 5 + } else { + // patch/pl/p / unknown → stable + 0 + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -126,6 +169,7 @@ mod tests { provides: vec![], conflicts: vec![], is_fixed: false, + is_alias_of: None, } } diff --git a/crates/mozart-sat-resolver/src/pool.rs b/crates/mozart-sat-resolver/src/pool.rs index 652fc60..0312c24 100644 --- a/crates/mozart-sat-resolver/src/pool.rs +++ b/crates/mozart-sat-resolver/src/pool.rs @@ -53,6 +53,13 @@ pub struct PoolPackage { pub conflicts: Vec<PoolLink>, /// Whether this is a fixed/locked package. pub is_fixed: bool, + /// If `Some`, this package is an `AliasPackage` whose target is the + /// other pool entry with the given ID. Composer creates these for + /// `extra.branch-alias` entries (dev branch → numeric alias). When set, + /// the rule generator emits `PackageAlias`/`PackageInverseAlias` rules + /// instead of regular requires; same-name conflict rules also skip + /// alias packages. + pub is_alias_of: Option<PackageId>, } impl PoolPackage { @@ -99,6 +106,12 @@ pub struct PoolPackageInput { pub provides: Vec<PoolLink>, pub conflicts: Vec<PoolLink>, pub is_fixed: bool, + /// When `Some`, the value is the **normalized** version of another input + /// in this build batch with the same `name`; the pool will resolve it to + /// that input's [`PackageId`] in [`PoolPackage::is_alias_of`]. Used by + /// the registry layer to materialize Composer's `AliasPackage` for + /// `extra.branch-alias` entries. + pub is_alias_of: Option<String>, } /// The package pool: contains all candidate packages for dependency resolution. @@ -119,11 +132,17 @@ pub struct Pool { impl Pool { /// Create a new pool from a list of package inputs. pub fn new(inputs: Vec<PoolPackageInput>, unacceptable_fixed_ids: Vec<PackageId>) -> Self { - let mut packages = Vec::with_capacity(inputs.len()); + let mut packages: Vec<PoolPackage> = Vec::with_capacity(inputs.len()); let mut package_by_name: HashMap<String, Vec<PackageId>> = HashMap::new(); + // Collect alias links (alias_idx, target_name, target_normalized) for + // a second pass once every input has a stable ID. + let mut pending_aliases: Vec<(usize, String, String)> = Vec::new(); for (idx, input) in inputs.into_iter().enumerate() { let id = (idx as PackageId) + 1; + if let Some(target) = input.is_alias_of.clone() { + pending_aliases.push((idx, input.name.clone(), target)); + } let pkg = PoolPackage { id, name: input.name, @@ -134,6 +153,7 @@ impl Pool { provides: input.provides, conflicts: input.conflicts, is_fixed: input.is_fixed, + is_alias_of: None, }; // Index by all names this package provides @@ -147,6 +167,25 @@ impl Pool { packages.push(pkg); } + // Resolve alias targets: for each alias input, find the matching + // (name, normalized version) entry and store its ID. Mirrors the + // post-construction wiring Composer does in + // `RepositorySet::createAliasPackage` / `addPackage`. + for (alias_idx, name, target_normalized) in pending_aliases { + if let Some(ids) = package_by_name.get(&name) { + let target_id = ids.iter().copied().find(|&id| { + let candidate = &packages[(id - 1) as usize]; + !candidate.name.is_empty() + && candidate.name == name + && candidate.version == target_normalized + && candidate.is_alias_of.is_none() + }); + if let Some(tid) = target_id { + packages[alias_idx].is_alias_of = Some(tid); + } + } + } + Pool { packages, package_by_name, @@ -317,6 +356,7 @@ mod tests { provides: vec![], conflicts: vec![], is_fixed: false, + is_alias_of: None, } } diff --git a/crates/mozart-sat-resolver/src/pool_builder.rs b/crates/mozart-sat-resolver/src/pool_builder.rs index 544cac3..94bbf4c 100644 --- a/crates/mozart-sat-resolver/src/pool_builder.rs +++ b/crates/mozart-sat-resolver/src/pool_builder.rs @@ -151,6 +151,7 @@ mod tests { provides: vec![], conflicts: vec![], is_fixed: false, + is_alias_of: None, }); // Should have b/b pending @@ -166,6 +167,7 @@ mod tests { provides: vec![], conflicts: vec![], is_fixed: false, + is_alias_of: None, }); // No more pending @@ -188,6 +190,7 @@ mod tests { provides: vec![], conflicts: vec![], is_fixed: false, + is_alias_of: None, }; assert!(builder.add_package(input.clone())); diff --git a/crates/mozart-sat-resolver/src/problem.rs b/crates/mozart-sat-resolver/src/problem.rs index 7ba60bc..c453fa9 100644 --- a/crates/mozart-sat-resolver/src/problem.rs +++ b/crates/mozart-sat-resolver/src/problem.rs @@ -415,6 +415,7 @@ mod tests { provides: vec![], conflicts: vec![], is_fixed: false, + is_alias_of: None, } } diff --git a/crates/mozart-sat-resolver/src/rule_set_generator.rs b/crates/mozart-sat-resolver/src/rule_set_generator.rs index 83570d5..b5dfcdb 100644 --- a/crates/mozart-sat-resolver/src/rule_set_generator.rs +++ b/crates/mozart-sat-resolver/src/rule_set_generator.rs @@ -128,6 +128,37 @@ impl<'a> RuleSetGenerator<'a> { let conflict_names: Vec<String> = pkg.conflict_names().into_iter().map(String::from).collect(); let requires = pkg.requires.clone(); + let alias_target = pkg.is_alias_of; + + if let Some(target_id) = alias_target { + // Mirror Composer's RuleSetGenerator::addRulesForPackage alias + // branch: enqueue the target, emit `(-alias | target)` so the + // alias forces the target, and `(-target | alias)` so the + // target forces the alias (they install together). The alias + // is NOT indexed under its name for same-name conflicts — + // Composer skips that for aliases too. + work_queue.push_back(target_id); + + let alias_rule = Rule::two_literals( + -(current_id as Literal), + target_id as Literal, + RuleReason::PackageAlias, + ReasonData::AliasPackage(current_id), + ); + self.rules.add(alias_rule, RuleType::Package); + + let inverse_rule = Rule::two_literals( + -(target_id as Literal), + current_id as Literal, + RuleReason::PackageInverseAlias, + ReasonData::AliasPackage(current_id), + ); + self.rules.add(inverse_rule, RuleType::Package); + + // The aliased target carries the actual requires; skip + // alias's own (link-rewritten copy) to avoid duplicates. + continue; + } // Index by every name this package fully claims (own name + // `replace` targets). Same-name conflict rules (below) then @@ -135,7 +166,8 @@ impl<'a> RuleSetGenerator<'a> { // identity. Mirrors `BasePackage::getNames(false)` indexing in // Composer's RuleSetGenerator::addRulesForPackage — `provide` // targets are intentionally omitted so that providers can - // coexist with the package they provide. + // coexist with the package they provide. Alias packages are + // skipped because the target package's name already covers them. for name in conflict_names { self.added_packages_by_name .entry(name) @@ -270,6 +302,7 @@ mod tests { provides: vec![], conflicts: vec![], is_fixed: false, + is_alias_of: None, } } @@ -313,6 +346,7 @@ mod tests { provides: vec![], conflicts: vec![], is_fixed: false, + is_alias_of: None, }, make_input("b/b", "1.0.0.0"), ], diff --git a/crates/mozart-sat-resolver/src/solver.rs b/crates/mozart-sat-resolver/src/solver.rs index 7ade361..49a4ce4 100644 --- a/crates/mozart-sat-resolver/src/solver.rs +++ b/crates/mozart-sat-resolver/src/solver.rs @@ -827,6 +827,7 @@ mod tests { provides: vec![], conflicts: vec![], is_fixed: false, + is_alias_of: None, } } diff --git a/crates/mozart-sat-resolver/src/transaction.rs b/crates/mozart-sat-resolver/src/transaction.rs index a325601..176b862 100644 --- a/crates/mozart-sat-resolver/src/transaction.rs +++ b/crates/mozart-sat-resolver/src/transaction.rs @@ -364,6 +364,7 @@ mod tests { provides: vec![], conflicts: vec![], is_fixed: false, + is_alias_of: None, } } @@ -391,6 +392,7 @@ mod tests { provides: vec![], conflicts: vec![], is_fixed: false, + is_alias_of: None, } } |
