From 489d00ca3f096f69f3b05f9564b23bb70a2475c7 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 3 May 2026 11:17:02 +0900 Subject: fix(resolver): fail when a root require has no matching providers Mirror Composer's `Solver::checkForRootRequireProblems`: a root require that resolves to zero pool providers produces no SAT rule, so the solver previously succeeded with an empty plan instead of reporting the unresolvable requirement. `RuleSetGenerator::generate` now returns those misses alongside the rule set, and `resolve()` short-circuits into `ResolveError::NoSolution` so install/update exit with code 2 to match Composer. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/mozart-registry/src/resolver.rs | 29 ++++++++++++++++++++-- .../mozart-sat-resolver/src/rule_set_generator.rs | 21 ++++++++++++---- crates/mozart/tests/installer.rs | 12 ++++----- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs index 9cc0751..da2f444 100644 --- a/crates/mozart-registry/src/resolver.rs +++ b/crates/mozart-registry/src/resolver.rs @@ -861,13 +861,38 @@ pub async fn resolve(request: &ResolveRequest) -> Result, 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( + let (rules, missing_root_requires) = generator.generate( &root_requires, &fixed_ids, &request.root_provide, &request.root_replace, ); + // Mirror Composer's `Solver::checkForRootRequireProblems`: a root require + // with no providers in the pool yields no SAT rule, so the solver would + // succeed with an empty plan. Surface it as an unresolvable problem + // instead, matching Composer's exit code 2 behaviour. + if !missing_root_requires.is_empty() { + let problems: Vec = missing_root_requires + .iter() + .map(|(name, constraint)| match constraint.as_deref() { + Some(c) if !c.is_empty() => format!( + " - Root composer.json requires {name} {c}, no matching package found." + ), + _ => { + format!(" - Root composer.json requires {name}, no matching package found.") + } + }) + .collect(); + let report = problems + .into_iter() + .enumerate() + .map(|(i, msg)| format!(" Problem {}\n{}", i + 1, msg)) + .collect::>() + .join("\n"); + return Err(ResolveError::NoSolution(report)); + } + // Create policy and solve let policy = DefaultPolicy::new(request.prefer_stable, request.prefer_lowest); let fixed_set: HashSet = fixed_ids.into_iter().collect(); @@ -1265,7 +1290,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, &[], &HashMap::new(), &HashMap::new()); + let (rules, _) = generator.generate(&requires, &[], &HashMap::new(), &HashMap::new()); let policy = DefaultPolicy::default(); let solver = Solver::new(rules, &pool, policy, HashSet::new()); diff --git a/crates/mozart-sat-resolver/src/rule_set_generator.rs b/crates/mozart-sat-resolver/src/rule_set_generator.rs index 8f754e5..11c04cc 100644 --- a/crates/mozart-sat-resolver/src/rule_set_generator.rs +++ b/crates/mozart-sat-resolver/src/rule_set_generator.rs @@ -67,13 +67,21 @@ impl<'a> RuleSetGenerator<'a> { /// 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: &HashMap>, fixed_packages: &[PackageId], root_provides: &HashMap, root_replaces: &HashMap, - ) -> RuleSet { + ) -> (RuleSet, Vec<(String, Option)>) { + let mut missing_root_requires: Vec<(String, Option)> = Vec::new(); // Process fixed packages for &pkg_id in fixed_packages { if self.pool.is_unacceptable_fixed_package(pkg_id) { @@ -130,6 +138,8 @@ impl<'a> RuleSetGenerator<'a> { }, ); self.rules.add(rule, RuleType::Request); + } else { + missing_root_requires.push((name.clone(), constraint.clone())); } } @@ -154,7 +164,7 @@ impl<'a> RuleSetGenerator<'a> { // Add conflict rules self.add_conflict_rules(); - self.rules + (self.rules, missing_root_requires) } /// Add rules for a package and its transitive dependencies. @@ -388,7 +398,7 @@ mod tests { requires.insert("a/a".to_string(), None); let generator = RuleSetGenerator::new(&mut pool); - let rules = generator.generate(&requires, &[], &HashMap::new(), &HashMap::new()); + 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(); @@ -428,7 +438,7 @@ mod tests { requires.insert("a/a".to_string(), None); let generator = RuleSetGenerator::new(&mut pool); - let rules = generator.generate(&requires, &[], &HashMap::new(), &HashMap::new()); + let (rules, _) = generator.generate(&requires, &[], &HashMap::new(), &HashMap::new()); // Should have: // 1. Request rule: (1) — root requires a/a @@ -441,7 +451,8 @@ 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], &HashMap::new(), &HashMap::new()); + 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/tests/installer.rs b/crates/mozart/tests/installer.rs index 7ba53a8..a13e8e6 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -219,7 +219,7 @@ installer_fixture!(abandoned_listed); installer_fixture!(alias_in_complex_constraints, ignore); installer_fixture!(alias_in_lock, ignore); installer_fixture!(alias_in_lock2, ignore); -installer_fixture!(alias_on_unloadable_package, ignore); +installer_fixture!(alias_on_unloadable_package); installer_fixture!(alias_solver_problems); installer_fixture!(alias_solver_problems2); installer_fixture!(alias_with_reference, ignore); @@ -257,7 +257,7 @@ installer_fixture!(github_issues_8902); installer_fixture!(github_issues_8903, ignore); installer_fixture!(github_issues_9012, ignore); installer_fixture!(github_issues_9290, ignore); -installer_fixture!(hint_main_rename, ignore); +installer_fixture!(hint_main_rename); installer_fixture!(install_aliased_alias, ignore); installer_fixture!(install_branch_alias_composer_repo); installer_fixture!(install_dev); @@ -304,7 +304,7 @@ installer_fixture!( installer_fixture!(partial_update_with_dependencies_provide, ignore); installer_fixture!(partial_update_with_dependencies_replace, ignore); installer_fixture!(partial_update_with_deps_warns_root, ignore); -installer_fixture!(partial_update_with_symlinked_path_repos, ignore); +installer_fixture!(partial_update_with_symlinked_path_repos); installer_fixture!(partial_update_without_lock); installer_fixture!(platform_ext_solver_problems); installer_fixture!(plugins_are_installed_first); @@ -343,15 +343,15 @@ installer_fixture!( ignore ); installer_fixture!(replacer_satisfies_its_own_requirement); -installer_fixture!(repositories_priorities, ignore); +installer_fixture!(repositories_priorities); installer_fixture!(repositories_priorities2); installer_fixture!(repositories_priorities3, ignore); installer_fixture!(repositories_priorities4); -installer_fixture!(repositories_priorities5, ignore); +installer_fixture!(repositories_priorities5); installer_fixture!(root_alias_change_with_circular_dep, ignore); installer_fixture!(root_alias_gets_loaded_for_locked_pkgs); installer_fixture!(root_requirements_do_not_affect_locked_versions); -installer_fixture!(solver_problem_with_hash_in_branch, ignore); +installer_fixture!(solver_problem_with_hash_in_branch); installer_fixture!(solver_problems); installer_fixture!(solver_problems_with_disabled_platform); installer_fixture!(suggest_installed); -- cgit v1.3.1