Skip to main content

rez_next_solver/
conflict.rs

1//! Conflict detection and resolution
2
3use crate::{ConflictResolution, ConflictStrategy, DependencyConflict};
4use rez_next_common::RezCoreError;
5use rez_next_version::Version;
6use std::collections::HashMap;
7
8/// Conflict resolver for dependency conflicts
9#[derive(Debug)]
10pub struct ConflictResolver {
11    /// Conflict resolution strategy
12    strategy: ConflictStrategy,
13    /// Resolution cache
14    cache: HashMap<String, ConflictResolution>,
15}
16
17impl ConflictResolver {
18    /// Create a new conflict resolver
19    pub fn new(strategy: ConflictStrategy) -> Self {
20        Self {
21            strategy,
22            cache: HashMap::new(),
23        }
24    }
25
26    /// Resolve a list of conflicts
27    pub async fn resolve_conflicts(
28        &self,
29        conflicts: Vec<DependencyConflict>,
30    ) -> Result<Vec<ConflictResolution>, RezCoreError> {
31        let mut resolutions = Vec::new();
32
33        for conflict in conflicts {
34            let resolution = self.resolve_single_conflict(&conflict).await?;
35            resolutions.push(resolution);
36        }
37
38        Ok(resolutions)
39    }
40
41    /// Resolve a single conflict
42    async fn resolve_single_conflict(
43        &self,
44        conflict: &DependencyConflict,
45    ) -> Result<ConflictResolution, RezCoreError> {
46        // Check cache first
47        let cache_key = self.generate_conflict_cache_key(conflict);
48        if let Some(cached_resolution) = self.cache.get(&cache_key) {
49            return Ok(cached_resolution.clone());
50        }
51
52        let resolution = match self.strategy {
53            ConflictStrategy::LatestWins => self.resolve_latest_wins(conflict).await?,
54            ConflictStrategy::EarliestWins => self.resolve_earliest_wins(conflict).await?,
55            ConflictStrategy::FailOnConflict => {
56                return Err(RezCoreError::Solver(format!(
57                    "Conflict detected for package {}: {:?}",
58                    conflict.package_name, conflict.conflicting_requirements
59                )));
60            }
61            ConflictStrategy::FindCompatible => self.resolve_find_compatible(conflict).await?,
62        };
63
64        Ok(resolution)
65    }
66
67    /// Resolve conflict by selecting the latest version (by lexicographic version spec comparison)
68    async fn resolve_latest_wins(
69        &self,
70        conflict: &DependencyConflict,
71    ) -> Result<ConflictResolution, RezCoreError> {
72        let mut latest_version: Option<Version> = None;
73        let modified_packages = conflict.source_packages.clone();
74
75        // Find the latest version among all requirements by parsing version specs
76        for requirement in &conflict.conflicting_requirements {
77            if let Some(ref version_spec) = requirement.version_spec {
78                if let Ok(v) = Version::parse(version_spec) {
79                    match &latest_version {
80                        Some(current) if v > *current => latest_version = Some(v),
81                        None => latest_version = Some(v),
82                        _ => {}
83                    }
84                }
85            }
86        }
87
88        Ok(ConflictResolution {
89            package_name: conflict.package_name.clone(),
90            selected_version: latest_version,
91            strategy: "latest_wins".to_string(),
92            modified_packages,
93        })
94    }
95
96    /// Resolve conflict by selecting the earliest version
97    async fn resolve_earliest_wins(
98        &self,
99        conflict: &DependencyConflict,
100    ) -> Result<ConflictResolution, RezCoreError> {
101        let mut earliest_version: Option<Version> = None;
102        let modified_packages = conflict.source_packages.clone();
103
104        for requirement in &conflict.conflicting_requirements {
105            if let Some(ref version_spec) = requirement.version_spec {
106                if let Ok(v) = Version::parse(version_spec) {
107                    match &earliest_version {
108                        Some(current) if v < *current => earliest_version = Some(v),
109                        None => earliest_version = Some(v),
110                        _ => {}
111                    }
112                }
113            }
114        }
115
116        Ok(ConflictResolution {
117            package_name: conflict.package_name.clone(),
118            selected_version: earliest_version,
119            strategy: "earliest_wins".to_string(),
120            modified_packages,
121        })
122    }
123
124    /// Resolve conflict by finding a compatible version (fallback to latest wins)
125    async fn resolve_find_compatible(
126        &self,
127        conflict: &DependencyConflict,
128    ) -> Result<ConflictResolution, RezCoreError> {
129        // Try to find a version that satisfies all requirements
130        // For now, collect all version specs and attempt to find a common one
131        let mut candidate: Option<Version> = None;
132
133        for requirement in &conflict.conflicting_requirements {
134            if let Some(ref version_spec) = requirement.version_spec {
135                if let Ok(v) = Version::parse(version_spec) {
136                    // Check if this version satisfies all other requirements
137                    let satisfies_all = conflict.conflicting_requirements.iter().all(|other_req| {
138                        if let Some(ref other_spec) = other_req.version_spec {
139                            // Simple compatibility: versions match or other has no constraint
140                            other_spec == version_spec || other_spec.is_empty()
141                        } else {
142                            true
143                        }
144                    });
145
146                    if satisfies_all {
147                        candidate = Some(v);
148                        break;
149                    }
150                }
151            }
152        }
153
154        // Fall back to latest wins if no compatible version found
155        if candidate.is_none() {
156            return self.resolve_latest_wins(conflict).await;
157        }
158
159        let modified_packages = conflict.source_packages.clone();
160        Ok(ConflictResolution {
161            package_name: conflict.package_name.clone(),
162            selected_version: candidate,
163            strategy: "find_compatible".to_string(),
164            modified_packages,
165        })
166    }
167
168    /// Generate a cache key for a conflict
169    fn generate_conflict_cache_key(&self, conflict: &DependencyConflict) -> String {
170        use std::collections::hash_map::DefaultHasher;
171        use std::hash::{Hash, Hasher};
172
173        let mut hasher = DefaultHasher::new();
174        conflict.package_name.hash(&mut hasher);
175        for req in &conflict.conflicting_requirements {
176            req.name.hash(&mut hasher);
177            if let Some(ref spec) = req.version_spec {
178                spec.hash(&mut hasher);
179            }
180        }
181        format!("{:x}", hasher.finish())
182    }
183}