aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-03 16:34:10 +0900
committernsfisis <nsfisis@gmail.com>2026-05-03 16:34:10 +0900
commit9eecfe303c944c80556b0b25fcd3ce6bbce3aeb8 (patch)
treedc195bda8422447fe9cd762120a850ba726be529
parentd84024fb179e3ebb55573971a329cb6ff72d7fa0 (diff)
downloadphp-mozart-9eecfe303c944c80556b0b25fcd3ce6bbce3aeb8.tar.gz
php-mozart-9eecfe303c944c80556b0b25fcd3ce6bbce3aeb8.tar.zst
php-mozart-9eecfe303c944c80556b0b25fcd3ce6bbce3aeb8.zip
fix(install): reject locks where two packages claim the same name
Mirror Composer's `RuleSetGenerator::addConflictRules` SAME_NAME pass on the locked package set: a package's `getNames(false)` is its canonical name plus the names it claims via `replace`, and any name with two providers makes the lock-verify solve unsatisfiable. Mozart's `install` skips that solve, so the conflict slipped through and both packages were installed; surface it explicitly and exit DEPENDENCY_RESOLUTION_FAILED. `provide` targets are deliberately excluded — `getNames(false)` excludes them, since multiple providers of a virtual name may co-exist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
-rw-r--r--crates/mozart/src/commands/install.rs64
-rw-r--r--crates/mozart/tests/installer.rs5
2 files changed, 65 insertions, 4 deletions
diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs
index c7caff4..52e82ec 100644
--- a/crates/mozart/src/commands/install.rs
+++ b/crates/mozart/src/commands/install.rs
@@ -486,6 +486,55 @@ fn collect_install_platform_problems(
)
}
+/// Mirror Composer's `RuleSetGenerator::addConflictRules` SAME_NAME loop on
+/// the locked package set. `getNames(false)` returns each package's
+/// canonical name plus the names it claims via `replace`; when two distinct
+/// locked packages claim the same name, only one of them can be installed.
+/// During Composer's lock-verify solve every locked package is `fix`-locked,
+/// so two providers of the same name make the rule unsatisfiable and the
+/// solver throws `SolverProblemsException` → exit 2.
+///
+/// `provide` is intentionally excluded — `getNames(false)` excludes it, and
+/// virtual `provide` targets allow multiple co-installed providers.
+fn collect_install_same_name_problems(lock: &lockfile::LockFile, dev_mode: bool) -> Vec<String> {
+ let mut providers: BTreeMap<String, Vec<String>> = BTreeMap::new();
+
+ let mut all_pkgs: Vec<&lockfile::LockedPackage> = lock.packages.iter().collect();
+ if dev_mode {
+ all_pkgs.extend(lock.packages_dev.iter().flatten());
+ }
+
+ for p in all_pkgs {
+ let canonical = p.name.to_lowercase();
+ providers
+ .entry(canonical.clone())
+ .or_default()
+ .push(p.name.clone());
+ for replace_target in p.replace.keys() {
+ let target_lower = replace_target.to_lowercase();
+ if target_lower == canonical {
+ continue;
+ }
+ providers
+ .entry(target_lower)
+ .or_default()
+ .push(p.name.clone());
+ }
+ }
+
+ let mut problems = Vec::new();
+ for (name, owners) in &providers {
+ if owners.len() > 1 {
+ problems.push(format!(
+ "- Conflict between locked packages on name {}: {}",
+ name,
+ owners.join(", ")
+ ));
+ }
+ }
+ problems
+}
+
/// Merge platform requirements from the lock's `platform`/`platform-dev`
/// fields and the root composer.json's `require`/`require-dev`. Root
/// composer.json overrides the lock on duplicate keys (matching Composer's
@@ -1043,6 +1092,21 @@ pub async fn run(
mozart_core::exit_code::DEPENDENCY_RESOLUTION_FAILED,
));
}
+
+ let same_name_problems = collect_install_same_name_problems(&lock, dev_mode);
+ if !same_name_problems.is_empty() {
+ console.info(
+ "Your lock file does not contain a compatible set of packages. Please run composer update.",
+ );
+ console.info("");
+ for (i, msg) in same_name_problems.iter().enumerate() {
+ console.info(&format!(" Problem {}", i + 1));
+ console.info(&format!(" {msg}"));
+ }
+ return Err(mozart_core::exit_code::bail_silent(
+ mozart_core::exit_code::DEPENDENCY_RESOLUTION_FAILED,
+ ));
+ }
}
// Step 6: Determine if prefer-source is enabled
diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs
index 4c877cc..cb7f430 100644
--- a/crates/mozart/tests/installer.rs
+++ b/crates/mozart/tests/installer.rs
@@ -332,10 +332,7 @@ installer_fixture!(replace_priorities);
installer_fixture!(replace_range_require_single_version);
installer_fixture!(replace_root_require);
installer_fixture!(replaced_packages_should_not_be_installed);
-installer_fixture!(
- replaced_packages_should_not_be_installed_when_installing_from_lock,
- ignore
-);
+installer_fixture!(replaced_packages_should_not_be_installed_when_installing_from_lock);
installer_fixture!(replacer_satisfies_its_own_requirement);
installer_fixture!(repositories_priorities);
installer_fixture!(repositories_priorities2);