aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-03 12:36:34 +0900
committernsfisis <nsfisis@gmail.com>2026-05-03 12:36:34 +0900
commitc53dfc52f6449c8d5ca0b160a2a25f99790711f2 (patch)
tree0d40dbf3a337f42982d3ee65f54a1c9824ee2b5f
parent2af684c6397001944c9d9aac20ca59677d6a9650 (diff)
downloadphp-mozart-c53dfc52f6449c8d5ca0b160a2a25f99790711f2.tar.gz
php-mozart-c53dfc52f6449c8d5ca0b160a2a25f99790711f2.tar.zst
php-mozart-c53dfc52f6449c8d5ca0b160a2a25f99790711f2.zip
fix(resolver): carry root composer.json conflicts onto the in-pool root entry
The root pool entry now seeded from composer.json carried provides and replaces but no conflicts, so a root-level conflict like \`{"some/dep": ">=1.3"}\` was silently dropped. Composer keeps these on the RootPackage (which lives in the pool via RootPackageRepository), and the SAT generator turns them into rules that forbid any candidate matching the constraint — including a branch alias that would resolve to a matching version. Without that, Mozart cheerfully installs both the required dev branch and its conflicting alias. Plumb composer.json's \`conflict\` map through ResolveRequest as root_conflict and project it onto the root pool entry as PoolLink conflicts; all callers updated. Unblocks conflict_on_root_with_alias_prevents_update_if_not_required and conflict_with_alias_prevents_update installer fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
-rw-r--r--crates/mozart-registry/src/lockfile.rs1
-rw-r--r--crates/mozart-registry/src/resolver.rs18
-rw-r--r--crates/mozart/src/commands/create_project.rs5
-rw-r--r--crates/mozart/src/commands/remove.rs12
-rw-r--r--crates/mozart/src/commands/require.rs7
-rw-r--r--crates/mozart/src/commands/update.rs6
-rw-r--r--crates/mozart/tests/installer.rs7
7 files changed, 50 insertions, 6 deletions
diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs
index 447e2cf..77a6b4c 100644
--- a/crates/mozart-registry/src/lockfile.rs
+++ b/crates/mozart-registry/src/lockfile.rs
@@ -1405,6 +1405,7 @@ mod tests {
raw_repositories: vec![],
root_provide: IndexMap::new(),
root_replace: IndexMap::new(),
+ root_conflict: IndexMap::new(),
};
let resolved = resolve(&resolve_request)
diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs
index adc8780..48db7c3 100644
--- a/crates/mozart-registry/src/resolver.rs
+++ b/crates/mozart-registry/src/resolver.rs
@@ -611,6 +611,13 @@ pub struct ResolveRequest {
/// 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: IndexMap<String, String>,
+ /// Root composer.json's `conflict` map (target → constraint). Composer's
+ /// `RootPackageRepository` carries these onto the in-pool root package
+ /// entry; the SAT generator then forbids any candidate matching the
+ /// constraint, so a root `conflict` blocks both direct selection of the
+ /// targeted version and any alias / replace / provide that would resolve
+ /// to it.
+ pub root_conflict: IndexMap<String, String>,
}
/// A single package in the resolution output.
@@ -776,7 +783,15 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
source: root_name_lower.clone(),
})
.collect(),
- conflicts: vec![],
+ conflicts: request
+ .root_conflict
+ .iter()
+ .map(|(target, constraint)| PoolLink {
+ target: target.to_lowercase(),
+ constraint: constraint.clone(),
+ source: root_name_lower.clone(),
+ })
+ .collect(),
is_fixed: true,
is_alias_of: None,
};
@@ -1410,6 +1425,7 @@ mod tests {
raw_repositories: vec![],
root_provide: IndexMap::new(),
root_replace: IndexMap::new(),
+ root_conflict: IndexMap::new(),
};
let result = resolve(&request).await;
diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs
index eceafd0..c0faa76 100644
--- a/crates/mozart/src/commands/create_project.rs
+++ b/crates/mozart/src/commands/create_project.rs
@@ -435,6 +435,11 @@ pub async fn execute(
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
+ root_conflict: raw
+ .conflict
+ .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 f11e9c3..08f7cc6 100644
--- a/crates/mozart/src/commands/remove.rs
+++ b/crates/mozart/src/commands/remove.rs
@@ -269,6 +269,11 @@ pub async fn execute(
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
+ root_conflict: raw
+ .conflict
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect(),
};
// Print header messages
@@ -540,6 +545,11 @@ async fn remove_unused(
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
+ root_conflict: raw
+ .conflict
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect(),
};
console.info("Resolving dependencies to detect unused packages...");
@@ -891,6 +901,7 @@ mod tests {
raw_repositories: vec![],
root_provide: IndexMap::new(),
root_replace: IndexMap::new(),
+ root_conflict: IndexMap::new(),
};
let resolved = resolve(&request)
.await
@@ -945,6 +956,7 @@ mod tests {
raw_repositories: vec![],
root_provide: IndexMap::new(),
root_replace: IndexMap::new(),
+ root_conflict: IndexMap::new(),
};
let resolved2 = resolve(&request2)
.await
diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs
index cac0dad..97d6b02 100644
--- a/crates/mozart/src/commands/require.rs
+++ b/crates/mozart/src/commands/require.rs
@@ -657,6 +657,11 @@ pub async fn execute(
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
+ root_conflict: raw
+ .conflict
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect(),
};
// Print header messages
@@ -1055,6 +1060,7 @@ mod tests {
raw_repositories: vec![],
root_provide: IndexMap::new(),
root_replace: IndexMap::new(),
+ root_conflict: IndexMap::new(),
};
let resolved = resolver::resolve(&request)
@@ -1126,6 +1132,7 @@ mod tests {
raw_repositories: vec![],
root_provide: IndexMap::new(),
root_replace: IndexMap::new(),
+ root_conflict: IndexMap::new(),
};
let resolved = resolver::resolve(&request)
diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs
index db9d616..130d7e3 100644
--- a/crates/mozart/src/commands/update.rs
+++ b/crates/mozart/src/commands/update.rs
@@ -907,6 +907,11 @@ pub async fn run(
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
+ root_conflict: composer_json
+ .conflict
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect(),
};
// Step 6: Print header and run resolver
@@ -2017,6 +2022,7 @@ mod tests {
raw_repositories: vec![],
root_provide: IndexMap::new(),
root_replace: IndexMap::new(),
+ root_conflict: IndexMap::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 0cf7e84..6ab083d 100644
--- a/crates/mozart/tests/installer.rs
+++ b/crates/mozart/tests/installer.rs
@@ -238,12 +238,9 @@ installer_fixture!(conflict_between_dependents);
installer_fixture!(conflict_between_root_and_dependent);
installer_fixture!(conflict_downgrade);
installer_fixture!(conflict_downgrade_nested);
-installer_fixture!(
- conflict_on_root_with_alias_prevents_update_if_not_required,
- ignore
-);
+installer_fixture!(conflict_on_root_with_alias_prevents_update_if_not_required);
installer_fixture!(conflict_with_alias_in_lock_does_prevents_install, ignore);
-installer_fixture!(conflict_with_alias_prevents_update, ignore);
+installer_fixture!(conflict_with_alias_prevents_update);
installer_fixture!(conflict_with_alias_prevents_update_if_not_required);
installer_fixture!(conflict_with_all_dependencies_option_dont_recommend_to_use_it);
installer_fixture!(deduplicate_solver_problems);