aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-registry/src/resolver.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-22 18:25:16 +0900
committernsfisis <nsfisis@gmail.com>2026-02-22 18:28:04 +0900
commit84d137a19feb1f79f5bd711faff63a6bbe651cbf (patch)
tree858dee19c5933ecda3f368cb586cf140b4e4c4d2 /crates/mozart-registry/src/resolver.rs
parent07733b3b328f6e4ec23754fcb3504ddb196d65a3 (diff)
downloadphp-mozart-84d137a19feb1f79f5bd711faff63a6bbe651cbf.tar.gz
php-mozart-84d137a19feb1f79f5bd711faff63a6bbe651cbf.tar.zst
php-mozart-84d137a19feb1f79f5bd711faff63a6bbe651cbf.zip
feat(resolver): replace pubgrub with Composer-ported SAT solver
Add mozart-sat-resolver crate implementing a CDCL SAT-based dependency resolver ported from Composer's DependencyResolver. This replaces the pubgrub library to ensure identical resolution behavior with Composer. The new crate includes: pool (package storage with integer IDs), rule/rule_set/rule_set_generator (constraint encoding), decisions (assignment tracking), rule_watch_graph (2-watched literal BCP), solver (CDCL loop with conflict analysis and clause learning), policy (version preference), problem (Composer-style error messages), and transaction (install/update/uninstall operation computation). The registry resolver is rewritten to use PoolBuilder → RuleSetGenerator → Solver pipeline instead of pubgrub's DependencyProvider trait. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-registry/src/resolver.rs')
-rw-r--r--crates/mozart-registry/src/resolver.rs1394
1 files changed, 381 insertions, 1013 deletions
diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs
index a683ef7..e3554be 100644
--- a/crates/mozart-registry/src/resolver.rs
+++ b/crates/mozart-registry/src/resolver.rs
@@ -1,22 +1,19 @@
-//! Dependency resolver using the pubgrub v0.3.0 algorithm.
+//! Dependency resolver using the SAT solver.
//!
-//! This module converts Composer-style dependency constraints into pubgrub's `Ranges<Version>`
-//! and implements `DependencyProvider` for Mozart's package resolution.
+//! This module fetches package metadata from Packagist, builds a Pool of all
+//! candidate packages, generates SAT rules, and runs the CDCL solver to find
+//! a compatible set of packages to install.
-use std::cell::RefCell;
-use std::cmp::Reverse;
-use std::collections::{BTreeMap, HashMap};
+use std::collections::{HashMap, HashSet};
use std::fmt;
-use pubgrub::{
- DefaultStringReporter, Dependencies, DependencyConstraints, DependencyProvider,
- PackageResolutionStatistics, PubGrubError, Ranges, Reporter,
-};
-
use crate::cache::Cache;
use crate::packagist;
use mozart_core::package::Stability;
-use mozart_semver::{Constraint, Version, VersionConstraint};
+use mozart_sat_resolver::{
+ DefaultPolicy, PoolBuilder, PoolPackageInput, RuleSetGenerator, Solver, make_pool_links,
+};
+use mozart_semver::Version;
// ─────────────────────────────────────────────────────────────────────────────
// Version helpers
@@ -128,60 +125,6 @@ impl PackageName {
}
// ─────────────────────────────────────────────────────────────────────────────
-// Type alias
-// ─────────────────────────────────────────────────────────────────────────────
-
-/// The version set type used throughout the resolver.
-pub type ComposerVS = Ranges<Version>;
-
-// ─────────────────────────────────────────────────────────────────────────────
-// Constraint-to-Ranges conversion
-// ─────────────────────────────────────────────────────────────────────────────
-
-/// Convert a Composer version constraint string to a pubgrub `Ranges<Version>`.
-///
-/// Supports: exact, >=, >, <=, <, !=, ^, ~, *, wildcards, hyphen ranges, AND, OR.
-pub fn constraint_to_ranges(constraint: &str) -> Result<ComposerVS, String> {
- let vc = VersionConstraint::parse(constraint)
- .map_err(|e| format!("Failed to parse constraint '{}': {}", constraint, e))?;
- version_constraint_to_ranges(&vc)
-}
-
-fn version_constraint_to_ranges(vc: &VersionConstraint) -> Result<ComposerVS, String> {
- match vc {
- VersionConstraint::Single(c) => single_constraint_to_ranges(c),
- VersionConstraint::And(cs) => {
- let mut result = Ranges::full();
- for c in cs {
- result = result.intersection(&version_constraint_to_ranges(c)?);
- }
- Ok(result)
- }
- VersionConstraint::Or(cs) => {
- let mut result = Ranges::empty();
- for c in cs {
- result = result.union(&version_constraint_to_ranges(c)?);
- }
- Ok(result)
- }
- }
-}
-
-fn single_constraint_to_ranges(c: &Constraint) -> Result<ComposerVS, String> {
- match c {
- Constraint::Any => Ok(Ranges::full()),
- Constraint::Exact(v) => Ok(Ranges::singleton(v.clone())),
- Constraint::GreaterThan(v) => Ok(Ranges::strictly_higher_than(v.clone())),
- Constraint::GreaterThanOrEqual(v) => Ok(Ranges::higher_than(v.clone())),
- Constraint::LessThan(v) => Ok(Ranges::strictly_lower_than(v.clone())),
- Constraint::LessThanOrEqual(v) => {
- Ok(Ranges::strictly_higher_than(v.clone()).complement())
- }
- Constraint::NotEqual(v) => Ok(Ranges::singleton(v.clone()).complement()),
- }
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
// Platform configuration
// ─────────────────────────────────────────────────────────────────────────────
@@ -199,9 +142,6 @@ impl Default for PlatformConfig {
impl PlatformConfig {
/// Detect platform packages from the local PHP installation.
- ///
- /// Runs `php -r …` to discover the PHP version, extensions and
- /// capabilities. Returns an empty config when PHP is not found.
pub fn new() -> Self {
let detected = mozart_core::platform::detect_platform();
let mut packages = HashMap::new();
@@ -226,26 +166,6 @@ impl PlatformConfig {
// Error types
// ─────────────────────────────────────────────────────────────────────────────
-/// Error returned by `DependencyProvider` methods (internal to the solver).
-#[derive(Debug)]
-pub enum ResolverError {
- /// Network or API error fetching package metadata.
- PackagistError(String),
- /// Internal error.
- Internal(String),
-}
-
-impl fmt::Display for ResolverError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::PackagistError(msg) => write!(f, "Packagist error: {}", msg),
- Self::Internal(msg) => write!(f, "Internal resolver error: {}", msg),
- }
- }
-}
-
-impl std::error::Error for ResolverError {}
-
/// Error returned by the public `resolve()` function.
#[derive(Debug)]
pub enum ResolveError {
@@ -286,400 +206,118 @@ impl fmt::Display for ResolveError {
impl std::error::Error for ResolveError {}
// ─────────────────────────────────────────────────────────────────────────────
-// Priority type
+// Stability filter
// ─────────────────────────────────────────────────────────────────────────────
-/// Priority for package resolution ordering.
-/// Higher priority = resolved first.
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-pub struct ResolverPriority {
- conflict_count: u32,
- version_count_inverse: Reverse<usize>,
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// Provider internals
-// ─────────────────────────────────────────────────────────────────────────────
-
-/// Cached version data for a single package.
-struct PackageVersions {
- /// All versions that pass the stability filter, sorted by Version.
- versions: BTreeMap<Version, VersionDependencies>,
+/// Check if a version passes the minimum-stability filter for the given package.
+fn passes_stability_filter(
+ package_name: &str,
+ version: &Version,
+ minimum_stability: Stability,
+ stability_flags: &HashMap<String, Stability>,
+) -> bool {
+ let min_stability = stability_flags
+ .get(package_name)
+ .copied()
+ .unwrap_or(minimum_stability);
+ let vs = version_stability(version);
+ vs <= min_stability
}
-/// Dependencies of a specific package version.
-struct VersionDependencies {
- /// Required packages: (package_name, constraint_string)
- require: Vec<(String, String)>,
- /// Replace declarations: (package_name, constraint_string)
- /// Stored for future replace/provide support (Phase 3.8+).
- #[allow(dead_code)]
- replace: Vec<(String, String)>,
- /// Provide declarations: (package_name, constraint_string)
- /// Stored for future replace/provide support (Phase 3.8+).
- #[allow(dead_code)]
- provide: Vec<(String, String)>,
- /// Conflict declarations: (package_name, constraint_string)
- conflict: Vec<(String, String)>,
- /// Original version string (for output).
- version_string: String,
- /// Normalized version string.
- version_normalized: String,
+/// Check whether a platform dependency should be skipped.
+fn should_skip_platform_dep(
+ dep_name: &str,
+ ignore_platform_reqs: bool,
+ ignore_platform_req_list: &[String],
+) -> bool {
+ if !PackageName(dep_name.to_string()).is_platform() {
+ return false;
+ }
+ if ignore_platform_reqs {
+ return true;
+ }
+ ignore_platform_req_list.iter().any(|p| p == dep_name)
}
// ─────────────────────────────────────────────────────────────────────────────
-// MozartProvider
+// Packagist → PoolPackageInput conversion
// ─────────────────────────────────────────────────────────────────────────────
-/// pubgrub `DependencyProvider` that fetches package metadata from Packagist.
-pub struct MozartProvider {
- /// Tokio runtime handle for calling async functions from sync trait methods.
- handle: tokio::runtime::Handle,
-
- /// Cache of fetched package metadata. Populated lazily from Packagist.
- package_cache: RefCell<HashMap<String, PackageVersions>>,
-
- /// Optional on-disk repo cache for Packagist API responses.
- repo_cache: Option<Cache>,
-
- /// Platform packages (php, ext-*, lib-*) with their fixed versions.
- platform_packages: HashMap<String, Version>,
-
- /// Minimum stability threshold. Versions below this are excluded.
+/// Convert a Packagist version entry to PoolPackageInput(s).
+/// May return multiple entries if branch aliases are present.
+fn packagist_to_pool_inputs(
+ package_name: &str,
+ pv: &packagist::PackagistVersion,
minimum_stability: Stability,
+ stability_flags: &HashMap<String, Stability>,
+) -> Vec<PoolPackageInput> {
+ let mut results = Vec::new();
- /// Per-package stability overrides from composer.json.
- stability_flags: HashMap<String, Stability>,
-
- /// Whether prefer-stable is enabled.
- prefer_stable: bool,
-
- /// Whether prefer-lowest is enabled (for testing).
- prefer_lowest: bool,
-
- /// Root package dependencies (require + optionally require-dev).
- root_dependencies: Vec<(PackageName, ComposerVS)>,
-
- /// Root package conflicts.
- root_conflicts: Vec<(PackageName, ComposerVS)>,
-
- /// Ignore all platform requirements.
- ignore_platform_reqs: bool,
-
- /// Specific platform requirements to ignore.
- ignore_platform_req_list: Vec<String>,
-}
-
-impl MozartProvider {
- /// Ensure package metadata is fetched from Packagist and stored in cache.
- fn ensure_fetched(&self, package_name: &str) -> Result<(), ResolverError> {
- // Check if already cached
- {
- let cache = self.package_cache.borrow();
- if cache.contains_key(package_name) {
- return Ok(());
- }
- }
-
- // Fetch from Packagist (with optional on-disk repo cache)
- // Uses block_on because pubgrub's DependencyProvider trait is synchronous.
- let packagist_versions = self
- .handle
- .block_on(packagist::fetch_package_versions(
+ let make_input = |version_str: &str, version_normalized: &str| -> PoolPackageInput {
+ PoolPackageInput {
+ name: package_name.to_string(),
+ version: version_normalized.to_string(),
+ pretty_version: version_str.to_string(),
+ requires: make_pool_links(
package_name,
- self.repo_cache.as_ref(),
- ))
- .map_err(|e| {
- ResolverError::PackagistError(format!("Failed to fetch {}: {}", package_name, e))
- })?;
-
- // Convert and filter
- let mut versions = BTreeMap::new();
- for pv in &packagist_versions {
- // Build the dependency metadata once (used for both the normal entry
- // and any branch-alias synthetic entry).
- let make_deps =
- |version_string: String, version_normalized: String| VersionDependencies {
- require: pv
- .require
- .iter()
- .map(|(k, v)| (k.clone(), v.clone()))
- .collect(),
- replace: pv
- .replace
- .iter()
- .map(|(k, v)| (k.clone(), v.clone()))
- .collect(),
- provide: pv
- .provide
- .iter()
- .map(|(k, v)| (k.clone(), v.clone()))
- .collect(),
- conflict: pv
- .conflict
- .iter()
- .map(|(k, v)| (k.clone(), v.clone()))
- .collect(),
- version_string,
- version_normalized,
- };
-
- match parse_normalized(&pv.version_normalized) {
- Some(v) => {
- // Regular (non-dev) version
- if self.passes_stability_filter(package_name, &v) {
- let deps = make_deps(pv.version.clone(), pv.version_normalized.clone());
- versions.insert(v, deps);
- }
- }
- None => {
- // Dev branch — check for branch aliases
- let aliases = pv.branch_aliases();
- for (branch, alias_target) in &aliases {
- // The key in branch-alias is the full branch name, e.g. "dev-master".
- // Verify it matches this version.
- if branch.to_lowercase() != pv.version.to_lowercase() {
- continue;
- }
- if let Some(alias_v) =
- parse_branch_alias_target(alias_target)
- && self.passes_stability_filter(package_name, &alias_v)
- {
- // Use the alias target as the normalized version string so
- // that constraint matching works correctly.
- let deps = make_deps(pv.version.clone(), alias_target.clone());
- // Only insert if no real release already occupies this slot
- versions.entry(alias_v).or_insert(deps);
- }
- }
- }
- }
- }
-
- let mut cache = self.package_cache.borrow_mut();
- cache.insert(package_name.to_string(), PackageVersions { versions });
-
- Ok(())
- }
-
- /// Check if a version passes the minimum-stability filter for the given package.
- fn passes_stability_filter(&self, package_name: &str, version: &Version) -> bool {
- // Per-package stability override takes precedence
- let min_stability = self
- .stability_flags
- .get(package_name)
- .copied()
- .unwrap_or(self.minimum_stability);
-
- let vs = version_stability(version);
-
- // `Stability` enum: Stable=0, RC=5, Beta=10, Alpha=15, Dev=20
- // Lower enum value = more stable.
- // vs must be <= min_stability (i.e., at least as stable as minimum).
- vs <= min_stability
- }
-
- /// Check whether a platform dependency should be skipped.
- fn should_skip_platform_dep(&self, dep_name: &str) -> bool {
- if !PackageName(dep_name.to_string()).is_platform() {
- return false;
- }
- if self.ignore_platform_reqs {
- return true;
- }
- self.ignore_platform_req_list.iter().any(|p| p == dep_name)
- }
-}
-
-impl DependencyProvider for MozartProvider {
- type P = PackageName;
- type V = Version;
- type VS = ComposerVS;
- type Priority = ResolverPriority;
- type M = String;
- type Err = ResolverError;
-
- fn choose_version(
- &self,
- package: &PackageName,
- range: &ComposerVS,
- ) -> Result<Option<Version>, ResolverError> {
- // Root package: always version 0.0.0.0 (stable)
- if package.is_root() {
- let root_v = Version {
- major: 0, minor: 0, patch: 0, build: 0,
- pre_release: None, is_dev_branch: false, dev_branch_name: None,
- };
- if range.contains(&root_v) {
- return Ok(Some(root_v));
- }
- return Ok(None);
- }
-
- // Platform packages: return the fixed version if it satisfies the range
- if package.is_platform() {
- if let Some(v) = self.platform_packages.get(&package.0)
- && range.contains(v)
- {
- return Ok(Some(v.clone()));
- }
- return Ok(None);
- }
-
- // Regular packages: ensure metadata is fetched
- self.ensure_fetched(&package.0)?;
-
- let cache = self.package_cache.borrow();
- let Some(pkg_versions) = cache.get(&package.0) else {
- return Ok(None);
- };
-
- if self.prefer_lowest {
- // Pick the lowest matching version
- return Ok(pkg_versions
- .versions
- .keys()
- .find(|v| range.contains(*v))
- .cloned());
- }
-
- if self.prefer_stable {
- // First try: highest stable version in range
- if let Some(v) = pkg_versions
- .versions
- .keys()
- .rev()
- .find(|v| version_stability(v) == Stability::Stable && range.contains(*v))
- {
- return Ok(Some(v.clone()));
- }
- }
-
- // Default: pick highest version in range
- Ok(pkg_versions
- .versions
- .keys()
- .rev()
- .find(|v| range.contains(*v))
- .cloned())
- }
-
- fn prioritize(
- &self,
- package: &PackageName,
- range: &ComposerVS,
- package_conflicts_counts: &PackageResolutionStatistics,
- ) -> Self::Priority {
- // Root and platform packages: highest priority (resolved first)
- if package.is_root() || package.is_platform() {
- return ResolverPriority {
- conflict_count: u32::MAX,
- version_count_inverse: Reverse(0),
- };
- }
-
- let cache = self.package_cache.borrow();
- let count = cache
- .get(&package.0)
- .map(|pvs| pvs.versions.keys().filter(|v| range.contains(*v)).count())
- .unwrap_or(0);
-
- ResolverPriority {
- conflict_count: package_conflicts_counts.conflict_count(),
- version_count_inverse: Reverse(count),
+ &pv.require
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect::<Vec<_>>(),
+ ),
+ replaces: make_pool_links(
+ package_name,
+ &pv.replace
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect::<Vec<_>>(),
+ ),
+ provides: make_pool_links(
+ package_name,
+ &pv.provide
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect::<Vec<_>>(),
+ ),
+ conflicts: make_pool_links(
+ package_name,
+ &pv.conflict
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect::<Vec<_>>(),
+ ),
+ is_fixed: false,
}
- }
+ };
- fn get_dependencies(
- &self,
- package: &PackageName,
- version: &Version,
- ) -> Result<Dependencies<PackageName, ComposerVS, String>, ResolverError> {
- // Root package: return the configured root dependencies
- if package.is_root() {
- let mut deps = DependencyConstraints::default();
- for (name, range) in &self.root_dependencies {
- deps.insert(name.clone(), range.clone());
- }
- // Apply root conflicts as complement ranges
- for (name, range) in &self.root_conflicts {
- let anti_range = range.complement();
- deps.entry(name.clone())
- .and_modify(|existing| *existing = existing.intersection(&anti_range))
- .or_insert(anti_range);
+ match parse_normalized(&pv.version_normalized) {
+ Some(v) => {
+ if passes_stability_filter(package_name, &v, minimum_stability, stability_flags) {
+ results.push(make_input(&pv.version, &pv.version_normalized));
}
- return Ok(Dependencies::Available(deps));
}
-
- // Platform packages: no dependencies
- if package.is_platform() {
- return Ok(Dependencies::Available(DependencyConstraints::default()));
- }
-
- // Regular packages: fetch metadata and build dependency map
- self.ensure_fetched(&package.0)?;
-
- let cache = self.package_cache.borrow();
- let Some(pkg_versions) = cache.get(&package.0) else {
- return Ok(Dependencies::Unavailable(format!(
- "package {} has no available versions",
- package
- )));
- };
-
- let Some(version_deps) = pkg_versions.versions.get(version) else {
- return Ok(Dependencies::Unavailable(format!(
- "{} {} is not available",
- package, version
- )));
- };
-
- let mut deps = DependencyConstraints::default();
-
- // Process `require` constraints
- for (dep_name, constraint_str) in &version_deps.require {
- // Skip self-dependencies
- if dep_name == &package.0 {
- continue;
- }
-
- // Skip platform dependencies if configured
- if self.should_skip_platform_dep(dep_name) {
- continue;
- }
-
- let dep_pkg = PackageName(dep_name.clone());
-
- match constraint_to_ranges(constraint_str) {
- Ok(range) => {
- deps.insert(dep_pkg, range);
+ None => {
+ // Dev branch — check for branch aliases
+ let aliases = pv.branch_aliases();
+ for (branch, alias_target) in &aliases {
+ if branch.to_lowercase() != pv.version.to_lowercase() {
+ continue;
}
- Err(e) => {
- // Unparseable constraint: mark this version as unavailable
- return Ok(Dependencies::Unavailable(format!(
- "cannot parse constraint '{}' for dependency {} of {} {}: {}",
- constraint_str, dep_name, package, version, e
- )));
+ if let Some(alias_v) = parse_branch_alias_target(alias_target)
+ && passes_stability_filter(
+ package_name,
+ &alias_v,
+ minimum_stability,
+ stability_flags,
+ )
+ {
+ results.push(make_input(&pv.version, alias_target));
}
}
}
-
- // Process `conflict` declarations as complement ranges
- for (conflict_name, constraint_str) in &version_deps.conflict {
- if self.should_skip_platform_dep(conflict_name) {
- continue;
- }
- let conflict_pkg = PackageName(conflict_name.clone());
- if let Ok(range) = constraint_to_ranges(constraint_str) {
- let anti_range = range.complement();
- deps.entry(conflict_pkg)
- .and_modify(|existing| *existing = existing.intersection(&anti_range))
- .or_insert(anti_range);
- }
- }
-
- Ok(Dependencies::Available(deps))
}
+
+ results
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -735,138 +373,173 @@ pub struct ResolvedPackage {
/// Returns a list of resolved packages (excluding root and platform packages),
/// or a human-readable error.
pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, ResolveError> {
- // 1. Build root dependencies (parsing is CPU-only, no async needed)
- let mut root_deps: Vec<(PackageName, ComposerVS)> = Vec::new();
- let root_conflicts: Vec<(PackageName, ComposerVS)> = Vec::new();
-
- let parse_dep =
- |name: &str, constraint: &str| -> Result<Option<(PackageName, ComposerVS)>, ResolveError> {
- let pkg = PackageName(name.to_string());
-
- // Skip platform deps if ignore_platform_reqs is set
- if pkg.is_platform()
- && (request.ignore_platform_reqs
- || request.ignore_platform_req_list.contains(&name.to_string()))
- {
- return Ok(None);
- }
-
- let range = constraint_to_ranges(constraint).map_err(|e| {
- ResolveError::ConstraintParseError(name.to_string(), constraint.to_string(), e)
- })?;
- Ok(Some((pkg, range)))
- };
+ // 1. Build root requirements
+ let mut root_requires: HashMap<String, Option<String>> = HashMap::new();
for (name, constraint) in &request.require {
- if let Some(dep) = parse_dep(name, constraint)? {
- root_deps.push(dep);
+ if should_skip_platform_dep(
+ name,
+ request.ignore_platform_reqs,
+ &request.ignore_platform_req_list,
+ ) {
+ continue;
}
+ root_requires.insert(name.to_lowercase(), Some(constraint.clone()));
}
if request.include_dev {
for (name, constraint) in &request.require_dev {
- if let Some(dep) = parse_dep(name, constraint)? {
- root_deps.push(dep);
+ if should_skip_platform_dep(
+ name,
+ request.ignore_platform_reqs,
+ &request.ignore_platform_req_list,
+ ) {
+ continue;
}
+ root_requires.insert(name.to_lowercase(), Some(constraint.clone()));
}
}
- // Capture the current tokio Handle so the provider can call async functions
- // from within pubgrub's synchronous DependencyProvider trait methods.
+ // Capture data needed by spawn_blocking
let handle = tokio::runtime::Handle::current();
-
- // Clone data needed by spawn_blocking (which requires 'static)
let repo_cache = request.repo_cache.clone();
- let platform_packages = request.platform.to_versions();
+ let platform_config = request.platform.to_versions();
let minimum_stability = request.minimum_stability;
let stability_flags = request.stability_flags.clone();
let prefer_stable = request.prefer_stable;
let prefer_lowest = request.prefer_lowest;
let ignore_platform_reqs = request.ignore_platform_reqs;
let ignore_platform_req_list = request.ignore_platform_req_list.clone();
- let root_name = request.root_name.clone();
- // 2. Run pubgrub on a blocking thread (it is CPU-bound + uses block_on for I/O)
- tokio::task::spawn_blocking(move || {
- let provider = MozartProvider {
- handle,
- package_cache: RefCell::new(HashMap::new()),
- repo_cache,
- platform_packages,
- minimum_stability,
- stability_flags,
- prefer_stable,
- prefer_lowest,
- root_dependencies: root_deps,
- root_conflicts,
- ignore_platform_reqs,
- ignore_platform_req_list,
- };
+ // 2. Build pool, generate rules, and solve on a blocking thread
+ tokio::task::spawn_blocking(move || -> Result<Vec<ResolvedPackage>, ResolveError> {
+ let mut builder = PoolBuilder::new();
- let root = PackageName::root();
- let root_version = Version {
- major: 0, minor: 0, patch: 0, build: 0,
- pre_release: None, is_dev_branch: false, dev_branch_name: None,
- };
+ // Set up ignore list for platform requirements
+ let mut ignore_set: HashSet<String> = HashSet::new();
+ if ignore_platform_reqs {
+ // We'll skip platform deps in the loop below
+ }
+ for name in &ignore_platform_req_list {
+ ignore_set.insert(name.clone());
+ }
+ builder.set_ignore_platform_reqs(ignore_set.clone());
+
+ // Add platform packages as fixed entries
+ let mut fixed_packages_by_name: HashMap<String, u32> = HashMap::new();
+ for (name, version) in &platform_config {
+ if should_skip_platform_dep(name, ignore_platform_reqs, &ignore_platform_req_list) {
+ continue;
+ }
+ let input = PoolPackageInput {
+ name: name.clone(),
+ version: version.to_string(),
+ pretty_version: version.to_string(),
+ requires: vec![],
+ replaces: vec![],
+ provides: vec![],
+ conflicts: vec![],
+ is_fixed: true,
+ };
+ builder.add_package(input);
+ }
+
+ // Seed the builder with packages for root requirements
+ for name in root_requires.keys() {
+ if PackageName(name.clone()).is_platform() {
+ continue; // platform packages already added
+ }
- match pubgrub::resolve(&provider, root, root_version) {
- Ok(solution) => {
- let mut result = Vec::new();
- for (pkg, version) in solution {
- if pkg.is_root() || pkg.is_platform() {
+ // Fetch available versions from Packagist
+ let versions = handle
+ .block_on(packagist::fetch_package_versions(name, repo_cache.as_ref()))
+ .map_err(|e| {
+ ResolveError::DependencyFetchError(format!("Failed to fetch {}: {}", name, e))
+ })?;
+
+ for pv in &versions {
+ let inputs =
+ packagist_to_pool_inputs(name, pv, minimum_stability, &stability_flags);
+ for input in inputs {
+ builder.add_package(input);
+ }
+ }
+ }
+
+ // Explore transitive dependencies
+ while let Some(name) = builder.next_pending() {
+ if PackageName(name.clone()).is_platform() {
+ // Platform package: already added if available, skip fetching
+ continue;
+ }
+
+ let versions = handle
+ .block_on(packagist::fetch_package_versions(
+ &name,
+ repo_cache.as_ref(),
+ ))
+ .map_err(|e| {
+ ResolveError::DependencyFetchError(format!("Failed to fetch {}: {}", name, e))
+ })?;
+
+ for pv in &versions {
+ let inputs =
+ packagist_to_pool_inputs(&name, pv, minimum_stability, &stability_flags);
+ for input in inputs {
+ builder.add_package(input);
+ }
+ }
+ }
+
+ // Build the pool
+ let mut pool = builder.build();
+
+ // Collect fixed package IDs
+ let mut fixed_ids: Vec<u32> = Vec::new();
+ for pkg in pool.packages() {
+ if pkg.is_fixed {
+ fixed_ids.push(pkg.id);
+ fixed_packages_by_name.insert(pkg.name.clone(), pkg.id);
+ }
+ }
+
+ // Generate rules
+ let mut generator = RuleSetGenerator::new(&mut pool);
+ generator.set_ignore_platform_reqs(ignore_set);
+ let rules = generator.generate(&root_requires, &fixed_ids);
+
+ // Create policy and solve
+ let policy = DefaultPolicy::new(prefer_stable, prefer_lowest);
+ let fixed_set: HashSet<u32> = fixed_ids.into_iter().collect();
+ let solver = Solver::new(rules, &pool, policy, fixed_set);
+
+ match solver.solve() {
+ Ok(result) => {
+ let mut resolved = Vec::new();
+ for pkg_id in result.installed {
+ let pkg = pool.package_by_id(pkg_id);
+
+ // Skip platform packages from output
+ if PackageName(pkg.name.clone()).is_platform() {
continue;
}
- let cache = provider.package_cache.borrow();
- let (version_str, version_normalized) = if let Some(pvs) = cache.get(&pkg.0) {
- if let Some(vd) = pvs.versions.get(&version) {
- (vd.version_string.clone(), vd.version_normalized.clone())
- } else {
- (version.to_string(), version.to_string())
- }
+ let is_dev = if let Ok(v) = Version::parse(&pkg.version) {
+ version_stability(&v) == Stability::Dev
} else {
- (version.to_string(), version.to_string())
+ false
};
- result.push(ResolvedPackage {
- name: pkg.0.clone(),
- version: version_str,
- version_normalized,
- is_dev: version_stability(&version) == Stability::Dev,
+ resolved.push(ResolvedPackage {
+ name: pkg.name.clone(),
+ version: pkg.pretty_version.clone(),
+ version_normalized: pkg.version.clone(),
+ is_dev,
});
}
- Ok(result)
- }
- Err(PubGrubError::NoSolution(mut derivation_tree)) => {
- derivation_tree.collapse_no_versions();
- let report = DefaultStringReporter::report(&derivation_tree);
- // Replace the internal __root__ identifier with the actual
- // package name so that error messages are user-friendly
- // (matching Composer behaviour).
- let report = if !root_name.is_empty() {
- report.replace(PackageName::ROOT, &root_name)
- } else {
- report
- };
- Err(ResolveError::NoSolution(report))
- }
- Err(PubGrubError::ErrorRetrievingDependencies {
- package,
- version,
- source,
- }) => Err(ResolveError::DependencyFetchError(format!(
- "Error retrieving dependencies for {} {}: {}",
- package, version, source
- ))),
- Err(PubGrubError::ErrorChoosingVersion { package, source }) => {
- Err(ResolveError::DependencyFetchError(format!(
- "Error choosing version for {}: {}",
- package, source
- )))
- }
- Err(PubGrubError::ErrorInShouldCancel(e)) => {
- Err(ResolveError::Internal(format!("Resolver cancelled: {}", e)))
+ Ok(resolved)
}
+ Err(e) => Err(ResolveError::NoSolution(e.to_string())),
}
})
.await
@@ -880,38 +553,27 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
#[cfg(test)]
mod tests {
use super::*;
- use pubgrub::{OfflineDependencyProvider, Ranges};
-
- fn test_handle() -> tokio::runtime::Handle {
- static RT: std::sync::OnceLock<tokio::runtime::Runtime> = std::sync::OnceLock::new();
- RT.get_or_init(|| tokio::runtime::Runtime::new().unwrap())
- .handle()
- .clone()
- }
// ──────────── Version parsing helpers ────────────
fn v(major: u64, minor: u64, patch: u64, build: u64) -> Version {
Version {
- major, minor, patch, build,
+ major,
+ minor,
+ patch,
+ build,
pre_release: None,
is_dev_branch: false,
dev_branch_name: None,
}
}
- fn v_dev(major: u64, minor: u64, patch: u64, build: u64) -> Version {
- Version {
- major, minor, patch, build,
- pre_release: Some("dev".to_string()),
- is_dev_branch: false,
- dev_branch_name: None,
- }
- }
-
fn v_pre(major: u64, minor: u64, patch: u64, build: u64, pre: &str) -> Version {
Version {
- major, minor, patch, build,
+ major,
+ minor,
+ patch,
+ build,
pre_release: Some(pre.to_string()),
is_dev_branch: false,
dev_branch_name: None,
@@ -956,7 +618,10 @@ mod tests {
#[test]
fn test_parse_normalized_dev_branch() {
let ver = parse_normalized("dev-master");
- assert!(ver.is_none(), "dev-master should not parse as normalized version");
+ assert!(
+ ver.is_none(),
+ "dev-master should not parse as normalized version"
+ );
}
#[test]
@@ -973,7 +638,6 @@ mod tests {
#[test]
fn test_parse_normalized_large_version() {
- // ext-dom version 20031129 — this was the u16 overflow bug
let ver = parse_normalized("20031129").unwrap();
assert_eq!(ver.major, 20031129);
assert_eq!(ver.pre_release, None);
@@ -1025,125 +689,22 @@ mod tests {
fn test_version_stability_fn() {
assert_eq!(version_stability(&v(1, 0, 0, 0)), Stability::Stable);
assert_eq!(version_stability(&v_pre(1, 0, 0, 0, "RC1")), Stability::RC);
- assert_eq!(version_stability(&v_pre(1, 0, 0, 0, "beta1")), Stability::Beta);
- assert_eq!(version_stability(&v_pre(1, 0, 0, 0, "alpha1")), Stability::Alpha);
+ assert_eq!(
+ version_stability(&v_pre(1, 0, 0, 0, "beta1")),
+ Stability::Beta
+ );
+ assert_eq!(
+ version_stability(&v_pre(1, 0, 0, 0, "alpha1")),
+ Stability::Alpha
+ );
assert_eq!(version_stability(&v_pre(1, 0, 0, 0, "dev")), Stability::Dev);
- // patch is treated as stable
- assert_eq!(version_stability(&v_pre(1, 0, 0, 0, "patch1")), Stability::Stable);
- }
-
- // ──────────── Constraint conversion ────────────
-
- #[test]
- fn test_constraint_any() {
- let range = constraint_to_ranges("*").unwrap();
- assert!(range.contains(&v(1, 2, 3, 0)));
- assert!(range.contains(&v(0, 0, 0, 0)));
- }
-
- #[test]
- fn test_constraint_exact() {
- let range = constraint_to_ranges("1.2.3").unwrap();
- assert!(range.contains(&v(1, 2, 3, 0)));
- assert!(!range.contains(&v(1, 2, 4, 0)));
- assert!(!range.contains(&v(1, 2, 2, 0)));
- }
-
- #[test]
- fn test_constraint_gte() {
- let range = constraint_to_ranges(">=1.0").unwrap();
- assert!(range.contains(&v(1, 0, 0, 0)));
- assert!(range.contains(&v(2, 0, 0, 0)));
- assert!(!range.contains(&v(0, 9, 0, 0)));
- // 1.0.0.0-dev is LESS than 1.0.0.0 (stable), so NOT in >=1.0
- assert!(!range.contains(&v_dev(1, 0, 0, 0)));
- }
-
- #[test]
- fn test_constraint_lt() {
- let range = constraint_to_ranges("<2.0").unwrap();
- assert!(range.contains(&v(1, 9, 9, 0)));
- assert!(range.contains(&v_dev(2, 0, 0, 0))); // 2.0.0.0-dev < 2.0.0.0 (stable)
- assert!(!range.contains(&v(2, 0, 0, 0)));
- }
-
- #[test]
- fn test_constraint_caret() {
- // ^1.2 → >=1.2.0.0-dev <2.0.0.0-dev
- let range = constraint_to_ranges("^1.2").unwrap();
- assert!(range.contains(&v_dev(1, 2, 0, 0)));
- assert!(range.contains(&v(1, 2, 0, 0)));
- assert!(range.contains(&v(1, 9, 9, 0)));
- assert!(!range.contains(&v_dev(2, 0, 0, 0)));
- assert!(!range.contains(&v(2, 0, 0, 0)));
- assert!(!range.contains(&v(1, 1, 9, 0)));
- }
-
- #[test]
- fn test_constraint_caret_zero() {
- // ^0.2.3 → >=0.2.3.0-dev <0.3.0.0-dev
- let range = constraint_to_ranges("^0.2.3").unwrap();
- assert!(range.contains(&v(0, 2, 3, 0)));
- assert!(range.contains(&v(0, 2, 9, 0)));
- assert!(!range.contains(&v_dev(0, 3, 0, 0)));
- assert!(!range.contains(&v(1, 0, 0, 0)));
- }
-
- #[test]
- fn test_constraint_tilde() {
- // ~1.2.3 → >=1.2.3.0-dev <1.3.0.0-dev
- let range = constraint_to_ranges("~1.2.3").unwrap();
- assert!(range.contains(&v(1, 2, 3, 0)));
- assert!(range.contains(&v(1, 2, 9, 0)));
- assert!(!range.contains(&v_dev(1, 3, 0, 0)));
- }
-
- #[test]
- fn test_constraint_wildcard() {
- // 1.2.* → >=1.2.0.0-dev <1.3.0.0-dev
- let range = constraint_to_ranges("1.2.*").unwrap();
- assert!(range.contains(&v(1, 2, 0, 0)));
- assert!(range.contains(&v(1, 2, 9, 0)));
- assert!(!range.contains(&v_dev(1, 3, 0, 0)));
- assert!(!range.contains(&v(1, 3, 0, 0)));
- }
-
- #[test]
- fn test_constraint_or() {
- let range = constraint_to_ranges("^1.0 || ^2.0").unwrap();
- assert!(range.contains(&v(1, 5, 0, 0)));
- assert!(range.contains(&v(2, 3, 0, 0)));
- assert!(!range.contains(&v(3, 0, 0, 0)));
- }
-
- #[test]
- fn test_constraint_and() {
- let range = constraint_to_ranges(">=1.0 <2.0").unwrap();
- assert!(!range.contains(&v_dev(1, 0, 0, 0)));
- assert!(range.contains(&v(1, 0, 0, 0)));
- assert!(range.contains(&v(1, 9, 9, 0)));
- assert!(range.contains(&v_dev(2, 0, 0, 0)));
- assert!(!range.contains(&v(2, 0, 0, 0)));
- }
-
- #[test]
- fn test_constraint_not_equal() {
- let range = constraint_to_ranges("!=1.5.0").unwrap();
- assert!(range.contains(&v(1, 4, 0, 0)));
- assert!(!range.contains(&v(1, 5, 0, 0)));
- assert!(range.contains(&v(1, 6, 0, 0)));
- }
-
- #[test]
- fn test_constraint_hyphen() {
- let range = constraint_to_ranges("1.0 - 2.0").unwrap();
- assert!(range.contains(&v(1, 0, 0, 0)));
- assert!(range.contains(&v(1, 5, 0, 0)));
- assert!(range.contains(&v(2, 0, 0, 0)));
- assert!(!range.contains(&v(2, 1, 0, 0)));
+ assert_eq!(
+ version_stability(&v_pre(1, 0, 0, 0, "patch1")),
+ Stability::Stable
+ );
}
- // ──────────── Provider tests (offline) ────────────
+ // ──────────── PackageName ────────────
#[test]
fn test_package_name_is_platform() {
@@ -1160,330 +721,111 @@ mod tests {
assert!(!PackageName("monolog/monolog".to_string()).is_root());
}
- #[test]
- fn test_platform_config_to_versions() {
- let config = PlatformConfig::new();
- let versions = config.to_versions();
- if !config.packages.is_empty() {
- assert!(
- versions.contains_key("php"),
- "detected packages should include php"
- );
- }
- }
-
- // ──────────── Integration tests (offline, using OfflineDependencyProvider) ────────────
-
- type TestVS = Ranges<Version>;
-
- /// Test simple resolution: root → foo ^1.0, foo 1.0 → bar ^2.0, bar 2.0 → (nothing)
- #[test]
- fn test_resolve_simple_offline() {
- let mut provider = OfflineDependencyProvider::<PackageName, TestVS>::new();
-
- let root = PackageName::root();
- let root_v = v(0, 0, 0, 0);
- let foo = PackageName("foo/foo".to_string());
- let bar = PackageName("bar/bar".to_string());
-
- let foo_1_0 = v(1, 0, 0, 0);
- let bar_2_0 = v(2, 0, 0, 0);
-
- let foo_range = constraint_to_ranges("^1.0").unwrap();
- provider.add_dependencies(root.clone(), root_v.clone(), [(foo.clone(), foo_range)]);
-
- let bar_range = constraint_to_ranges("^2.0").unwrap();
- provider.add_dependencies(foo.clone(), foo_1_0.clone(), [(bar.clone(), bar_range)]);
-
- provider.add_dependencies(bar.clone(), bar_2_0.clone(), []);
-
- let solution = pubgrub::resolve(&provider, root.clone(), root_v).unwrap();
-
- assert_eq!(*solution.get(&foo).unwrap(), foo_1_0);
- assert_eq!(*solution.get(&bar).unwrap(), bar_2_0);
- }
-
- /// Test conflict detection: two packages require incompatible versions of a third.
- #[test]
- fn test_resolve_no_solution_offline() {
- let mut provider = OfflineDependencyProvider::<PackageName, TestVS>::new();
-
- let root = PackageName::root();
- let root_v = v(0, 0, 0, 0);
- let foo = PackageName("foo/foo".to_string());
- let bar = PackageName("bar/bar".to_string());
- let dep = PackageName("dep/dep".to_string());
-
- let foo_1_0 = v(1, 0, 0, 0);
- let bar_1_0 = v(1, 0, 0, 0);
- let dep_1_0 = v(1, 0, 0, 0);
- let dep_2_0 = v(2, 0, 0, 0);
-
- let foo_range = Ranges::singleton(foo_1_0.clone());
- let bar_range = Ranges::singleton(bar_1_0.clone());
- provider.add_dependencies(
- root.clone(),
- root_v.clone(),
- [(foo.clone(), foo_range), (bar.clone(), bar_range)],
- );
-
- let dep_range_1 = constraint_to_ranges("^1.0").unwrap();
- provider.add_dependencies(foo.clone(), foo_1_0, [(dep.clone(), dep_range_1)]);
-
- let dep_range_2 = constraint_to_ranges("^2.0").unwrap();
- provider.add_dependencies(bar.clone(), bar_1_0, [(dep.clone(), dep_range_2)]);
-
- provider.add_dependencies(dep.clone(), dep_1_0, []);
- provider.add_dependencies(dep.clone(), dep_2_0, []);
-
- let result = pubgrub::resolve(&provider, root.clone(), root_v);
- assert!(result.is_err(), "Expected no solution for conflicting deps");
- }
-
- /// Test prefer-stable: stable version should have Stability::Stable.
- #[test]
- fn test_prefer_stable() {
- let stable = v(1, 0, 0, 0);
- let beta = v_pre(1, 1, 0, 0, "beta1");
-
- assert_eq!(version_stability(&stable), Stability::Stable);
- assert_eq!(version_stability(&beta), Stability::Beta);
- }
+ // ──────────── Stability filter ────────────
- /// Test stability filter: alpha versions should be excluded when minimum_stability = stable.
#[test]
fn test_stability_filter() {
- let provider = MozartProvider {
- handle: test_handle(),
- package_cache: RefCell::new(HashMap::new()),
- platform_packages: HashMap::new(),
- minimum_stability: Stability::Stable,
- stability_flags: HashMap::new(),
- prefer_stable: false,
- prefer_lowest: false,
- root_dependencies: vec![],
- root_conflicts: vec![],
- ignore_platform_reqs: false,
- ignore_platform_req_list: vec![],
- repo_cache: None,
- };
-
let stable_v = v(1, 0, 0, 0);
let alpha_v = v_pre(1, 1, 0, 0, "alpha1");
let beta_v = v_pre(1, 0, 0, 0, "beta1");
let rc_v = v_pre(1, 0, 0, 0, "RC1");
let dev_v = v_pre(1, 0, 0, 0, "dev");
- assert!(provider.passes_stability_filter("foo/foo", &stable_v));
- assert!(!provider.passes_stability_filter("foo/foo", &alpha_v));
- assert!(!provider.passes_stability_filter("foo/foo", &beta_v));
- assert!(!provider.passes_stability_filter("foo/foo", &rc_v));
- assert!(!provider.passes_stability_filter("foo/foo", &dev_v));
+ let flags = HashMap::new();
+
+ assert!(passes_stability_filter(
+ "foo/foo",
+ &stable_v,
+ Stability::Stable,
+ &flags
+ ));
+ assert!(!passes_stability_filter(
+ "foo/foo",
+ &alpha_v,
+ Stability::Stable,
+ &flags
+ ));
+ assert!(!passes_stability_filter(
+ "foo/foo",
+ &beta_v,
+ Stability::Stable,
+ &flags
+ ));
+ assert!(!passes_stability_filter(
+ "foo/foo",
+ &rc_v,
+ Stability::Stable,
+ &flags
+ ));
+ assert!(!passes_stability_filter(
+ "foo/foo",
+ &dev_v,
+ Stability::Stable,
+ &flags
+ ));
}
#[test]
fn test_stability_filter_beta() {
- let provider = MozartProvider {
- handle: test_handle(),
- package_cache: RefCell::new(HashMap::new()),
- platform_packages: HashMap::new(),
- minimum_stability: Stability::Beta,
- stability_flags: HashMap::new(),
- prefer_stable: false,
- prefer_lowest: false,
- root_dependencies: vec![],
- root_conflicts: vec![],
- ignore_platform_reqs: false,
- ignore_platform_req_list: vec![],
- repo_cache: None,
- };
-
let stable_v = v(1, 0, 0, 0);
let beta_v = v_pre(1, 0, 0, 0, "beta1");
let alpha_v = v_pre(1, 0, 0, 0, "alpha1");
let dev_v = v_pre(1, 0, 0, 0, "dev");
- assert!(provider.passes_stability_filter("foo/foo", &stable_v));
- assert!(provider.passes_stability_filter("foo/foo", &beta_v));
- assert!(!provider.passes_stability_filter("foo/foo", &alpha_v));
- assert!(!provider.passes_stability_filter("foo/foo", &dev_v));
+ let flags = HashMap::new();
+
+ assert!(passes_stability_filter(
+ "foo/foo",
+ &stable_v,
+ Stability::Beta,
+ &flags
+ ));
+ assert!(passes_stability_filter(
+ "foo/foo",
+ &beta_v,
+ Stability::Beta,
+ &flags
+ ));
+ assert!(!passes_stability_filter(
+ "foo/foo",
+ &alpha_v,
+ Stability::Beta,
+ &flags
+ ));
+ assert!(!passes_stability_filter(
+ "foo/foo",
+ &dev_v,
+ Stability::Beta,
+ &flags
+ ));
}
#[test]
fn test_stability_filter_dev() {
- let provider = MozartProvider {
- handle: test_handle(),
- package_cache: RefCell::new(HashMap::new()),
- platform_packages: HashMap::new(),
- minimum_stability: Stability::Dev,
- stability_flags: HashMap::new(),
- prefer_stable: false,
- prefer_lowest: false,
- root_dependencies: vec![],
- root_conflicts: vec![],
- ignore_platform_reqs: false,
- ignore_platform_req_list: vec![],
- repo_cache: None,
- };
-
let dev_v = v_pre(1, 0, 0, 0, "dev");
- assert!(provider.passes_stability_filter("foo/foo", &dev_v));
+ let flags = HashMap::new();
+ assert!(passes_stability_filter(
+ "foo/foo",
+ &dev_v,
+ Stability::Dev,
+ &flags
+ ));
}
#[test]
fn test_skip_platform_dep() {
- let provider = MozartProvider {
- handle: test_handle(),
- package_cache: RefCell::new(HashMap::new()),
- platform_packages: HashMap::new(),
- minimum_stability: Stability::Stable,
- stability_flags: HashMap::new(),
- prefer_stable: false,
- prefer_lowest: false,
- root_dependencies: vec![],
- root_conflicts: vec![],
- ignore_platform_reqs: true,
- ignore_platform_req_list: vec![],
- repo_cache: None,
- };
-
- assert!(provider.should_skip_platform_dep("php"));
- assert!(provider.should_skip_platform_dep("ext-json"));
- assert!(!provider.should_skip_platform_dep("monolog/monolog"));
+ assert!(should_skip_platform_dep("php", true, &[]));
+ assert!(should_skip_platform_dep("ext-json", true, &[]));
+ assert!(!should_skip_platform_dep("monolog/monolog", true, &[]));
}
#[test]
fn test_skip_specific_platform_dep() {
- let provider = MozartProvider {
- handle: test_handle(),
- package_cache: RefCell::new(HashMap::new()),
- platform_packages: HashMap::new(),
- minimum_stability: Stability::Stable,
- stability_flags: HashMap::new(),
- prefer_stable: false,
- prefer_lowest: false,
- root_dependencies: vec![],
- root_conflicts: vec![],
- ignore_platform_reqs: false,
- ignore_platform_req_list: vec!["ext-intl".to_string()],
- repo_cache: None,
- };
-
- assert!(provider.should_skip_platform_dep("ext-intl"));
- assert!(!provider.should_skip_platform_dep("ext-json"));
- assert!(!provider.should_skip_platform_dep("php"));
- assert!(!provider.should_skip_platform_dep("monolog/monolog"));
- }
-
- #[test]
- fn test_root_package_choose_version() {
- let provider = MozartProvider {
- handle: test_handle(),
- package_cache: RefCell::new(HashMap::new()),
- platform_packages: HashMap::new(),
- minimum_stability: Stability::Stable,
- stability_flags: HashMap::new(),
- prefer_stable: false,
- prefer_lowest: false,
- root_dependencies: vec![],
- root_conflicts: vec![],
- ignore_platform_reqs: false,
- ignore_platform_req_list: vec![],
- repo_cache: None,
- };
-
- let root = PackageName::root();
- let root_v = v(0, 0, 0, 0);
- let full_range: ComposerVS = Ranges::full();
- let result = provider.choose_version(&root, &full_range).unwrap();
- assert_eq!(result, Some(root_v));
- }
-
- #[test]
- fn test_platform_choose_version() {
- let mut platform = HashMap::new();
- let php_v = Version::parse("8.1.0").unwrap();
- platform.insert("php".to_string(), php_v.clone());
-
- let provider = MozartProvider {
- handle: test_handle(),
- package_cache: RefCell::new(HashMap::new()),
- platform_packages: platform,
- minimum_stability: Stability::Stable,
- stability_flags: HashMap::new(),
- prefer_stable: false,
- prefer_lowest: false,
- root_dependencies: vec![],
- root_conflicts: vec![],
- ignore_platform_reqs: false,
- ignore_platform_req_list: vec![],
- repo_cache: None,
- };
-
- let php = PackageName("php".to_string());
- let range = constraint_to_ranges(">=8.0").unwrap();
- let result = provider.choose_version(&php, &range).unwrap();
- assert_eq!(result, Some(php_v));
-
- let too_new_range = constraint_to_ranges(">=9.0").unwrap();
- let result2 = provider.choose_version(&php, &too_new_range).unwrap();
- assert_eq!(result2, None);
- }
-
- #[test]
- fn test_constraint_contains_version() {
- let range = constraint_to_ranges("^3.0").unwrap();
- assert!(range.contains(&v(3, 5, 1, 0)));
- assert!(!range.contains(&v(4, 0, 0, 0)));
- assert!(!range.contains(&v(2, 9, 9, 0)));
- }
-
- // ──────────── Integration test with MozartProvider (no network) ────────────
-
- #[test]
- fn test_resolve_with_offline_provider_simple() {
- let mut provider = OfflineDependencyProvider::<PackageName, TestVS>::new();
-
- let root = PackageName::root();
- let root_v = v(0, 0, 0, 0);
- let foo = PackageName("foo/foo".to_string());
-
- let foo_1_0 = v(1, 0, 0, 0);
- let foo_1_1 = v(1, 1, 0, 0);
-
- let foo_range = constraint_to_ranges("^1.0").unwrap();
- provider.add_dependencies(root.clone(), root_v.clone(), [(foo.clone(), foo_range)]);
- provider.add_dependencies(foo.clone(), foo_1_0, []);
- provider.add_dependencies(foo.clone(), foo_1_1.clone(), []);
-
- let solution = pubgrub::resolve(&provider, root.clone(), root_v).unwrap();
-
- assert_eq!(*solution.get(&foo).unwrap(), foo_1_1);
- }
-
- #[test]
- fn test_resolve_or_constraint() {
- let mut provider = OfflineDependencyProvider::<PackageName, TestVS>::new();
-
- let root = PackageName::root();
- let root_v = v(0, 0, 0, 0);
- let foo = PackageName("foo/foo".to_string());
-
- let foo_1_5 = v(1, 5, 0, 0);
- let foo_2_3 = v(2, 3, 0, 0);
-
- let foo_range = constraint_to_ranges("^1.0 || ^2.0").unwrap();
- provider.add_dependencies(root.clone(), root_v.clone(), [(foo.clone(), foo_range)]);
- provider.add_dependencies(foo.clone(), foo_1_5.clone(), []);
- provider.add_dependencies(foo.clone(), foo_2_3.clone(), []);
-
- let solution = pubgrub::resolve(&provider, root.clone(), root_v).unwrap();
-
- let picked = solution.get(&foo).unwrap();
- assert!(
- *picked == foo_1_5 || *picked == foo_2_3,
- "picked version should be one of the available versions"
- );
+ let list = vec!["ext-intl".to_string()];
+ assert!(should_skip_platform_dep("ext-intl", false, &list));
+ assert!(!should_skip_platform_dep("ext-json", false, &list));
+ assert!(!should_skip_platform_dep("php", false, &list));
+ assert!(!should_skip_platform_dep("monolog/monolog", false, &list));
}
// ──────────── Branch alias tests ────────────
@@ -1516,29 +858,55 @@ mod tests {
assert!(parse_branch_alias_target("").is_none());
}
- #[test]
- fn test_branch_alias_in_range() {
- let aliased_v = parse_branch_alias_target("2.x-dev").unwrap();
- let range = constraint_to_ranges("^2.0").unwrap();
- assert!(
- range.contains(&aliased_v),
- "dev-master aliased to 2.x-dev should satisfy ^2.0"
- );
- }
+ // ──────────── SAT solver integration tests (offline) ────────────
#[test]
- fn test_branch_alias_1_x_in_range() {
- let aliased_v = parse_branch_alias_target("1.0.x-dev").unwrap();
- let range = constraint_to_ranges("^1.0").unwrap();
- assert!(
- range.contains(&aliased_v),
- "dev branch aliased to 1.0.x-dev should satisfy ^1.0"
- );
- let range2 = constraint_to_ranges("^2.0").unwrap();
- assert!(
- !range2.contains(&aliased_v),
- "1.0.x-dev alias should not satisfy ^2.0"
+ fn test_sat_resolve_simple_offline() {
+ use mozart_sat_resolver::*;
+
+ let mut pool = Pool::new(
+ vec![
+ PoolPackageInput {
+ name: "foo/foo".to_string(),
+ version: "1.0.0.0".to_string(),
+ pretty_version: "1.0.0".to_string(),
+ requires: vec![PoolLink {
+ target: "bar/bar".to_string(),
+ constraint: "^2.0".to_string(),
+ source: "foo/foo".to_string(),
+ }],
+ replaces: vec![],
+ provides: vec![],
+ conflicts: vec![],
+ is_fixed: false,
+ },
+ PoolPackageInput {
+ name: "bar/bar".to_string(),
+ version: "2.0.0.0".to_string(),
+ pretty_version: "2.0.0".to_string(),
+ requires: vec![],
+ replaces: vec![],
+ provides: vec![],
+ conflicts: vec![],
+ is_fixed: false,
+ },
+ ],
+ vec![],
);
+
+ let mut requires = HashMap::new();
+ requires.insert("foo/foo".to_string(), Some("^1.0".to_string()));
+
+ let generator = RuleSetGenerator::new(&mut pool);
+ let rules = generator.generate(&requires, &[]);
+
+ let policy = DefaultPolicy::default();
+ let solver = Solver::new(rules, &pool, policy, HashSet::new());
+ let result = solver.solve().unwrap();
+
+ // Should install foo/foo (id=1) and bar/bar (id=2)
+ assert!(result.installed.contains(&1));
+ assert!(result.installed.contains(&2));
}
// ──────────── End-to-end tests (require network, marked #[ignore]) ────────────