Skip to main content

rez_next_solver/
graph.rs

1//! Dependency graph implementation
2
3use rez_next_common::RezCoreError;
4use rez_next_package::{Package, PackageRequirement};
5use rez_next_version::Version;
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet, VecDeque};
8
9/// Dependency graph node
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct GraphNode {
12    /// Package information
13    pub package: Package,
14    /// Direct dependencies (package names)
15    pub dependencies: HashSet<String>,
16    /// Packages that depend on this one
17    pub dependents: HashSet<String>,
18    /// Node metadata
19    pub metadata: HashMap<String, String>,
20}
21
22impl GraphNode {
23    /// Create a new graph node
24    pub fn new(package: Package) -> Self {
25        Self {
26            package,
27            dependencies: HashSet::new(),
28            dependents: HashSet::new(),
29            metadata: HashMap::new(),
30        }
31    }
32
33    /// Get the package key (name-version)
34    pub fn key(&self) -> String {
35        match &self.package.version {
36            Some(version) => format!("{}-{}", self.package.name, version.as_str()),
37            None => self.package.name.clone(),
38        }
39    }
40
41    /// Add a dependency
42    pub fn add_dependency(&mut self, dependency_key: String) {
43        self.dependencies.insert(dependency_key);
44    }
45
46    /// Add a dependent
47    pub fn add_dependent(&mut self, dependent_key: String) {
48        self.dependents.insert(dependent_key);
49    }
50
51    /// Remove a dependency
52    pub fn remove_dependency(&mut self, dependency_key: &str) {
53        self.dependencies.remove(dependency_key);
54    }
55
56    /// Remove a dependent
57    pub fn remove_dependent(&mut self, dependent_key: &str) {
58        self.dependents.remove(dependent_key);
59    }
60}
61
62/// Dependency conflict information
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct DependencyConflict {
65    /// Package name that has conflicting requirements
66    pub package_name: String,
67    /// Conflicting requirements
68    pub conflicting_requirements: Vec<PackageRequirement>,
69    /// Packages that introduced these requirements
70    pub source_packages: Vec<String>,
71    /// Conflict severity
72    pub severity: ConflictSeverity,
73}
74
75/// Conflict severity levels
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
77pub enum ConflictSeverity {
78    /// Minor version conflict
79    Minor,
80    /// Major version conflict
81    Major,
82    /// Incompatible requirements
83    Incompatible,
84}
85
86/// Conflict resolution
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct ConflictResolution {
89    /// Package name being resolved
90    pub package_name: String,
91    /// Selected package version
92    pub selected_version: Option<Version>,
93    /// Resolution strategy used
94    pub strategy: String,
95    /// Packages that were modified/removed
96    pub modified_packages: Vec<String>,
97}
98
99/// High-performance dependency graph
100#[derive(Debug, Clone)]
101pub struct DependencyGraph {
102    /// Graph nodes indexed by package key
103    nodes: HashMap<String, GraphNode>,
104    /// Requirements indexed by package name
105    requirements: HashMap<String, Vec<PackageRequirement>>,
106    /// Constraints that must be satisfied
107    constraints: Vec<PackageRequirement>,
108    /// Packages to exclude
109    exclusions: HashSet<String>,
110    /// Graph metadata
111    metadata: HashMap<String, String>,
112}
113
114impl DependencyGraph {
115    /// Create a new dependency graph
116    pub fn new() -> Self {
117        Self {
118            nodes: HashMap::new(),
119            requirements: HashMap::new(),
120            constraints: Vec::new(),
121            exclusions: HashSet::new(),
122            metadata: HashMap::new(),
123        }
124    }
125
126    /// Clear the graph
127    pub fn clear(&mut self) {
128        self.nodes.clear();
129        self.requirements.clear();
130        self.constraints.clear();
131        self.exclusions.clear();
132        self.metadata.clear();
133    }
134
135    /// Add a package to the graph
136    pub fn add_package(&mut self, package: Package) -> Result<(), RezCoreError> {
137        let key = match &package.version {
138            Some(version) => format!("{}-{}", package.name, version.as_str()),
139            None => package.name.clone(),
140        };
141
142        // Check if package is excluded
143        if self.exclusions.contains(&package.name) {
144            return Err(RezCoreError::Solver(format!(
145                "Package {} is excluded",
146                package.name
147            )));
148        }
149
150        let node = GraphNode::new(package);
151        self.nodes.insert(key, node);
152
153        Ok(())
154    }
155
156    /// Add a requirement to the graph
157    pub fn add_requirement(&mut self, requirement: PackageRequirement) -> Result<(), RezCoreError> {
158        self.requirements
159            .entry(requirement.name.clone())
160            .or_default()
161            .push(requirement);
162
163        Ok(())
164    }
165
166    /// Add a constraint
167    pub fn add_constraint(&mut self, constraint: PackageRequirement) -> Result<(), RezCoreError> {
168        self.constraints.push(constraint);
169        Ok(())
170    }
171
172    /// Add an exclusion
173    pub fn add_exclusion(&mut self, package_name: String) -> Result<(), RezCoreError> {
174        self.exclusions.insert(package_name);
175        Ok(())
176    }
177
178    /// Add a dependency edge between two packages
179    pub fn add_dependency_edge(
180        &mut self,
181        from_key: &str,
182        to_key: &str,
183    ) -> Result<(), RezCoreError> {
184        // Add dependency to the from node
185        if let Some(from_node) = self.nodes.get_mut(from_key) {
186            from_node.add_dependency(to_key.to_string());
187        } else {
188            return Err(RezCoreError::Solver(format!(
189                "Package {} not found in graph",
190                from_key
191            )));
192        }
193
194        // Add dependent to the to node
195        if let Some(to_node) = self.nodes.get_mut(to_key) {
196            to_node.add_dependent(from_key.to_string());
197        } else {
198            return Err(RezCoreError::Solver(format!(
199                "Package {} not found in graph",
200                to_key
201            )));
202        }
203
204        Ok(())
205    }
206
207    /// Remove a package from the graph
208    pub fn remove_package(&mut self, package_key: &str) -> Result<(), RezCoreError> {
209        if let Some(node) = self.nodes.remove(package_key) {
210            // Remove all edges involving this package
211            for dep_key in &node.dependencies {
212                if let Some(dep_node) = self.nodes.get_mut(dep_key) {
213                    dep_node.remove_dependent(package_key);
214                }
215            }
216
217            for dependent_key in &node.dependents {
218                if let Some(dependent_node) = self.nodes.get_mut(dependent_key) {
219                    dependent_node.remove_dependency(package_key);
220                }
221            }
222        }
223
224        Ok(())
225    }
226
227    /// Detect conflicts in the dependency graph
228    pub fn detect_conflicts(&self) -> Vec<DependencyConflict> {
229        let mut conflicts = Vec::new();
230
231        // Group requirements by package name
232        for (package_name, requirements) in &self.requirements {
233            if requirements.len() > 1 {
234                // Check if requirements are compatible via version range intersection
235                let mut incompatible_groups = Vec::new();
236
237                for (i, req1) in requirements.iter().enumerate() {
238                    for req2 in requirements.iter().skip(i + 1) {
239                        if !requirements_compatible(req1, req2) {
240                            incompatible_groups.push((req1.clone(), req2.clone()));
241                        }
242                    }
243                }
244
245                if !incompatible_groups.is_empty() {
246                    let severity = self.determine_conflict_severity(&incompatible_groups);
247                    let source_packages = self.find_requirement_sources(package_name);
248
249                    conflicts.push(DependencyConflict {
250                        package_name: package_name.clone(),
251                        conflicting_requirements: requirements.clone(),
252                        source_packages,
253                        severity,
254                    });
255                }
256            }
257        }
258
259        conflicts
260    }
261
262    /// Determine the severity of a conflict
263    fn determine_conflict_severity(
264        &self,
265        incompatible_groups: &[(PackageRequirement, PackageRequirement)],
266    ) -> ConflictSeverity {
267        // Simple heuristic: if any requirements are completely incompatible, it's incompatible
268        // If major versions differ, it's major
269        // Otherwise, it's minor
270
271        // Use range intersection to determine severity
272        for (req1, req2) in incompatible_groups {
273            let range1 = req1.version_spec.as_deref().unwrap_or("");
274            let range2 = req2.version_spec.as_deref().unwrap_or("");
275            // Both have ranges and they don't intersect → Incompatible
276            if !range1.is_empty() && !range2.is_empty() {
277                if let (Ok(r1), Ok(r2)) = (
278                    rez_next_version::VersionRange::parse(range1),
279                    rez_next_version::VersionRange::parse(range2),
280                ) {
281                    if r1.intersect(&r2).is_none() {
282                        return ConflictSeverity::Incompatible;
283                    }
284                }
285            }
286        }
287
288        ConflictSeverity::Major
289    }
290
291    /// Find which packages introduced requirements for a given package
292    fn find_requirement_sources(&self, package_name: &str) -> Vec<String> {
293        let mut sources = Vec::new();
294
295        for node in self.nodes.values() {
296            for req_str in &node.package.requires {
297                if let Ok(req) = PackageRequirement::parse(req_str) {
298                    if req.name == package_name {
299                        sources.push(node.key());
300                        break;
301                    }
302                }
303            }
304        }
305
306        sources
307    }
308
309    /// Apply a conflict resolution to the graph
310    pub fn apply_conflict_resolution(
311        &mut self,
312        resolution: ConflictResolution,
313    ) -> Result<(), RezCoreError> {
314        // Remove packages that were modified
315        for package_key in &resolution.modified_packages {
316            self.remove_package(package_key)?;
317        }
318
319        // Update requirements for the resolved package
320        if let Some(requirements) = self.requirements.get_mut(&resolution.package_name) {
321            // Create a new requirement based on the resolution
322            if let Some(ref version) = resolution.selected_version {
323                let new_requirement = PackageRequirement::with_version(
324                    resolution.package_name.clone(),
325                    version.as_str().to_string(),
326                );
327                requirements.clear();
328                requirements.push(new_requirement);
329            }
330        }
331
332        Ok(())
333    }
334
335    /// Get all resolved packages in topological order
336    pub fn get_resolved_packages(&self) -> Result<Vec<Package>, RezCoreError> {
337        let sorted_keys = self.topological_sort()?;
338        let mut packages = Vec::new();
339
340        for key in sorted_keys {
341            if let Some(node) = self.nodes.get(&key) {
342                packages.push(node.package.clone());
343            }
344        }
345
346        Ok(packages)
347    }
348
349    /// Perform topological sort of the dependency graph
350    fn topological_sort(&self) -> Result<Vec<String>, RezCoreError> {
351        let mut in_degree: HashMap<String, usize> = HashMap::new();
352        let mut result = Vec::new();
353        let mut queue = VecDeque::new();
354
355        // Calculate in-degrees
356        for (key, node) in &self.nodes {
357            in_degree.insert(key.clone(), node.dependents.len());
358        }
359
360        // Find nodes with no incoming edges
361        for (key, &degree) in &in_degree {
362            if degree == 0 {
363                queue.push_back(key.clone());
364            }
365        }
366
367        // Process nodes
368        while let Some(current_key) = queue.pop_front() {
369            result.push(current_key.clone());
370
371            if let Some(current_node) = self.nodes.get(&current_key) {
372                // Reduce in-degree of dependent nodes
373                for dep_key in &current_node.dependencies {
374                    if let Some(degree) = in_degree.get_mut(dep_key) {
375                        *degree -= 1;
376                        if *degree == 0 {
377                            queue.push_back(dep_key.clone());
378                        }
379                    }
380                }
381            }
382        }
383
384        // Check for cycles
385        if result.len() != self.nodes.len() {
386            return Err(RezCoreError::Solver(
387                "Circular dependency detected in package graph".to_string(),
388            ));
389        }
390
391        Ok(result)
392    }
393
394    /// Get graph statistics
395    pub fn get_stats(&self) -> GraphStats {
396        let mut total_dependencies = 0;
397        let mut max_depth = 0;
398
399        for node in self.nodes.values() {
400            total_dependencies += node.dependencies.len();
401            let depth = self.calculate_node_depth(&node.key());
402            max_depth = max_depth.max(depth);
403        }
404
405        GraphStats {
406            node_count: self.nodes.len(),
407            edge_count: total_dependencies,
408            max_depth,
409            conflict_count: self.detect_conflicts().len(),
410            constraint_count: self.constraints.len(),
411            exclusion_count: self.exclusions.len(),
412        }
413    }
414
415    /// Calculate the depth of a node in the graph
416    fn calculate_node_depth(&self, node_key: &str) -> usize {
417        let mut visited = HashSet::new();
418        self.calculate_depth_recursive(node_key, &mut visited)
419    }
420
421    /// Recursive helper for depth calculation
422    fn calculate_depth_recursive(&self, node_key: &str, visited: &mut HashSet<String>) -> usize {
423        if visited.contains(node_key) {
424            return 0; // Avoid infinite recursion
425        }
426
427        visited.insert(node_key.to_string());
428
429        if let Some(node) = self.nodes.get(node_key) {
430            let mut max_dep_depth = 0;
431            for dep_key in &node.dependencies {
432                let dep_depth = self.calculate_depth_recursive(dep_key, visited);
433                max_dep_depth = max_dep_depth.max(dep_depth);
434            }
435            max_dep_depth + 1
436        } else {
437            0
438        }
439    }
440}
441
442/// Graph statistics
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct GraphStats {
445    /// Number of nodes in the graph
446    pub node_count: usize,
447    /// Number of edges in the graph
448    pub edge_count: usize,
449    /// Maximum depth of the graph
450    pub max_depth: usize,
451    /// Number of conflicts detected
452    pub conflict_count: usize,
453    /// Number of constraints
454    pub constraint_count: usize,
455    /// Number of exclusions
456    pub exclusion_count: usize,
457}
458
459impl Default for DependencyGraph {
460    fn default() -> Self {
461        Self::new()
462    }
463}
464
465/// Check if two PackageRequirements for the same package are compatible.
466/// Two requirements are compatible if their version ranges have a non-empty intersection.
467fn requirements_compatible(req1: &PackageRequirement, req2: &PackageRequirement) -> bool {
468    let spec1 = req1.version_spec.as_deref().unwrap_or("");
469    let spec2 = req2.version_spec.as_deref().unwrap_or("");
470
471    // If either has no version constraint, they're compatible
472    if spec1.is_empty() || spec2.is_empty() {
473        return true;
474    }
475
476    // Parse both ranges and check for intersection
477    match (
478        rez_next_version::VersionRange::parse(spec1),
479        rez_next_version::VersionRange::parse(spec2),
480    ) {
481        (Ok(r1), Ok(r2)) => r1.intersect(&r2).is_some(),
482        _ => true, // If parsing fails, assume compatible (don't false-flag)
483    }
484}
485
486#[cfg(test)]
487mod graph_tests {
488    use super::*;
489
490    fn make_pkg(name: &str, ver: &str) -> Package {
491        let mut p = Package::new(name.to_string());
492        p.version = Some(Version::parse(ver).unwrap());
493        p
494    }
495
496    /// New graph starts empty
497    #[test]
498    fn test_graph_new_is_empty() {
499        let g = DependencyGraph::new();
500        let stats = g.get_stats();
501        assert_eq!(stats.node_count, 0);
502        assert_eq!(stats.edge_count, 0);
503        assert_eq!(stats.conflict_count, 0);
504    }
505
506    /// Adding a package increases node count
507    #[test]
508    fn test_graph_add_package_increments_nodes() {
509        let mut g = DependencyGraph::new();
510        g.add_package(make_pkg("python", "3.9.0")).unwrap();
511        let stats = g.get_stats();
512        assert_eq!(stats.node_count, 1);
513    }
514
515    /// Adding duplicate package is idempotent (no error, same count)
516    #[test]
517    fn test_graph_add_duplicate_package_no_error() {
518        let mut g = DependencyGraph::new();
519        g.add_package(make_pkg("maya", "2023.0")).unwrap();
520        // Adding same package again should be ok (update or ignore)
521        let result = g.add_package(make_pkg("maya", "2023.0"));
522        assert!(result.is_ok(), "Re-adding same package should not error");
523    }
524
525    /// clear() resets graph to empty
526    #[test]
527    fn test_graph_clear_resets_state() {
528        let mut g = DependencyGraph::new();
529        g.add_package(make_pkg("houdini", "20.0")).unwrap();
530        g.add_package(make_pkg("python", "3.10.0")).unwrap();
531        g.clear();
532        let stats = g.get_stats();
533        assert_eq!(stats.node_count, 0);
534        assert_eq!(stats.conflict_count, 0);
535    }
536
537    /// get_resolved_packages returns packages with no conflicts
538    #[test]
539    fn test_graph_get_resolved_packages_no_conflicts() {
540        let mut g = DependencyGraph::new();
541        g.add_package(make_pkg("nuke", "14.0")).unwrap();
542        g.add_package(make_pkg("ocio", "2.1")).unwrap();
543        let resolved = g.get_resolved_packages().unwrap();
544        assert_eq!(resolved.len(), 2);
545    }
546
547    /// requirements_compatible: unconstrained requirements are always compatible
548    #[test]
549    fn test_requirements_compatible_unconstrained() {
550        let r1 = PackageRequirement::parse("python").unwrap();
551        let r2 = PackageRequirement::parse("python").unwrap();
552        assert!(
553            requirements_compatible(&r1, &r2),
554            "Two unconstrained requirements for same package should be compatible"
555        );
556    }
557}