aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-sat-resolver/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-02 22:21:25 +0900
committernsfisis <nsfisis@gmail.com>2026-05-02 22:21:25 +0900
commit8da98493daf5013585e07ec98ca6960a42924edf (patch)
treebe57603fec29a4bf1e5f546b1ba2e14778595cb3 /crates/mozart-sat-resolver/src
parent804b5b9a2a7759af24e41408c82dfc60c6092cf3 (diff)
downloadphp-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.rs46
-rw-r--r--crates/mozart-sat-resolver/src/pool.rs42
-rw-r--r--crates/mozart-sat-resolver/src/pool_builder.rs3
-rw-r--r--crates/mozart-sat-resolver/src/problem.rs1
-rw-r--r--crates/mozart-sat-resolver/src/rule_set_generator.rs36
-rw-r--r--crates/mozart-sat-resolver/src/solver.rs1
-rw-r--r--crates/mozart-sat-resolver/src/transaction.rs2
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,
}
}