aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src/dependency_resolver/pool_builder.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart-core/src/dependency_resolver/pool_builder.rs')
-rw-r--r--crates/mozart-core/src/dependency_resolver/pool_builder.rs222
1 files changed, 222 insertions, 0 deletions
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<PoolPackageInput>,
+ /// Names already added (to avoid duplicates).
+ added: IndexSet<String>,
+ /// Queue of package names that need to be explored.
+ pending_names: VecDeque<String>,
+ /// Package names that have already been explored (returned by next_pending).
+ explored_names: IndexSet<String>,
+ /// Specific platform packages to ignore (from `--ignore-platform-req=name`).
+ ignore_platform_reqs: IndexSet<String>,
+ /// 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<String>) {
+ 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<String> {
+ 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<PoolLink> {
+ 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);
+ }
+}