Skip to main content

rez_next_solver/
dependency_resolver.rs

1//! Dependency resolution implementation - equivalent to Python's solver
2
3use crate::resolution_state::ResolutionState;
4use crate::SolverConfig;
5use rez_next_common::RezCoreError;
6use rez_next_package::{Package, Requirement};
7use rez_next_repository::simple_repository::RepositoryManager;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// A dependency resolver that finds compatible package combinations
12pub struct DependencyResolver {
13    /// Repository manager for package discovery
14    repository_manager: Arc<RepositoryManager>,
15
16    /// Solver configuration
17    config: SolverConfig,
18
19    /// Cache of resolved packages
20    package_cache: HashMap<String, Vec<Arc<Package>>>,
21}
22
23/// Detailed resolution result containing resolved packages, failures, conflicts and stats
24#[derive(Debug, Clone)]
25pub struct DetailedResolutionResult {
26    /// Successfully resolved packages
27    pub resolved_packages: Vec<ResolvedPackageInfo>,
28
29    /// Requirements that couldn't be satisfied
30    pub failed_requirements: Vec<Requirement>,
31
32    /// Conflicts encountered during resolution
33    pub conflicts: Vec<ResolutionConflict>,
34
35    /// Resolution statistics
36    pub stats: ResolutionStats,
37}
38
39/// Information about a resolved package
40#[derive(Debug, Clone)]
41pub struct ResolvedPackageInfo {
42    /// The resolved package
43    pub package: Arc<Package>,
44
45    /// The variant index if this package has variants
46    pub variant_index: Option<usize>,
47
48    /// Whether this package was explicitly requested
49    pub requested: bool,
50
51    /// The requirements that led to this package being included
52    pub required_by: Vec<String>,
53
54    /// The specific requirement that was satisfied
55    pub satisfying_requirement: Option<Requirement>,
56}
57
58/// A conflict encountered during resolution
59#[derive(Debug, Clone, serde::Serialize)]
60pub struct ResolutionConflict {
61    /// The package name that has conflicting requirements
62    pub package_name: String,
63
64    /// The conflicting requirements
65    pub conflicting_requirements: Vec<Requirement>,
66
67    /// The packages that introduced these requirements
68    pub source_packages: Vec<String>,
69}
70
71/// Statistics about the resolution process
72#[derive(Debug, Clone, serde::Serialize, Default)]
73pub struct ResolutionStats {
74    /// Number of packages considered
75    pub packages_considered: usize,
76
77    /// Number of variants evaluated
78    pub variants_evaluated: usize,
79
80    /// Time spent resolving (in milliseconds)
81    pub resolution_time_ms: u64,
82
83    /// Number of conflicts encountered
84    pub conflicts_encountered: usize,
85
86    /// Number of backtracking steps
87    pub backtrack_steps: usize,
88}
89
90impl DependencyResolver {
91    /// Create a new dependency resolver
92    pub fn new(repository_manager: Arc<RepositoryManager>, config: SolverConfig) -> Self {
93        Self {
94            repository_manager,
95            config,
96            package_cache: HashMap::new(),
97        }
98    }
99
100    /// Resolve a set of requirements into a consistent package set
101    pub async fn resolve(
102        &mut self,
103        requirements: Vec<Requirement>,
104    ) -> Result<DetailedResolutionResult, RezCoreError> {
105        let start_time = std::time::Instant::now();
106
107        // Initialize resolution state
108        let mut resolution_state = ResolutionState::new(requirements.clone());
109
110        // Perform resolution
111        let result = self.resolve_recursive(&mut resolution_state).await?;
112
113        // Check for cyclic dependencies after full resolution
114        if let Some(cycle) = resolution_state.detect_cycle() {
115            return Err(RezCoreError::Solver(format!(
116                "Cyclic dependency detected: {}",
117                cycle.join(" -> ")
118            )));
119        }
120
121        // Calculate statistics
122        let resolution_time = start_time.elapsed().as_millis() as u64;
123        let stats = ResolutionStats {
124            packages_considered: resolution_state.packages_considered,
125            variants_evaluated: resolution_state.variants_evaluated,
126            resolution_time_ms: resolution_time,
127            conflicts_encountered: resolution_state.conflicts.len(),
128            backtrack_steps: resolution_state.backtrack_steps,
129        };
130
131        let failed = resolution_state.failed_requirements;
132
133        // Strict mode: any unsatisfied requirement is a hard error
134        if self.config.strict_mode && !failed.is_empty() {
135            let names: Vec<String> = failed.iter().map(|r| r.to_string()).collect();
136            return Err(RezCoreError::Solver(format!(
137                "Strict mode: failed to satisfy requirements: {}",
138                names.join(", ")
139            )));
140        }
141
142        Ok(DetailedResolutionResult {
143            resolved_packages: result,
144            failed_requirements: failed,
145            conflicts: resolution_state.conflicts,
146            stats,
147        })
148    }
149
150    /// Recursive resolution implementation
151    async fn resolve_recursive(
152        &mut self,
153        state: &mut ResolutionState,
154    ) -> Result<Vec<ResolvedPackageInfo>, RezCoreError> {
155        // Get next requirement to resolve
156        while let Some(requirement) = state.get_next_requirement() {
157            // Check if we already have a package that satisfies this requirement
158            if let Some(existing) = state.find_satisfying_package(&requirement) {
159                // Mark this requirement as satisfied
160                state.mark_requirement_satisfied(&requirement, existing.package.name.clone());
161                continue;
162            }
163
164            // Find candidate packages for this requirement
165            let candidates = self.find_candidate_packages(&requirement).await?;
166            state.packages_considered += candidates.len();
167
168            if candidates.is_empty() {
169                state.failed_requirements.push(requirement.clone());
170                continue;
171            }
172
173            // Try each candidate
174            let mut resolved = false;
175            for candidate in candidates {
176                // Check for conflicts with existing packages
177                if let Some(conflict) = state.check_conflicts(&candidate, &requirement) {
178                    state.conflicts.push(conflict);
179                    continue;
180                }
181
182                // Try to resolve with this candidate
183                if let Ok(resolved_info) = self
184                    .try_resolve_with_candidate(state, &candidate, &requirement)
185                    .await
186                {
187                    state.add_resolved_package(resolved_info);
188                    resolved = true;
189                    break;
190                }
191            }
192
193            if !resolved {
194                state.failed_requirements.push(requirement);
195            }
196        }
197
198        Ok(state.resolved_packages.clone())
199    }
200
201    /// Find candidate packages that could satisfy a requirement
202    async fn find_candidate_packages(
203        &mut self,
204        requirement: &Requirement,
205    ) -> Result<Vec<Arc<Package>>, RezCoreError> {
206        // Check cache first
207        if let Some(cached) = self.package_cache.get(&requirement.name) {
208            return Ok(self.filter_and_sort_candidates(cached, requirement));
209        }
210
211        // Search repositories
212        let packages = self
213            .repository_manager
214            .find_packages(&requirement.name)
215            .await?;
216
217        // Cache the results
218        self.package_cache
219            .insert(requirement.name.clone(), packages.clone());
220
221        Ok(self.filter_and_sort_candidates(&packages, requirement))
222    }
223
224    /// Filter candidate packages based on version constraints and sort them
225    fn filter_and_sort_candidates(
226        &self,
227        packages: &[Arc<Package>],
228        requirement: &Requirement,
229    ) -> Vec<Arc<Package>> {
230        let mut candidates: Vec<Arc<Package>> = packages
231            .iter()
232            .filter(|pkg| {
233                if let Some(ref version) = pkg.version {
234                    // Respect allow_prerelease flag
235                    if !self.config.allow_prerelease && version.is_prerelease() {
236                        return false;
237                    }
238                    requirement.is_satisfied_by(version)
239                } else {
240                    true
241                }
242            })
243            .cloned()
244            .collect();
245
246        // Sort by version: latest first (prefer latest behavior)
247        if self.config.prefer_latest {
248            candidates.sort_by(|a, b| match (&b.version, &a.version) {
249                (Some(bv), Some(av)) => bv.cmp(av),
250                (Some(_), None) => std::cmp::Ordering::Less,
251                (None, Some(_)) => std::cmp::Ordering::Greater,
252                (None, None) => std::cmp::Ordering::Equal,
253            });
254        } else {
255            // oldest first
256            candidates.sort_by(|a, b| match (&a.version, &b.version) {
257                (Some(av), Some(bv)) => av.cmp(bv),
258                (Some(_), None) => std::cmp::Ordering::Less,
259                (None, Some(_)) => std::cmp::Ordering::Greater,
260                (None, None) => std::cmp::Ordering::Equal,
261            });
262        }
263
264        candidates
265    }
266
267    /// Try to resolve using a specific candidate package
268    async fn try_resolve_with_candidate(
269        &mut self,
270        state: &mut ResolutionState,
271        candidate: &Arc<Package>,
272        requirement: &Requirement,
273    ) -> Result<ResolvedPackageInfo, RezCoreError> {
274        // Determine which variant to use (if package has variants)
275        let variant_index = self.select_variant(candidate, state);
276        state.variants_evaluated += candidate.variants.len().max(1);
277
278        // Get the effective requires list (base + variant-specific)
279        let effective_requires = self.get_effective_requires(candidate, variant_index);
280
281        // Create resolved package info
282        let resolved_info = ResolvedPackageInfo {
283            package: candidate.clone(),
284            variant_index,
285            requested: state.is_original_requirement(requirement),
286            required_by: vec![requirement.name.clone()],
287            satisfying_requirement: Some(requirement.clone()),
288        };
289
290        // Add transitive dependencies to resolution queue
291        for dep_req_str in &effective_requires {
292            let dep_requirement: Requirement = dep_req_str.parse().map_err(|e| {
293                RezCoreError::RequirementParse(format!(
294                    "Invalid requirement '{}': {}",
295                    dep_req_str, e
296                ))
297            })?;
298            // Record the dependency edge for cycle detection
299            state.record_dependency(&candidate.name, &dep_requirement.name);
300            state.add_requirement(dep_requirement);
301        }
302
303        Ok(resolved_info)
304    }
305
306    /// Select the best variant for a package given current resolution state
307    fn select_variant(&self, package: &Package, state: &ResolutionState) -> Option<usize> {
308        if package.variants.is_empty() {
309            return None;
310        }
311
312        // Try each variant, pick the first one that doesn't conflict with resolved packages
313        for (i, variant_requires) in package.variants.iter().enumerate() {
314            let mut compatible = true;
315            for req_str in variant_requires {
316                if let Ok(req) = req_str.parse::<Requirement>() {
317                    // Check if this variant requirement conflicts with already resolved packages
318                    for resolved in &state.resolved_packages {
319                        if resolved.package.name == req.name {
320                            if let Some(ref version) = resolved.package.version {
321                                if !req.is_satisfied_by(version) {
322                                    compatible = false;
323                                    break;
324                                }
325                            }
326                        }
327                    }
328                }
329                if !compatible {
330                    break;
331                }
332            }
333            if compatible {
334                return Some(i);
335            }
336        }
337
338        // Fall back to first variant
339        Some(0)
340    }
341
342    /// Get the effective requires list, merging base requires with variant requires
343    fn get_effective_requires(
344        &self,
345        package: &Package,
346        variant_index: Option<usize>,
347    ) -> Vec<String> {
348        let mut requires = package.requires.clone();
349
350        if let Some(idx) = variant_index {
351            if let Some(variant_reqs) = package.variants.get(idx) {
352                // Variant requires are appended to base requires
353                for vreq in variant_reqs {
354                    if !requires.contains(vreq) {
355                        requires.push(vreq.clone());
356                    }
357                }
358            }
359        }
360
361        requires
362    }
363}