1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct GraphNode {
12 pub package: Package,
14 pub dependencies: HashSet<String>,
16 pub dependents: HashSet<String>,
18 pub metadata: HashMap<String, String>,
20}
21
22impl GraphNode {
23 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 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 pub fn add_dependency(&mut self, dependency_key: String) {
43 self.dependencies.insert(dependency_key);
44 }
45
46 pub fn add_dependent(&mut self, dependent_key: String) {
48 self.dependents.insert(dependent_key);
49 }
50
51 pub fn remove_dependency(&mut self, dependency_key: &str) {
53 self.dependencies.remove(dependency_key);
54 }
55
56 pub fn remove_dependent(&mut self, dependent_key: &str) {
58 self.dependents.remove(dependent_key);
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct DependencyConflict {
65 pub package_name: String,
67 pub conflicting_requirements: Vec<PackageRequirement>,
69 pub source_packages: Vec<String>,
71 pub severity: ConflictSeverity,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
77pub enum ConflictSeverity {
78 Minor,
80 Major,
82 Incompatible,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct ConflictResolution {
89 pub package_name: String,
91 pub selected_version: Option<Version>,
93 pub strategy: String,
95 pub modified_packages: Vec<String>,
97}
98
99#[derive(Debug, Clone)]
101pub struct DependencyGraph {
102 nodes: HashMap<String, GraphNode>,
104 requirements: HashMap<String, Vec<PackageRequirement>>,
106 constraints: Vec<PackageRequirement>,
108 exclusions: HashSet<String>,
110 metadata: HashMap<String, String>,
112}
113
114impl DependencyGraph {
115 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 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 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 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 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 pub fn add_constraint(&mut self, constraint: PackageRequirement) -> Result<(), RezCoreError> {
168 self.constraints.push(constraint);
169 Ok(())
170 }
171
172 pub fn add_exclusion(&mut self, package_name: String) -> Result<(), RezCoreError> {
174 self.exclusions.insert(package_name);
175 Ok(())
176 }
177
178 pub fn add_dependency_edge(
180 &mut self,
181 from_key: &str,
182 to_key: &str,
183 ) -> Result<(), RezCoreError> {
184 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 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 pub fn remove_package(&mut self, package_key: &str) -> Result<(), RezCoreError> {
209 if let Some(node) = self.nodes.remove(package_key) {
210 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 pub fn detect_conflicts(&self) -> Vec<DependencyConflict> {
229 let mut conflicts = Vec::new();
230
231 for (package_name, requirements) in &self.requirements {
233 if requirements.len() > 1 {
234 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 fn determine_conflict_severity(
264 &self,
265 incompatible_groups: &[(PackageRequirement, PackageRequirement)],
266 ) -> ConflictSeverity {
267 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 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 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 pub fn apply_conflict_resolution(
311 &mut self,
312 resolution: ConflictResolution,
313 ) -> Result<(), RezCoreError> {
314 for package_key in &resolution.modified_packages {
316 self.remove_package(package_key)?;
317 }
318
319 if let Some(requirements) = self.requirements.get_mut(&resolution.package_name) {
321 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 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 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 for (key, node) in &self.nodes {
357 in_degree.insert(key.clone(), node.dependents.len());
358 }
359
360 for (key, °ree) in &in_degree {
362 if degree == 0 {
363 queue.push_back(key.clone());
364 }
365 }
366
367 while let Some(current_key) = queue.pop_front() {
369 result.push(current_key.clone());
370
371 if let Some(current_node) = self.nodes.get(¤t_key) {
372 for dep_key in ¤t_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 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 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 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 fn calculate_depth_recursive(&self, node_key: &str, visited: &mut HashSet<String>) -> usize {
423 if visited.contains(node_key) {
424 return 0; }
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#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct GraphStats {
445 pub node_count: usize,
447 pub edge_count: usize,
449 pub max_depth: usize,
451 pub conflict_count: usize,
453 pub constraint_count: usize,
455 pub exclusion_count: usize,
457}
458
459impl Default for DependencyGraph {
460 fn default() -> Self {
461 Self::new()
462 }
463}
464
465fn 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 spec1.is_empty() || spec2.is_empty() {
473 return true;
474 }
475
476 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, }
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 #[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 #[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 #[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 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 #[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 #[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 #[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}