aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--crates/mozart-core/src/package.rs8
-rw-r--r--crates/mozart-registry/src/lockfile.rs1
-rw-r--r--crates/mozart-registry/src/resolver.rs63
-rw-r--r--crates/mozart/src/commands/create_project.rs1
-rw-r--r--crates/mozart/src/commands/remove.rs4
-rw-r--r--crates/mozart/src/commands/require.rs3
-rw-r--r--crates/mozart/src/commands/update.rs2
-rw-r--r--crates/mozart/tests/installer.rs6
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);