use rez_next_common::RezCoreError;
use rez_next_package::{Package, PackageRequirement};
use rez_next_version::Version;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphNode {
pub package: Package,
pub dependencies: HashSet<String>,
pub dependents: HashSet<String>,
pub metadata: HashMap<String, String>,
}
impl GraphNode {
pub fn new(package: Package) -> Self {
Self {
package,
dependencies: HashSet::new(),
dependents: HashSet::new(),
metadata: HashMap::new(),
}
}
pub fn key(&self) -> String {
match &self.package.version {
Some(version) => format!("{}-{}", self.package.name, version.as_str()),
None => self.package.name.clone(),
}
}
pub fn add_dependency(&mut self, dependency_key: String) {
self.dependencies.insert(dependency_key);
}
pub fn add_dependent(&mut self, dependent_key: String) {
self.dependents.insert(dependent_key);
}
pub fn remove_dependency(&mut self, dependency_key: &str) {
self.dependencies.remove(dependency_key);
}
pub fn remove_dependent(&mut self, dependent_key: &str) {
self.dependents.remove(dependent_key);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyConflict {
pub package_name: String,
pub conflicting_requirements: Vec<PackageRequirement>,
pub source_packages: Vec<String>,
pub severity: ConflictSeverity,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
pub enum ConflictSeverity {
Minor,
Major,
Incompatible,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConflictResolution {
pub package_name: String,
pub selected_version: Option<Version>,
pub strategy: String,
pub modified_packages: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct DependencyGraph {
nodes: HashMap<String, GraphNode>,
requirements: HashMap<String, Vec<PackageRequirement>>,
constraints: Vec<PackageRequirement>,
exclusions: HashSet<String>,
metadata: HashMap<String, String>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
requirements: HashMap::new(),
constraints: Vec::new(),
exclusions: HashSet::new(),
metadata: HashMap::new(),
}
}
pub fn clear(&mut self) {
self.nodes.clear();
self.requirements.clear();
self.constraints.clear();
self.exclusions.clear();
self.metadata.clear();
}
pub fn add_package(&mut self, package: Package) -> Result<(), RezCoreError> {
let key = match &package.version {
Some(version) => format!("{}-{}", package.name, version.as_str()),
None => package.name.clone(),
};
if self.exclusions.contains(&package.name) {
return Err(RezCoreError::Solver(format!(
"Package {} is excluded",
package.name
)));
}
let node = GraphNode::new(package);
self.nodes.insert(key, node);
Ok(())
}
pub fn add_requirement(&mut self, requirement: PackageRequirement) -> Result<(), RezCoreError> {
self.requirements
.entry(requirement.name.clone())
.or_default()
.push(requirement);
Ok(())
}
pub fn add_constraint(&mut self, constraint: PackageRequirement) -> Result<(), RezCoreError> {
self.constraints.push(constraint);
Ok(())
}
pub fn add_exclusion(&mut self, package_name: String) -> Result<(), RezCoreError> {
self.exclusions.insert(package_name);
Ok(())
}
pub fn add_dependency_edge(
&mut self,
from_key: &str,
to_key: &str,
) -> Result<(), RezCoreError> {
if let Some(from_node) = self.nodes.get_mut(from_key) {
from_node.add_dependency(to_key.to_string());
} else {
return Err(RezCoreError::Solver(format!(
"Package {} not found in graph",
from_key
)));
}
if let Some(to_node) = self.nodes.get_mut(to_key) {
to_node.add_dependent(from_key.to_string());
} else {
return Err(RezCoreError::Solver(format!(
"Package {} not found in graph",
to_key
)));
}
Ok(())
}
pub fn remove_package(&mut self, package_key: &str) -> Result<(), RezCoreError> {
if let Some(node) = self.nodes.remove(package_key) {
for dep_key in &node.dependencies {
if let Some(dep_node) = self.nodes.get_mut(dep_key) {
dep_node.remove_dependent(package_key);
}
}
for dependent_key in &node.dependents {
if let Some(dependent_node) = self.nodes.get_mut(dependent_key) {
dependent_node.remove_dependency(package_key);
}
}
}
Ok(())
}
pub fn detect_conflicts(&self) -> Vec<DependencyConflict> {
let mut conflicts = Vec::new();
for (package_name, requirements) in &self.requirements {
if requirements.len() > 1 {
let mut incompatible_groups = Vec::new();
for (i, req1) in requirements.iter().enumerate() {
for req2 in requirements.iter().skip(i + 1) {
if !requirements_compatible(req1, req2) {
incompatible_groups.push((req1.clone(), req2.clone()));
}
}
}
if !incompatible_groups.is_empty() {
let severity = self.determine_conflict_severity(&incompatible_groups);
let source_packages = self.find_requirement_sources(package_name);
conflicts.push(DependencyConflict {
package_name: package_name.clone(),
conflicting_requirements: requirements.clone(),
source_packages,
severity,
});
}
}
}
conflicts
}
fn determine_conflict_severity(
&self,
incompatible_groups: &[(PackageRequirement, PackageRequirement)],
) -> ConflictSeverity {
for (req1, req2) in incompatible_groups {
let range1 = req1.version_spec.as_deref().unwrap_or("");
let range2 = req2.version_spec.as_deref().unwrap_or("");
if !range1.is_empty() && !range2.is_empty() {
if let (Ok(r1), Ok(r2)) = (
rez_next_version::VersionRange::parse(range1),
rez_next_version::VersionRange::parse(range2),
) {
if r1.intersect(&r2).is_none() {
return ConflictSeverity::Incompatible;
}
}
}
}
ConflictSeverity::Major
}
fn find_requirement_sources(&self, package_name: &str) -> Vec<String> {
let mut sources = Vec::new();
for node in self.nodes.values() {
for req_str in &node.package.requires {
if let Ok(req) = PackageRequirement::parse(req_str) {
if req.name == package_name {
sources.push(node.key());
break;
}
}
}
}
sources
}
pub fn apply_conflict_resolution(
&mut self,
resolution: ConflictResolution,
) -> Result<(), RezCoreError> {
for package_key in &resolution.modified_packages {
self.remove_package(package_key)?;
}
if let Some(requirements) = self.requirements.get_mut(&resolution.package_name) {
if let Some(ref version) = resolution.selected_version {
let new_requirement = PackageRequirement::with_version(
resolution.package_name.clone(),
version.as_str().to_string(),
);
requirements.clear();
requirements.push(new_requirement);
}
}
Ok(())
}
pub fn get_resolved_packages(&self) -> Result<Vec<Package>, RezCoreError> {
let sorted_keys = self.topological_sort()?;
let mut packages = Vec::new();
for key in sorted_keys {
if let Some(node) = self.nodes.get(&key) {
packages.push(node.package.clone());
}
}
Ok(packages)
}
fn topological_sort(&self) -> Result<Vec<String>, RezCoreError> {
let mut in_degree: HashMap<String, usize> = HashMap::new();
let mut result = Vec::new();
let mut queue = VecDeque::new();
for (key, node) in &self.nodes {
in_degree.insert(key.clone(), node.dependents.len());
}
for (key, °ree) in &in_degree {
if degree == 0 {
queue.push_back(key.clone());
}
}
while let Some(current_key) = queue.pop_front() {
result.push(current_key.clone());
if let Some(current_node) = self.nodes.get(¤t_key) {
for dep_key in ¤t_node.dependencies {
if let Some(degree) = in_degree.get_mut(dep_key) {
*degree -= 1;
if *degree == 0 {
queue.push_back(dep_key.clone());
}
}
}
}
}
if result.len() != self.nodes.len() {
return Err(RezCoreError::Solver(
"Circular dependency detected in package graph".to_string(),
));
}
Ok(result)
}
pub fn get_stats(&self) -> GraphStats {
let mut total_dependencies = 0;
let mut max_depth = 0;
for node in self.nodes.values() {
total_dependencies += node.dependencies.len();
let depth = self.calculate_node_depth(&node.key());
max_depth = max_depth.max(depth);
}
GraphStats {
node_count: self.nodes.len(),
edge_count: total_dependencies,
max_depth,
conflict_count: self.detect_conflicts().len(),
constraint_count: self.constraints.len(),
exclusion_count: self.exclusions.len(),
}
}
fn calculate_node_depth(&self, node_key: &str) -> usize {
let mut visited = HashSet::new();
self.calculate_depth_recursive(node_key, &mut visited)
}
fn calculate_depth_recursive(&self, node_key: &str, visited: &mut HashSet<String>) -> usize {
if visited.contains(node_key) {
return 0; }
visited.insert(node_key.to_string());
if let Some(node) = self.nodes.get(node_key) {
let mut max_dep_depth = 0;
for dep_key in &node.dependencies {
let dep_depth = self.calculate_depth_recursive(dep_key, visited);
max_dep_depth = max_dep_depth.max(dep_depth);
}
max_dep_depth + 1
} else {
0
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphStats {
pub node_count: usize,
pub edge_count: usize,
pub max_depth: usize,
pub conflict_count: usize,
pub constraint_count: usize,
pub exclusion_count: usize,
}
impl Default for DependencyGraph {
fn default() -> Self {
Self::new()
}
}
fn requirements_compatible(req1: &PackageRequirement, req2: &PackageRequirement) -> bool {
let spec1 = req1.version_spec.as_deref().unwrap_or("");
let spec2 = req2.version_spec.as_deref().unwrap_or("");
if spec1.is_empty() || spec2.is_empty() {
return true;
}
match (
rez_next_version::VersionRange::parse(spec1),
rez_next_version::VersionRange::parse(spec2),
) {
(Ok(r1), Ok(r2)) => r1.intersect(&r2).is_some(),
_ => true, }
}
#[cfg(test)]
mod graph_tests {
use super::*;
fn make_pkg(name: &str, ver: &str) -> Package {
let mut p = Package::new(name.to_string());
p.version = Some(Version::parse(ver).unwrap());
p
}
#[test]
fn test_graph_new_is_empty() {
let g = DependencyGraph::new();
let stats = g.get_stats();
assert_eq!(stats.node_count, 0);
assert_eq!(stats.edge_count, 0);
assert_eq!(stats.conflict_count, 0);
}
#[test]
fn test_graph_add_package_increments_nodes() {
let mut g = DependencyGraph::new();
g.add_package(make_pkg("python", "3.9.0")).unwrap();
let stats = g.get_stats();
assert_eq!(stats.node_count, 1);
}
#[test]
fn test_graph_add_duplicate_package_no_error() {
let mut g = DependencyGraph::new();
g.add_package(make_pkg("maya", "2023.0")).unwrap();
let result = g.add_package(make_pkg("maya", "2023.0"));
assert!(result.is_ok(), "Re-adding same package should not error");
}
#[test]
fn test_graph_clear_resets_state() {
let mut g = DependencyGraph::new();
g.add_package(make_pkg("houdini", "20.0")).unwrap();
g.add_package(make_pkg("python", "3.10.0")).unwrap();
g.clear();
let stats = g.get_stats();
assert_eq!(stats.node_count, 0);
assert_eq!(stats.conflict_count, 0);
}
#[test]
fn test_graph_get_resolved_packages_no_conflicts() {
let mut g = DependencyGraph::new();
g.add_package(make_pkg("nuke", "14.0")).unwrap();
g.add_package(make_pkg("ocio", "2.1")).unwrap();
let resolved = g.get_resolved_packages().unwrap();
assert_eq!(resolved.len(), 2);
}
#[test]
fn test_requirements_compatible_unconstrained() {
let r1 = PackageRequirement::parse("python").unwrap();
let r2 = PackageRequirement::parse("python").unwrap();
assert!(
requirements_compatible(&r1, &r2),
"Two unconstrained requirements for same package should be compatible"
);
}
}