aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-registry
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-03 12:25:45 +0900
committernsfisis <nsfisis@gmail.com>2026-05-03 12:25:45 +0900
commitab2772b8c85139df7d5e625ac5262d385e5ab4c0 (patch)
treeca2bbabecdb6068cbf646153d959c72edddec933 /crates/mozart-registry
parent7badb54195131da9c3561c351138c0ba083e38e4 (diff)
downloadphp-mozart-ab2772b8c85139df7d5e625ac5262d385e5ab4c0.tar.gz
php-mozart-ab2772b8c85139df7d5e625ac5262d385e5ab4c0.tar.zst
php-mozart-ab2772b8c85139df7d5e625ac5262d385e5ab4c0.zip
fix(resolver): seed root package into pool as fixed entry
Composer's RootPackageRepository puts a clone of the root package into the pool as a fixed entry — its `require` / `require-dev` cleared, but its name, version, provides, and replaces preserved. That way a transitive `require` pointing back at the root resolves through the pool the same way any other reference would, and legal circular dependencies (root requires A, A requires root) work. Mozart had no such seed: the rule generator only knew about the root through the explicit root-require / root-provide / root-replace tables, so a transitive consumer requiring the root by name failed with no provider. Plumb root_version through ResolveRequest (RawPackageData gains a matching `Option<String>` field), build a fixed PoolPackageInput for the root with provides/replaces lifted from request.root_provide / root_replace, and skip the root by name when collecting the resolver's output so it doesn't leak into the lock file. Falls back to `1.0.0+no-version-set` (Composer's RootPackage::DEFAULT_PRETTY_VERSION) when the root composer.json omits `version`. Unblocks circular_dependency2, conflict_against_replaced_package_problem, and provider_conflicts installer fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-registry')
-rw-r--r--crates/mozart-registry/src/lockfile.rs1
-rw-r--r--crates/mozart-registry/src/resolver.rs63
2 files changed, 63 insertions, 1 deletions
diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs
index de2c030..447e2cf 100644
--- a/crates/mozart-registry/src/lockfile.rs
+++ b/crates/mozart-registry/src/lockfile.rs
@@ -1386,6 +1386,7 @@ mod tests {
// Resolve monolog/monolog ^3.0
let resolve_request = ResolveRequest {
root_name: String::new(),
+ root_version: None,
require: vec![("monolog/monolog".to_string(), "^3.0".to_string())],
require_dev: vec![],
include_dev: false,
diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs
index 89d3a68..adc8780 100644
--- a/crates/mozart-registry/src/resolver.rs
+++ b/crates/mozart-registry/src/resolver.rs
@@ -13,7 +13,8 @@ use crate::repository::{PackageQuery, RepositorySet};
use crate::vcs_bridge;
use mozart_core::package::{RawRepository, Stability};
use mozart_sat_resolver::{
- DefaultPolicy, PoolBuilder, PoolPackageInput, RuleSetGenerator, Solver, make_pool_links,
+ DefaultPolicy, PoolBuilder, PoolLink, PoolPackageInput, RuleSetGenerator, Solver,
+ make_pool_links,
};
use mozart_semver::Version;
@@ -561,6 +562,12 @@ pub struct ResolveRequest {
/// Root package name from composer.json "name" field (e.g. "laravel/laravel").
/// Used in error messages. Falls back to `__root__` if empty.
pub root_name: String,
+ /// Root package version from composer.json "version" field. `None` falls
+ /// back to Composer's `RootPackage::DEFAULT_PRETTY_VERSION` (1.0.0+no-version-set).
+ /// Used to seed a fixed pool entry for the root so transitive requires
+ /// pointing at the root (legal circular dependencies via an intermediate
+ /// package) can be satisfied.
+ pub root_version: Option<String>,
/// Dependencies from composer.json "require" section.
pub require: Vec<(String, String)>,
/// Dependencies from composer.json "require-dev" section.
@@ -732,6 +739,50 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
builder.add_package(input);
}
+ // Mirror Composer's `RootPackageRepository`: put the root package itself
+ // in the pool as a fixed entry so transitive requires pointing at the
+ // root (legal circular dependencies via an intermediate package) can
+ // resolve. Composer clears the root's `require` / `require-dev` on this
+ // copy because the root requires are already plumbed through the
+ // rule generator's root-require path; carrying them here too would
+ // emit duplicate rules. Provide / replace links survive, so virtual
+ // packages declared on the root keep working for transitive consumers.
+ let root_name_lower = request.root_name.to_lowercase();
+ if !root_name_lower.is_empty() {
+ let (root_pretty, root_normalized) = match request.root_version.as_deref() {
+ Some(v) if !v.is_empty() => (v.to_string(), v.to_string()),
+ _ => ("1.0.0+no-version-set".to_string(), "1.0.0.0".to_string()),
+ };
+ let root_input = PoolPackageInput {
+ name: root_name_lower.clone(),
+ version: root_normalized,
+ pretty_version: root_pretty,
+ requires: vec![],
+ replaces: request
+ .root_replace
+ .iter()
+ .map(|(target, constraint)| PoolLink {
+ target: target.to_lowercase(),
+ constraint: constraint.clone(),
+ source: root_name_lower.clone(),
+ })
+ .collect(),
+ provides: request
+ .root_provide
+ .iter()
+ .map(|(target, constraint)| PoolLink {
+ target: target.to_lowercase(),
+ constraint: constraint.clone(),
+ source: root_name_lower.clone(),
+ })
+ .collect(),
+ conflicts: vec![],
+ is_fixed: true,
+ is_alias_of: None,
+ };
+ builder.add_package(root_input);
+ }
+
// Scan VCS repositories and collect packages from them
let vcs_packages = vcs_bridge::scan_vcs_repositories(&request.raw_repositories).await;
let mut vcs_package_names: IndexSet<String> = IndexSet::new();
@@ -931,6 +982,15 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
continue;
}
+ // Skip the root package itself. It's in the pool as a fixed
+ // entry only so transitive requires pointing back at it
+ // can resolve; it must not appear in the lock file or
+ // operations list. Mirrors Composer's `LockTransaction`
+ // which discards fixed packages from the result.
+ if !root_name_lower.is_empty() && pkg.name == root_name_lower {
+ continue;
+ }
+
let is_dev = if let Ok(v) = Version::parse(&pkg.version) {
version_stability(&v) == Stability::Dev
} else {
@@ -1331,6 +1391,7 @@ mod tests {
use crate::cache::Cache;
let request = ResolveRequest {
root_name: String::new(),
+ root_version: None,
require: vec![("monolog/monolog".to_string(), "^3.0".to_string())],
require_dev: vec![],
include_dev: false,