diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
| commit | 8cc1ba8a02c0318b65658f1634de378c780392b9 (patch) | |
| tree | fdd5cb61e488018891a486b25991b87c84220bb8 /crates/mozart-sat-resolver/src/rule_set_generator.rs | |
| parent | 72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4 (diff) | |
| download | php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.gz php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.zst php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.zip | |
refactor(workspace): consolidate crates into mozart-core
Merged mozart-archiver, mozart-autoload, mozart-registry,
mozart-sat-resolver, and mozart-vcs into mozart-core to align
the source layout with Composer's structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-sat-resolver/src/rule_set_generator.rs')
| -rw-r--r-- | crates/mozart-sat-resolver/src/rule_set_generator.rs | 464 |
1 files changed, 0 insertions, 464 deletions
diff --git a/crates/mozart-sat-resolver/src/rule_set_generator.rs b/crates/mozart-sat-resolver/src/rule_set_generator.rs deleted file mode 100644 index 2ab9f86..0000000 --- a/crates/mozart-sat-resolver/src/rule_set_generator.rs +++ /dev/null @@ -1,464 +0,0 @@ -use crate::pool::{Literal, PackageId, Pool, PoolLink}; -use crate::rule::{ReasonData, Rule, RuleReason, RuleType}; -use crate::rule_set::RuleSet; -use indexmap::IndexMap; -use indexmap::IndexSet; -use mozart_semver::VersionConstraint; -use std::collections::VecDeque; - -/// Generates SAT rules from the pool and request. -/// -/// Port of Composer's RuleSetGenerator.php. -pub struct RuleSetGenerator<'a> { - pool: &'a mut Pool, - rules: RuleSet, - /// Packages already processed. - added_map: IndexSet<PackageId>, - /// Package names → list of package IDs with that name (non-alias). - added_packages_by_name: IndexMap<String, Vec<PackageId>>, - /// Specific platform packages to ignore (from `--ignore-platform-req=name`). - ignore_platform_reqs: IndexSet<String>, - /// When true, every platform package is treated as ignored. - /// Mirrors `--ignore-platform-reqs` (no value). - ignore_all_platform_reqs: bool, -} - -impl<'a> RuleSetGenerator<'a> { - pub fn new(pool: &'a mut Pool) -> Self { - RuleSetGenerator { - pool, - rules: RuleSet::new(), - added_map: IndexSet::new(), - added_packages_by_name: IndexMap::new(), - ignore_platform_reqs: IndexSet::new(), - ignore_all_platform_reqs: false, - } - } - - /// Set platform requirements to ignore. - pub fn set_ignore_platform_reqs(&mut self, names: IndexSet<String>) { - self.ignore_platform_reqs = names; - } - - /// When set, every platform package is treated as ignored. - pub fn set_ignore_all_platform_reqs(&mut self, ignore_all: bool) { - self.ignore_all_platform_reqs = ignore_all; - } - - fn is_ignored_platform_dep(&self, name: &str) -> bool { - if self - .ignore_platform_reqs - .iter() - .any(|p| mozart_core::matches_wildcard(name, p)) - { - return true; - } - self.ignore_all_platform_reqs && mozart_core::platform::is_platform_package(name) - } - - /// Generate rules for a set of requirements and fixed packages. - /// - /// Port of Composer's RuleSetGenerator::getRulesFor. - /// - /// `root_provides` / `root_replaces` map a target package name to the - /// constraint declared in the root composer.json's `provide` / `replace` - /// section. They mirror the "self-fulfilling rule" check in Composer's - /// `RuleSetGenerator::createRequireRule`: when the root package itself - /// provides or replaces a name it requires, no install-one-of rule is - /// emitted for that root require — root is implicitly already installed, - /// so the requirement is trivially satisfied without forcing a real - /// provider. Without this, Mozart picks up an inline `provided/pkg` from - /// the repository even though the root claims to fulfill it itself. - /// - /// Returns the generated rule set together with the list of root requires - /// that have no matching providers in the pool. Mirrors Composer's - /// `Solver::checkForRootRequireProblems`: a root require with zero - /// providers does not produce a SAT rule (so the solver would otherwise - /// succeed with an empty plan), but it must still be reported as an - /// unresolvable problem. - pub fn generate( - mut self, - requires: &IndexMap<String, Option<String>>, - fixed_packages: &[PackageId], - root_provides: &IndexMap<String, String>, - root_replaces: &IndexMap<String, String>, - ) -> (RuleSet, Vec<(String, Option<String>)>) { - let mut missing_root_requires: Vec<(String, Option<String>)> = Vec::new(); - // Process fixed packages - for &pkg_id in fixed_packages { - if self.pool.is_unacceptable_fixed_package(pkg_id) { - continue; - } - - self.add_rules_for_package(pkg_id); - - // Create assertion rule: this package must be installed - let rule = Rule::new( - vec![pkg_id as Literal], - RuleReason::Fixed, - ReasonData::Fixed { package_id: pkg_id }, - ); - self.rules.add(rule, RuleType::Request); - } - - // Process root requirements - for (name, constraint) in requires { - if self.is_ignored_platform_dep(name.as_str()) { - continue; - } - - // Self-fulfilling root require: if the root composer.json declares - // `provide` / `replace` for this name and the link constraint - // intersects the require constraint, drop the install-one-of rule - // entirely. Mirrors Composer's `createRequireRule` returning null - // when a provider IS the package itself: there, the root is in the - // pool as a fixed package and `whatProvides` includes it, so the - // resulting rule is trivially satisfied. Mozart does not yet add - // the root to the pool, so we make the same decision here based - // on the explicit root provide/replace tables. - if root_self_fulfills(name, constraint.as_deref(), root_provides) - || root_self_fulfills(name, constraint.as_deref(), root_replaces) - { - continue; - } - - let providers = self.pool.what_provides(name, constraint.as_deref()); - - if !providers.is_empty() { - for &pkg_id in &providers { - self.add_rules_for_package(pkg_id); - } - - // Create "install one of" rule - let literals: Vec<Literal> = providers.iter().map(|&id| id as Literal).collect(); - let rule = Rule::new( - literals, - RuleReason::RootRequire, - ReasonData::RootRequire { - package_name: name.clone(), - constraint: constraint.clone().unwrap_or_default(), - }, - ); - self.rules.add(rule, RuleType::Request); - } else { - missing_root_requires.push((name.clone(), constraint.clone())); - } - } - - // Mirror Composer's `RuleSetGenerator::addRulesForRootAliases`: - // ensure every alias whose target was already added gets its own - // alias↔target rules, even when the alias itself didn't appear in - // any root require's `whatProvides` (e.g. the synthetic - // `9999999-dev` alias from a `default-branch: true` package, which - // only matches a literal `9999999-dev` constraint). - let alias_pairs: Vec<(PackageId, PackageId)> = self - .pool - .packages() - .iter() - .filter_map(|p| p.is_alias_of.map(|t| (p.id, t))) - .collect(); - for (alias_id, target_id) in alias_pairs { - if self.added_map.contains(&target_id) && !self.added_map.contains(&alias_id) { - self.add_rules_for_package(alias_id); - } - } - - // Add conflict rules - self.add_conflict_rules(); - - (self.rules, missing_root_requires) - } - - /// Add rules for a package and its transitive dependencies. - /// - /// Port of Composer's RuleSetGenerator::addRulesForPackage. - fn add_rules_for_package(&mut self, pkg_id: PackageId) { - let mut work_queue: VecDeque<PackageId> = VecDeque::new(); - work_queue.push_back(pkg_id); - - while let Some(current_id) = work_queue.pop_front() { - if self.added_map.contains(¤t_id) { - continue; - } - self.added_map.insert(current_id); - - let pkg = self.pool.package_by_id(current_id); - 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 - // prevent two packages from coexisting under the same logical - // 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. 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) - .or_default() - .push(current_id); - } - - // Process each requirement - for link in requires { - if self.is_ignored_platform_dep(&link.target) { - continue; - } - - let possible_requires = self - .pool - .what_provides(&link.target, Some(&link.constraint)); - - // Create require rule: (-current | provider1 | provider2 | ...) - let mut literals: Vec<Literal> = vec![-(current_id as Literal)]; - let mut self_fulfilling = false; - - for &provider_id in &possible_requires { - if provider_id == current_id { - self_fulfilling = true; - break; - } - literals.push(provider_id as Literal); - } - - if !self_fulfilling { - let rule = Rule::new( - literals, - RuleReason::PackageRequires, - ReasonData::Link(PoolLink { - target: link.target.clone(), - constraint: link.constraint.clone(), - source: self.pool.package_by_id(current_id).name.clone(), - }), - ); - self.rules.add(rule, RuleType::Package); - } - - // Enqueue providers for further processing - for &provider_id in &possible_requires { - work_queue.push_back(provider_id); - } - } - } - } - - /// Add conflict rules: explicit conflicts and same-name rules. - /// - /// Port of Composer's RuleSetGenerator::addConflictRules. - fn add_conflict_rules(&mut self) { - // Explicit conflicts - let added_ids: Vec<PackageId> = self.added_map.iter().copied().collect(); - for &pkg_id in &added_ids { - let pkg = self.pool.package_by_id(pkg_id); - let conflicts = pkg.conflicts.clone(); - - for link in conflicts { - if self.is_ignored_platform_dep(&link.target) { - continue; - } - - if !self.added_packages_by_name.contains_key(&link.target) { - continue; - } - - let conflicting = self - .pool - .what_provides(&link.target, Some(&link.constraint)); - - for &conflict_id in &conflicting { - if conflict_id == pkg_id { - continue; // ignore self-conflict - } - let rule = Rule::two_literals( - -(pkg_id as Literal), - -(conflict_id as Literal), - RuleReason::PackageConflict, - ReasonData::Link(link.clone()), - ); - self.rules.add(rule, RuleType::Package); - } - } - } - - // Same-name rules: only one version of a package can be installed - let names_to_process: Vec<(String, Vec<PackageId>)> = self - .added_packages_by_name - .iter() - .filter(|(_, ids)| ids.len() > 1) - .map(|(name, ids)| (name.clone(), ids.clone())) - .collect(); - - for (name, pkg_ids) in names_to_process { - let literals: Vec<Literal> = pkg_ids.iter().map(|&id| -(id as Literal)).collect(); - - if literals.len() == 2 { - let rule = Rule::two_literals( - literals[0], - literals[1], - RuleReason::PackageSameName, - ReasonData::PackageName(name), - ); - self.rules.add(rule, RuleType::Package); - } else if literals.len() >= 3 { - let rule = Rule::multi_conflict( - literals, - RuleReason::PackageSameName, - ReasonData::PackageName(name), - ); - self.rules.add(rule, RuleType::Package); - } - } - } -} - -/// True when the root composer.json's `provide` / `replace` map declares -/// `target` with a constraint that intersects the require's constraint. A -/// missing require constraint is treated as `*` (matches anything), and a -/// missing/unparsable link constraint conservatively does NOT match — the -/// fixture fails closed back to the regular install-one-of path. -fn root_self_fulfills( - target: &str, - require_constraint: Option<&str>, - root_links: &IndexMap<String, String>, -) -> bool { - let Some(link_constraint_str) = root_links.get(target) else { - return false; - }; - let Ok(link_vc) = VersionConstraint::parse(link_constraint_str) else { - return false; - }; - match require_constraint { - None => true, - Some(req) => match VersionConstraint::parse(req) { - Ok(req_vc) => req_vc.intersects(&link_vc), - Err(_) => false, - }, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::pool::{Pool, PoolLink, PoolPackageInput}; - - fn make_input(name: &str, version: &str) -> PoolPackageInput { - PoolPackageInput { - name: name.to_string(), - version: version.to_string(), - pretty_version: version.to_string(), - requires: vec![], - replaces: vec![], - provides: vec![], - conflicts: vec![], - is_fixed: false, - is_alias_of: None, - } - } - - #[test] - fn test_root_require_generates_rule() { - let mut pool = Pool::new( - vec![make_input("a/a", "1.0.0.0"), make_input("a/a", "2.0.0.0")], - vec![], - ); - - let mut requires = IndexMap::new(); - requires.insert("a/a".to_string(), None); - - let generator = RuleSetGenerator::new(&mut pool); - let (rules, _) = generator.generate(&requires, &[], &IndexMap::new(), &IndexMap::new()); - - // Should have a request rule: (1 | 2) - let request_count = rules.iter_type(RuleType::Request).count(); - assert_eq!(request_count, 1); - - // Should have a same-name rule: (-1 | -2) - let package_count = rules.iter_type(RuleType::Package).count(); - assert!(package_count >= 1); - } - - #[test] - fn test_dependency_chain_rules() { - // a/a 1.0 requires b/b - let mut pool = Pool::new( - vec![ - PoolPackageInput { - name: "a/a".to_string(), - version: "1.0.0.0".to_string(), - pretty_version: "1.0.0".to_string(), - requires: vec![PoolLink { - target: "b/b".to_string(), - constraint: "*".to_string(), - source: "a/a".to_string(), - }], - replaces: vec![], - provides: vec![], - conflicts: vec![], - is_fixed: false, - is_alias_of: None, - }, - make_input("b/b", "1.0.0.0"), - ], - vec![], - ); - - let mut requires = IndexMap::new(); - requires.insert("a/a".to_string(), None); - - let generator = RuleSetGenerator::new(&mut pool); - let (rules, _) = generator.generate(&requires, &[], &IndexMap::new(), &IndexMap::new()); - - // Should have: - // 1. Request rule: (1) — root requires a/a - // 2. Package rule: (-1 | 2) — a/a requires b/b - assert!(rules.len() >= 2); - } - - #[test] - fn test_fixed_package_rule() { - let mut pool = Pool::new(vec![make_input("php", "8.2.0.0")], vec![]); - - let generator = RuleSetGenerator::new(&mut pool); - let (rules, _) = - generator.generate(&IndexMap::new(), &[1], &IndexMap::new(), &IndexMap::new()); - - // Should have an assertion rule: (1) - let request_rules: Vec<_> = rules.iter_type(RuleType::Request).collect(); - assert_eq!(request_rules.len(), 1); - assert!(request_rules[0].1.is_assertion()); - } -} |
