diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-02 22:59:23 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-02 22:59:23 +0900 |
| commit | b60cf8d9cb6776e5df85f080b5bb3fba252e154c (patch) | |
| tree | 66770723795378fc65f1aeab726973b18813aef8 /crates | |
| parent | 3c61a7e1e557e3b90128d2ec29227f166b17c05b (diff) | |
| download | php-mozart-b60cf8d9cb6776e5df85f080b5bb3fba252e154c.tar.gz php-mozart-b60cf8d9cb6776e5df85f080b5bb3fba252e154c.tar.zst php-mozart-b60cf8d9cb6776e5df85f080b5bb3fba252e154c.zip | |
fix(resolver): honor root self-provide/replace as require fulfilment
Port Composer's RuleSetGenerator::createRequireRule self-fulfilling
branch: when the root composer.json's `provide` or `replace` covers a
name it also requires (with intersecting constraints), skip emitting an
install-one-of rule for that root require. Composer relies on the root
package being a fixed entry in the pool so whatProvides() includes it;
Mozart does not yet add the root to the pool, so the same decision is
made via explicit `root_provide` / `root_replace` tables threaded
through ResolveRequest. Without this, an inline repo package whose name
matches the root's provide was being force-installed.
Fixes installer fixtures `provider_satisfies_its_own_requirement` and
`replacer_satisfies_its_own_requirement`.
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/mozart-registry/src/lockfile.rs | 2 | ||||
| -rw-r--r-- | crates/mozart-registry/src/resolver.rs | 20 | ||||
| -rw-r--r-- | crates/mozart-sat-resolver/src/rule_set_generator.rs | 59 | ||||
| -rw-r--r-- | crates/mozart/src/commands/create_project.rs | 10 | ||||
| -rw-r--r-- | crates/mozart/src/commands/remove.rs | 24 | ||||
| -rw-r--r-- | crates/mozart/src/commands/require.rs | 14 | ||||
| -rw-r--r-- | crates/mozart/src/commands/update.rs | 12 | ||||
| -rw-r--r-- | crates/mozart/tests/installer.rs | 4 |
8 files changed, 138 insertions, 7 deletions
diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs index 045b189..99e87c8 100644 --- a/crates/mozart-registry/src/lockfile.rs +++ b/crates/mozart-registry/src/lockfile.rs @@ -1400,6 +1400,8 @@ mod tests { ))), temporary_constraints: HashMap::new(), raw_repositories: vec![], + root_provide: HashMap::new(), + root_replace: HashMap::new(), }; let resolved = resolve(&resolve_request) diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs index a83304f..e8076b4 100644 --- a/crates/mozart-registry/src/resolver.rs +++ b/crates/mozart-registry/src/resolver.rs @@ -569,6 +569,15 @@ pub struct ResolveRequest { /// preload that still live in `resolve()` (Step B follow-up will move /// these through `RepositorySet` too). pub raw_repositories: Vec<RawRepository>, + /// Root composer.json's `provide` map (target → constraint string). Drives + /// the self-fulfilling-rule check in the SAT generator: when a root + /// `require` names something the root itself `provide`s with a matching + /// constraint, no install-one-of rule is emitted, mirroring Composer's + /// `RuleSetGenerator::createRequireRule` self-fulfillment branch. + pub root_provide: HashMap<String, String>, + /// Root composer.json's `replace` map. Same role as `root_provide` for the + /// `replace` link: a replaced target counts as fulfilled by the root. + pub root_replace: HashMap<String, String>, } /// A single package in the resolution output. @@ -848,7 +857,12 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R let mut generator = RuleSetGenerator::new(&mut pool); generator.set_ignore_platform_reqs(ignore_set); generator.set_ignore_all_platform_reqs(request.ignore_platform_reqs); - let rules = generator.generate(&root_requires, &fixed_ids); + let rules = generator.generate( + &root_requires, + &fixed_ids, + &request.root_provide, + &request.root_replace, + ); // Create policy and solve let policy = DefaultPolicy::new(request.prefer_stable, request.prefer_lowest); @@ -1247,7 +1261,7 @@ mod tests { requires.insert("foo/foo".to_string(), Some("^1.0".to_string())); let generator = RuleSetGenerator::new(&mut pool); - let rules = generator.generate(&requires, &[]); + let rules = generator.generate(&requires, &[], &HashMap::new(), &HashMap::new()); let policy = DefaultPolicy::default(); let solver = Solver::new(rules, &pool, policy, HashSet::new()); @@ -1282,6 +1296,8 @@ mod tests { ))), temporary_constraints: HashMap::new(), raw_repositories: vec![], + root_provide: HashMap::new(), + root_replace: HashMap::new(), }; let result = resolve(&request).await; diff --git a/crates/mozart-sat-resolver/src/rule_set_generator.rs b/crates/mozart-sat-resolver/src/rule_set_generator.rs index 92b9f77..8f754e5 100644 --- a/crates/mozart-sat-resolver/src/rule_set_generator.rs +++ b/crates/mozart-sat-resolver/src/rule_set_generator.rs @@ -1,6 +1,7 @@ use crate::pool::{Literal, PackageId, Pool, PoolLink}; use crate::rule::{ReasonData, Rule, RuleReason, RuleType}; use crate::rule_set::RuleSet; +use mozart_semver::VersionConstraint; use std::collections::{HashMap, HashSet, VecDeque}; /// Generates SAT rules from the pool and request. @@ -56,10 +57,22 @@ impl<'a> RuleSetGenerator<'a> { /// 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. pub fn generate( mut self, requires: &HashMap<String, Option<String>>, fixed_packages: &[PackageId], + root_provides: &HashMap<String, String>, + root_replaces: &HashMap<String, String>, ) -> RuleSet { // Process fixed packages for &pkg_id in fixed_packages { @@ -84,6 +97,21 @@ impl<'a> RuleSetGenerator<'a> { 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() { @@ -305,6 +333,31 @@ impl<'a> RuleSetGenerator<'a> { } } +/// 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: &HashMap<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::*; @@ -335,7 +388,7 @@ mod tests { requires.insert("a/a".to_string(), None); let generator = RuleSetGenerator::new(&mut pool); - let rules = generator.generate(&requires, &[]); + let rules = generator.generate(&requires, &[], &HashMap::new(), &HashMap::new()); // Should have a request rule: (1 | 2) let request_count = rules.iter_type(RuleType::Request).count(); @@ -375,7 +428,7 @@ mod tests { requires.insert("a/a".to_string(), None); let generator = RuleSetGenerator::new(&mut pool); - let rules = generator.generate(&requires, &[]); + let rules = generator.generate(&requires, &[], &HashMap::new(), &HashMap::new()); // Should have: // 1. Request rule: (1) — root requires a/a @@ -388,7 +441,7 @@ mod tests { let mut pool = Pool::new(vec![make_input("php", "8.2.0.0")], vec![]); let generator = RuleSetGenerator::new(&mut pool); - let rules = generator.generate(&HashMap::new(), &[1]); + let rules = generator.generate(&HashMap::new(), &[1], &HashMap::new(), &HashMap::new()); // Should have an assertion rule: (1) let request_rules: Vec<_> = rules.iter_type(RuleType::Request).collect(); diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs index 92081d0..af77ba6 100644 --- a/crates/mozart/src/commands/create_project.rs +++ b/crates/mozart/src/commands/create_project.rs @@ -424,6 +424,16 @@ pub async fn execute( ), temporary_constraints: HashMap::new(), raw_repositories: raw.repositories.clone(), + root_provide: raw + .provide + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + root_replace: raw + .replace + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), }; console.info("Resolving dependencies..."); diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs index 58917e9..20cb6a2 100644 --- a/crates/mozart/src/commands/remove.rs +++ b/crates/mozart/src/commands/remove.rs @@ -258,6 +258,16 @@ pub async fn execute( ), temporary_constraints: HashMap::new(), raw_repositories: raw.repositories.clone(), + root_provide: raw + .provide + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + root_replace: raw + .replace + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), }; // Print header messages @@ -518,6 +528,16 @@ async fn remove_unused( ), temporary_constraints: HashMap::new(), raw_repositories: raw.repositories.clone(), + root_provide: raw + .provide + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + root_replace: raw + .replace + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), }; console.info("Resolving dependencies to detect unused packages..."); @@ -866,6 +886,8 @@ mod tests { ), temporary_constraints: HashMap::new(), raw_repositories: vec![], + root_provide: HashMap::new(), + root_replace: HashMap::new(), }; let resolved = resolve(&request) .await @@ -917,6 +939,8 @@ mod tests { ), temporary_constraints: HashMap::new(), raw_repositories: vec![], + root_provide: HashMap::new(), + root_replace: HashMap::new(), }; let resolved2 = resolve(&request2) .await diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs index 630d960..95b26ea 100644 --- a/crates/mozart/src/commands/require.rs +++ b/crates/mozart/src/commands/require.rs @@ -647,6 +647,16 @@ pub async fn execute( ), temporary_constraints: HashMap::new(), raw_repositories: raw.repositories.clone(), + root_provide: raw + .provide + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + root_replace: raw + .replace + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), }; // Print header messages @@ -1042,6 +1052,8 @@ mod tests { ), temporary_constraints: HashMap::new(), raw_repositories: vec![], + root_provide: HashMap::new(), + root_replace: HashMap::new(), }; let resolved = resolver::resolve(&request) @@ -1110,6 +1122,8 @@ mod tests { ), temporary_constraints: HashMap::new(), raw_repositories: vec![], + root_provide: HashMap::new(), + root_replace: HashMap::new(), }; let resolved = resolver::resolve(&request) diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index 847ccf7..33b305a 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -887,6 +887,16 @@ pub async fn run( repositories: repositories.clone(), temporary_constraints, raw_repositories: composer_json.repositories.clone(), + root_provide: composer_json + .provide + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + root_replace: composer_json + .replace + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), }; // Step 6: Print header and run resolver @@ -1994,6 +2004,8 @@ mod tests { ), temporary_constraints: HashMap::new(), raw_repositories: vec![], + root_provide: HashMap::new(), + root_replace: HashMap::new(), }; let resolved = resolve(&request).await.expect("Resolution should succeed"); diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index fcb29c1..99ef2d8 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -327,7 +327,7 @@ installer_fixture!( provider_packages_can_not_be_installed_unless_selected, ignore ); -installer_fixture!(provider_satisfies_its_own_requirement, ignore); +installer_fixture!(provider_satisfies_its_own_requirement); installer_fixture!(remove_deletes_unused_deps); installer_fixture!( remove_does_nothing_if_removal_requires_update_of_dep, @@ -342,7 +342,7 @@ installer_fixture!( replaced_packages_should_not_be_installed_when_installing_from_lock, ignore ); -installer_fixture!(replacer_satisfies_its_own_requirement, ignore); +installer_fixture!(replacer_satisfies_its_own_requirement); installer_fixture!(repositories_priorities, ignore); installer_fixture!(repositories_priorities2, ignore); installer_fixture!(repositories_priorities3, ignore); |
