diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-03 12:25:45 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-03 12:25:45 +0900 |
| commit | ab2772b8c85139df7d5e625ac5262d385e5ab4c0 (patch) | |
| tree | ca2bbabecdb6068cbf646153d959c72edddec933 | |
| parent | 7badb54195131da9c3561c351138c0ba083e38e4 (diff) | |
| download | php-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>
| -rw-r--r-- | crates/mozart-core/src/package.rs | 8 | ||||
| -rw-r--r-- | crates/mozart-registry/src/lockfile.rs | 1 | ||||
| -rw-r--r-- | crates/mozart-registry/src/resolver.rs | 63 | ||||
| -rw-r--r-- | crates/mozart/src/commands/create_project.rs | 1 | ||||
| -rw-r--r-- | crates/mozart/src/commands/remove.rs | 4 | ||||
| -rw-r--r-- | crates/mozart/src/commands/require.rs | 3 | ||||
| -rw-r--r-- | crates/mozart/src/commands/update.rs | 2 | ||||
| -rw-r--r-- | crates/mozart/tests/installer.rs | 6 |
8 files changed, 84 insertions, 4 deletions
diff --git a/crates/mozart-core/src/package.rs b/crates/mozart-core/src/package.rs index 02ec935..8bda13d 100644 --- a/crates/mozart-core/src/package.rs +++ b/crates/mozart-core/src/package.rs @@ -461,6 +461,13 @@ pub struct RawPackageData { #[serde(default = "default_root_package_name")] pub name: String, + /// Root project's version, when explicitly set. Composer falls back to + /// `1.0.0+no-version-set` when this is missing; we keep the raw `Option` + /// here and let the resolver apply that default so the in-memory shape + /// stays close to the JSON input. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub version: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option<String>, @@ -554,6 +561,7 @@ impl RawPackageData { pub fn new(name: String) -> Self { Self { name, + version: None, description: None, package_type: None, homepage: None, 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, diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs index 139550a..eceafd0 100644 --- a/crates/mozart/src/commands/create_project.rs +++ b/crates/mozart/src/commands/create_project.rs @@ -409,6 +409,7 @@ pub async fn execute( let request = ResolveRequest { root_name: raw.name.clone(), + root_version: raw.version.clone(), require, require_dev, include_dev: dev_mode, diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs index df8bf2b..f11e9c3 100644 --- a/crates/mozart/src/commands/remove.rs +++ b/crates/mozart/src/commands/remove.rs @@ -243,6 +243,7 @@ pub async fn execute( let request = ResolveRequest { root_name: raw.name.clone(), + root_version: raw.version.clone(), require, require_dev, include_dev: dev_mode, @@ -513,6 +514,7 @@ async fn remove_unused( let request = ResolveRequest { root_name: raw.name.clone(), + root_version: raw.version.clone(), require, require_dev, include_dev: dev_mode, @@ -866,6 +868,7 @@ mod tests { // Simulate initial install let request = ResolveRequest { root_name: String::new(), + root_version: None, require: vec![("psr/log".to_string(), "^3.0".to_string())], require_dev: vec![], include_dev: false, @@ -919,6 +922,7 @@ mod tests { // Re-resolve with empty require let request2 = ResolveRequest { root_name: String::new(), + root_version: None, require: vec![], require_dev: vec![], include_dev: false, diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs index 69d7ea2..cac0dad 100644 --- a/crates/mozart/src/commands/require.rs +++ b/crates/mozart/src/commands/require.rs @@ -631,6 +631,7 @@ pub async fn execute( let request = ResolveRequest { root_name: raw.name.clone(), + root_version: raw.version.clone(), require, require_dev, include_dev: dev_mode, @@ -1031,6 +1032,7 @@ mod tests { let request = ResolveRequest { root_name: String::new(), + root_version: None, require: vec![("psr/log".to_string(), "^3.0".to_string())], require_dev: vec![], include_dev: false, @@ -1101,6 +1103,7 @@ mod tests { let request = ResolveRequest { root_name: String::new(), + root_version: None, require: vec![("psr/log".to_string(), "^3.0".to_string())], require_dev: vec![], include_dev: false, diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index 0c25a9e..db9d616 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -883,6 +883,7 @@ pub async fn run( let request = ResolveRequest { root_name: composer_json.name.clone(), + root_version: composer_json.version.clone(), require, require_dev, include_dev: dev_mode, @@ -1993,6 +1994,7 @@ mod tests { 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, diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index c680764..07e69d2 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -228,12 +228,12 @@ installer_fixture!(aliased_priority_conflicting, ignore); installer_fixture!(aliases_with_require_dev, ignore); installer_fixture!(broken_deps_do_not_replace, ignore); installer_fixture!(circular_dependency, ignore); -installer_fixture!(circular_dependency2, ignore); +installer_fixture!(circular_dependency2); installer_fixture!(circular_dependency_errors); installer_fixture!(conflict_against_provided_by_dep_package_works); installer_fixture!(conflict_against_provided_package_works); installer_fixture!(conflict_against_replaced_by_dep_package_problem); -installer_fixture!(conflict_against_replaced_package_problem, ignore); +installer_fixture!(conflict_against_replaced_package_problem); installer_fixture!(conflict_between_dependents); installer_fixture!(conflict_between_root_and_dependent); installer_fixture!(conflict_downgrade); @@ -311,7 +311,7 @@ installer_fixture!(plugins_are_installed_first); installer_fixture!(prefer_lowest_branches); installer_fixture!(problems_reduce_versions); installer_fixture!(provider_can_coexist_with_other_version_of_provided); -installer_fixture!(provider_conflicts, ignore); +installer_fixture!(provider_conflicts); installer_fixture!(provider_conflicts2); installer_fixture!(provider_conflicts3); installer_fixture!(provider_dev_require_can_satisfy_require, ignore); |
