From 8cc1ba8a02c0318b65658f1634de378c780392b9 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 10 May 2026 00:32:08 +0900 Subject: refactor(workspace): consolidate crates into mozart-core Merged mozart-archiver, mozart-autoload, mozart-registry, mozart-sat-resolver, and mozart-vcs into mozart-core to align the source layout with Composer's structure. Co-Authored-By: Claude Sonnet 4.6 --- .../src/dependency_resolver/pool_builder.rs | 222 +++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 crates/mozart-core/src/dependency_resolver/pool_builder.rs (limited to 'crates/mozart-core/src/dependency_resolver/pool_builder.rs') diff --git a/crates/mozart-core/src/dependency_resolver/pool_builder.rs b/crates/mozart-core/src/dependency_resolver/pool_builder.rs new file mode 100644 index 0000000..e037b01 --- /dev/null +++ b/crates/mozart-core/src/dependency_resolver/pool_builder.rs @@ -0,0 +1,222 @@ +use super::pool::{Pool, PoolLink, PoolPackageInput}; +use indexmap::IndexSet; +use std::collections::VecDeque; + +/// Builder for constructing a Pool from package metadata. +/// +/// The builder accepts package inputs and recursively discovers +/// transitive dependencies. This is done by the registry layer +/// before solving. +pub struct PoolBuilder { + /// Packages to add to the pool. + inputs: Vec, + /// Names already added (to avoid duplicates). + added: IndexSet, + /// Queue of package names that need to be explored. + pending_names: VecDeque, + /// Package names that have already been explored (returned by next_pending). + explored_names: IndexSet, + /// Specific platform packages to ignore (from `--ignore-platform-req=name`). + ignore_platform_reqs: IndexSet, + /// When true, ignore every platform package (php, ext-*, lib-*, composer-*). + /// Mirrors `--ignore-platform-reqs` (no value). + ignore_all_platform_reqs: bool, +} + +impl PoolBuilder { + pub fn new() -> Self { + PoolBuilder { + inputs: Vec::new(), + added: IndexSet::new(), + pending_names: VecDeque::new(), + explored_names: IndexSet::new(), + ignore_platform_reqs: IndexSet::new(), + ignore_all_platform_reqs: false, + } + } + + /// Set platform requirements to ignore during exploration. + pub fn set_ignore_platform_reqs(&mut self, names: IndexSet) { + self.ignore_platform_reqs = names; + } + + /// When set, every platform package is skipped during exploration. + pub fn set_ignore_all_platform_reqs(&mut self, ignore_all: bool) { + self.ignore_all_platform_reqs = ignore_all; + } + + fn is_ignored_platform_dep(&self, name: &str) -> bool { + if self + .ignore_platform_reqs + .iter() + .any(|p| crate::matches_wildcard(name, p)) + { + return true; + } + self.ignore_all_platform_reqs && crate::platform::is_platform_package(name) + } + + /// Add a package version to the builder. Returns true if it's new. + pub fn add_package(&mut self, input: PoolPackageInput) -> bool { + let key = format!("{}@{}", input.name, input.version); + if self.added.contains(&key) { + return false; + } + self.added.insert(key); + + // Queue dependency names for exploration + for link in &input.requires { + if !self.is_ignored_platform_dep(&link.target) { + self.pending_names.push_back(link.target.clone()); + } + } + + self.inputs.push(input); + true + } + + /// Get the next package name that needs to be explored. + /// The caller should fetch available versions for this package + /// and add them via `add_package`. + pub fn next_pending(&mut self) -> Option { + while let Some(name) = self.pending_names.pop_front() { + // Skip if already explored or already has versions in inputs + if self.explored_names.contains(&name) { + continue; + } + if self.inputs.iter().any(|p| p.name == name) { + continue; + } + self.explored_names.insert(name.clone()); + return Some(name); + } + None + } + + /// Check if there are more names to explore. + pub fn has_pending(&self) -> bool { + !self.pending_names.is_empty() + } + + /// Build the final Pool. + pub fn build(self) -> Pool { + Pool::new(self.inputs, vec![]) + } + + /// Get the number of packages added so far. + pub fn len(&self) -> usize { + self.inputs.len() + } + + /// Read-only access to package inputs collected so far. Used by the + /// registry layer to materialize root aliases (`require: "X as Y"`) once + /// every base + branch-alias entry is in place: a second pass scans for + /// matching `(name, version)` and pushes the alias entry on top. + pub fn inputs(&self) -> &[PoolPackageInput] { + &self.inputs + } + + /// Whether the builder has no packages. + pub fn is_empty(&self) -> bool { + self.inputs.is_empty() + } +} + +impl Default for PoolBuilder { + fn default() -> Self { + Self::new() + } +} + +/// Helper to convert (name, constraint) pairs from Packagist into PoolLinks. +/// +/// `source_version` is the normalized version of the package declaring these +/// links; it replaces any `"self.version"` constraint, mirroring Composer's +/// `ArrayLoader::createLink` (and `AliasPackage::replaceSelfVersionDependencies`, +/// which feeds the alias's own version in for the same purpose). +pub fn make_pool_links( + source: &str, + source_version: &str, + deps: &[(String, String)], +) -> Vec { + deps.iter() + .map(|(target, constraint)| PoolLink { + target: target.clone(), + constraint: if constraint.trim() == "self.version" { + source_version.to_string() + } else { + constraint.clone() + }, + source: source.to_string(), + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pool_builder_basic() { + let mut builder = PoolBuilder::new(); + + builder.add_package(PoolPackageInput { + name: "a/a".to_string(), + version: "1.0.0.0".to_string(), + pretty_version: "1.0.0".to_string(), + requires: vec![PoolLink { + target: "b/b".to_string(), + constraint: "^1.0".to_string(), + source: "a/a".to_string(), + }], + replaces: vec![], + provides: vec![], + conflicts: vec![], + is_fixed: false, + is_alias_of: None, + }); + + // Should have b/b pending + let pending = builder.next_pending(); + assert_eq!(pending, Some("b/b".to_string())); + + builder.add_package(PoolPackageInput { + name: "b/b".to_string(), + version: "1.0.0.0".to_string(), + pretty_version: "1.0.0".to_string(), + requires: vec![], + replaces: vec![], + provides: vec![], + conflicts: vec![], + is_fixed: false, + is_alias_of: None, + }); + + // No more pending + assert!(builder.next_pending().is_none()); + + let pool = builder.build(); + assert_eq!(pool.len(), 2); + } + + #[test] + fn test_deduplication() { + let mut builder = PoolBuilder::new(); + + let input = PoolPackageInput { + name: "a/a".to_string(), + version: "1.0.0.0".to_string(), + pretty_version: "1.0.0".to_string(), + requires: vec![], + replaces: vec![], + provides: vec![], + conflicts: vec![], + is_fixed: false, + is_alias_of: None, + }; + + assert!(builder.add_package(input.clone())); + assert!(!builder.add_package(input)); + assert_eq!(builder.len(), 1); + } +} -- cgit v1.3.1