1use std::collections::{HashMap, HashSet, VecDeque};
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use tracing::{debug, error, warn};
8
9use crate::dependency::{Dependency, DependencyTag};
10use crate::error::{Result, SpsError};
11use crate::formulary::Formulary;
12use crate::keg::KegRegistry;
13use crate::model::formula::Formula;
14
15#[derive(Debug, Clone)]
16pub struct ResolvedDependency {
17 pub formula: Arc<Formula>,
18 pub keg_path: Option<PathBuf>,
19 pub opt_path: Option<PathBuf>,
20 pub status: ResolutionStatus,
21 pub tags: DependencyTag,
22 pub failure_reason: Option<String>,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ResolutionStatus {
27 Installed,
28 Missing,
29 Requested,
30 SkippedOptional,
31 NotFound,
32 Failed,
33}
34
35#[derive(Debug, Clone)]
36pub struct ResolvedGraph {
37 pub install_plan: Vec<ResolvedDependency>,
38 pub build_dependency_opt_paths: Vec<PathBuf>,
39 pub runtime_dependency_opt_paths: Vec<PathBuf>,
40 pub resolution_details: HashMap<String, ResolvedDependency>,
41}
42
43pub struct ResolutionContext<'a> {
44 pub formulary: &'a Formulary,
45 pub keg_registry: &'a KegRegistry,
46 pub sps_prefix: &'a Path,
47 pub include_optional: bool,
48 pub include_test: bool,
49 pub skip_recommended: bool,
50 pub force_build: bool,
51}
52
53pub struct DependencyResolver<'a> {
54 context: ResolutionContext<'a>,
55 formula_cache: HashMap<String, Arc<Formula>>,
56 visiting: HashSet<String>,
57 resolution_details: HashMap<String, ResolvedDependency>,
58 errors: HashMap<String, Arc<SpsError>>,
60}
61
62impl<'a> DependencyResolver<'a> {
63 pub fn new(context: ResolutionContext<'a>) -> Self {
64 Self {
65 context,
66 formula_cache: HashMap::new(),
67 visiting: HashSet::new(),
68 resolution_details: HashMap::new(),
69 errors: HashMap::new(),
70 }
71 }
72
73 pub fn resolve_targets(&mut self, targets: &[String]) -> Result<ResolvedGraph> {
74 debug!("Starting dependency resolution for targets: {:?}", targets);
75 self.visiting.clear();
76 self.resolution_details.clear();
77 self.errors.clear();
78
79 for target_name in targets {
80 if let Err(e) = self.resolve_recursive(target_name, DependencyTag::RUNTIME, true) {
81 self.errors.insert(target_name.clone(), Arc::new(e));
83 warn!(
84 "Resolution failed for target '{}', but continuing for others.",
85 target_name
86 );
87 }
88 }
89
90 debug!(
91 "Raw resolved map after initial pass: {:?}",
92 self.resolution_details
93 .iter()
94 .map(|(k, v)| (k.clone(), v.status, v.tags))
95 .collect::<Vec<_>>()
96 );
97
98 let sorted_list = match self.topological_sort() {
99 Ok(list) => list,
100 Err(e @ SpsError::DependencyError(_)) => {
101 error!("Topological sort failed due to dependency cycle: {}", e);
102 return Err(e);
103 }
104 Err(e) => {
105 error!("Topological sort failed: {}", e);
106 return Err(e);
107 }
108 };
109
110 let install_plan: Vec<ResolvedDependency> = sorted_list
111 .into_iter()
112 .filter(|dep| {
113 matches!(
114 dep.status,
115 ResolutionStatus::Missing | ResolutionStatus::Requested
116 )
117 })
118 .collect();
119
120 let mut build_paths = Vec::new();
121 let mut runtime_paths = Vec::new();
122 let mut seen_build_paths = HashSet::new();
123 let mut seen_runtime_paths = HashSet::new();
124
125 for dep in self.resolution_details.values() {
126 if matches!(
127 dep.status,
128 ResolutionStatus::Installed
129 | ResolutionStatus::Requested
130 | ResolutionStatus::Missing
131 ) {
132 if let Some(opt_path) = &dep.opt_path {
133 if dep.tags.contains(DependencyTag::BUILD)
134 && seen_build_paths.insert(opt_path.clone())
135 {
136 debug!("Adding build dep path: {}", opt_path.display());
137 build_paths.push(opt_path.clone());
138 }
139 if dep.tags.intersects(
140 DependencyTag::RUNTIME
141 | DependencyTag::RECOMMENDED
142 | DependencyTag::OPTIONAL,
143 ) && seen_runtime_paths.insert(opt_path.clone())
144 {
145 debug!("Adding runtime dep path: {}", opt_path.display());
146 runtime_paths.push(opt_path.clone());
147 }
148 } else if dep.status != ResolutionStatus::NotFound
149 && dep.status != ResolutionStatus::Failed
150 {
151 debug!(
152 "Warning: No opt_path found for resolved dependency {} ({:?})",
153 dep.formula.name(),
154 dep.status
155 );
156 }
157 }
158 }
159
160 if !self.errors.is_empty() {
161 warn!(
162 "Resolution encountered errors for specific targets: {:?}",
163 self.errors
164 .iter()
165 .map(|(k, v)| (k, v.to_string()))
166 .collect::<HashMap<_, _>>()
167 );
168 }
169
170 debug!(
171 "Final installation plan (needs install/build): {:?}",
172 install_plan
173 .iter()
174 .map(|d| (d.formula.name(), d.status))
175 .collect::<Vec<_>>()
176 );
177 debug!(
178 "Collected build dependency paths: {:?}",
179 build_paths.iter().map(|p| p.display()).collect::<Vec<_>>()
180 );
181 debug!(
182 "Collected runtime dependency paths: {:?}",
183 runtime_paths
184 .iter()
185 .map(|p| p.display())
186 .collect::<Vec<_>>()
187 );
188
189 Ok(ResolvedGraph {
190 install_plan,
191 build_dependency_opt_paths: build_paths,
192 runtime_dependency_opt_paths: runtime_paths,
193 resolution_details: self.resolution_details.clone(),
194 })
195 }
196
197 fn resolve_recursive(
199 &mut self,
200 name: &str,
201 tags_from_parent: DependencyTag,
202 is_target: bool,
203 ) -> Result<()> {
204 debug!(
205 "Resolving: {} (requested as {:?}, is_target: {})",
206 name, tags_from_parent, is_target
207 );
208
209 if self.visiting.contains(name) {
211 error!("Dependency cycle detected involving: {}", name);
212 return Err(SpsError::DependencyError(format!(
213 "Dependency cycle detected involving '{name}'"
214 )));
215 }
216
217 if let Some(existing) = self.resolution_details.get_mut(name) {
219 let original_status = existing.status;
220 let original_tags = existing.tags;
221
222 let mut new_status = original_status;
224 if is_target && new_status == ResolutionStatus::Missing {
225 new_status = ResolutionStatus::Requested;
226 }
227 if new_status == ResolutionStatus::SkippedOptional
228 && (tags_from_parent.contains(DependencyTag::RUNTIME)
229 || tags_from_parent.contains(DependencyTag::BUILD)
230 || (tags_from_parent.contains(DependencyTag::RECOMMENDED)
231 && !self.context.skip_recommended)
232 || (is_target && self.context.include_optional))
233 {
234 new_status = if existing.keg_path.is_some() {
235 ResolutionStatus::Installed
236 } else if is_target {
237 ResolutionStatus::Requested
238 } else {
239 ResolutionStatus::Missing
240 };
241 }
242
243 let mut needs_revisit = false;
245 if new_status != original_status {
246 debug!(
247 "Updating status for '{name}' from {:?} to {:?}",
248 original_status, new_status
249 );
250 existing.status = new_status;
251 needs_revisit = true;
252 }
253
254 let combined_tags = original_tags | tags_from_parent;
255 if combined_tags != original_tags {
256 debug!(
257 "Updating tags for '{name}' from {:?} to {:?}",
258 original_tags, combined_tags
259 );
260 existing.tags = combined_tags;
261 needs_revisit = true;
262 }
263
264 if !needs_revisit {
266 debug!("'{}' already resolved with compatible status/tags.", name);
267 return Ok(());
268 }
269
270 debug!(
271 "Re-evaluating dependencies for '{}' due to status/tag update",
272 name
273 );
274 }
275 else {
277 self.visiting.insert(name.to_string());
278
279 let formula: Arc<Formula> = match self.formula_cache.get(name) {
281 Some(f) => f.clone(),
282 None => {
283 debug!("Loading formula definition for '{}'", name);
284 match self.context.formulary.load_formula(name) {
285 Ok(f) => {
286 let arc = Arc::new(f);
287 self.formula_cache.insert(name.to_string(), arc.clone());
288 arc
289 }
290 Err(e) => {
291 error!("Failed to load formula definition for '{}': {}", name, e);
292
293 let msg = e.to_string();
294 self.resolution_details.insert(
295 name.to_string(),
296 ResolvedDependency {
297 formula: Arc::new(Formula::placeholder(name)),
298 keg_path: None,
299 opt_path: None,
300 status: ResolutionStatus::NotFound,
301 tags: tags_from_parent,
302 failure_reason: Some(msg.clone()),
303 },
304 );
305 self.visiting.remove(name);
306
307 self.errors
308 .insert(name.to_string(), Arc::new(SpsError::NotFound(msg)));
309
310 return Ok(()); }
312 }
313 }
314 };
315
316 let installed_keg = if self.context.force_build {
318 None
319 } else {
320 self.context.keg_registry.get_installed_keg(name)?
321 };
322 let opt_path = self.context.keg_registry.get_opt_path(name);
323
324 let (status, keg_path) = match installed_keg {
325 Some(keg) => (ResolutionStatus::Installed, Some(keg.path)),
326 None => (
327 if is_target {
328 ResolutionStatus::Requested
329 } else {
330 ResolutionStatus::Missing
331 },
332 None,
333 ),
334 };
335
336 debug!(
337 "Initial status for '{}': {:?}, keg: {:?}, opt: {}",
338 name,
339 status,
340 keg_path,
341 opt_path.display()
342 );
343
344 self.resolution_details.insert(
345 name.to_string(),
346 ResolvedDependency {
347 formula,
348 keg_path,
349 opt_path: Some(opt_path),
350 status,
351 tags: tags_from_parent,
352 failure_reason: None,
353 },
354 );
355 }
356
357 let dep_snapshot = self
359 .resolution_details
360 .get(name)
361 .expect("just inserted")
362 .clone();
363
364 if matches!(
366 dep_snapshot.status,
367 ResolutionStatus::Failed | ResolutionStatus::NotFound
368 ) {
369 self.visiting.remove(name);
370 return Ok(());
371 }
372
373 for dep in dep_snapshot.formula.dependencies()? {
375 let dep_name = &dep.name;
376 let dep_tags = dep.tags;
377
378 debug!(
379 "Processing dependency '{}' for '{}' with tags {:?}",
380 dep_name, name, dep_tags
381 );
382
383 if !self.should_consider_dependency(&dep) {
385 if !self.resolution_details.contains_key(dep_name.as_str()) {
386 debug!("Marking '{}' as SkippedOptional", dep_name);
387
388 if let Ok(f) = self.context.formulary.load_formula(dep_name) {
389 let arc = Arc::new(f);
390 let opt = self.context.keg_registry.get_opt_path(dep_name);
391
392 self.formula_cache.insert(dep_name.to_string(), arc.clone());
393 self.resolution_details.insert(
394 dep_name.to_string(),
395 ResolvedDependency {
396 formula: arc,
397 keg_path: None,
398 opt_path: Some(opt),
399 status: ResolutionStatus::SkippedOptional,
400 tags: dep_tags,
401 failure_reason: None,
402 },
403 );
404 }
405 }
406 continue;
407 }
408
409 if let Err(e) = self.resolve_recursive(dep_name, dep_tags, false) {
411 warn!(
412 "Recursive resolution for '{}' (child of '{}') failed: {}",
413 dep_name, name, e
414 );
415
416 let is_cycle = matches!(e, SpsError::DependencyError(_));
418 let msg = e.to_string();
419
420 self.errors
422 .entry(dep_name.to_string())
423 .or_insert_with(|| Arc::new(e));
424
425 if let Some(node) = self.resolution_details.get_mut(dep_name.as_str()) {
427 node.status = ResolutionStatus::Failed;
428 node.failure_reason = Some(msg);
429 }
430
431 if is_cycle {
433 self.visiting.remove(name);
434 return Err(SpsError::DependencyError(
435 "Circular dependency detected".into(),
436 ));
437 }
438 }
439 }
440
441 self.visiting.remove(name);
442 debug!("Finished resolving '{}'", name);
443 Ok(())
444 }
445
446 fn topological_sort(&self) -> Result<Vec<ResolvedDependency>> {
447 debug!("Starting topological sort");
448 let mut in_degree: HashMap<String, usize> = HashMap::new();
449 let mut adj: HashMap<String, HashSet<String>> = HashMap::new();
450 let mut sorted_list = Vec::new();
451 let mut queue = VecDeque::new();
452
453 let relevant_nodes: Vec<_> = self
454 .resolution_details
455 .iter()
456 .filter(|(_, dep)| {
457 matches!(
458 dep.status,
459 ResolutionStatus::Installed
460 | ResolutionStatus::Missing
461 | ResolutionStatus::Requested
462 )
463 })
464 .map(|(name, _)| name.clone())
465 .collect();
466
467 for name in &relevant_nodes {
468 in_degree.entry(name.clone()).or_insert(0);
469 adj.entry(name.clone()).or_default();
470 }
471
472 for name in &relevant_nodes {
473 let resolved_dep = self.resolution_details.get(name).unwrap();
474 match resolved_dep.formula.dependencies() {
475 Ok(dependencies) => {
476 for dep in dependencies {
477 if relevant_nodes.contains(&dep.name)
478 && self.should_consider_dependency(&dep)
479 && adj
480 .entry(dep.name.clone())
481 .or_default()
482 .insert(name.clone())
483 {
484 *in_degree.entry(name.clone()).or_insert(0) += 1;
485 }
486 }
487 }
488 Err(e) => {
489 error!(
490 "Failed to get dependencies for '{}' during sort: {}",
491 name, e
492 );
493 return Err(e);
494 }
495 }
496 }
497
498 debug!("In-degrees (relevant nodes only): {:?}", in_degree);
499
500 for name in &relevant_nodes {
501 if *in_degree.get(name).unwrap_or(&1) == 0 {
502 queue.push_back(name.clone());
503 }
504 }
505
506 debug!("Initial queue: {:?}", queue);
507
508 while let Some(u_name) = queue.pop_front() {
509 if let Some(resolved_dep) = self.resolution_details.get(&u_name) {
510 if matches!(
511 resolved_dep.status,
512 ResolutionStatus::Installed
513 | ResolutionStatus::Missing
514 | ResolutionStatus::Requested
515 ) {
516 sorted_list.push(resolved_dep.clone());
517 }
518 } else {
519 error!(
520 "Error: Node '{}' from queue not found in resolved map!",
521 u_name
522 );
523 return Err(SpsError::Generic(format!(
524 "Topological sort inconsistency: node {u_name} not found"
525 )));
526 }
527
528 if let Some(neighbors) = adj.get(&u_name) {
529 for v_name in neighbors {
530 if relevant_nodes.contains(v_name) {
531 if let Some(degree) = in_degree.get_mut(v_name) {
532 *degree = degree.saturating_sub(1);
533 if *degree == 0 {
534 queue.push_back(v_name.clone());
535 }
536 }
537 }
538 }
539 }
540 }
541
542 if sorted_list.len() != relevant_nodes.len() {
543 error!(
544 "Cycle detected! Sorted count ({}) != Relevant node count ({}).",
545 sorted_list.len(),
546 relevant_nodes.len()
547 );
548 let cyclic_nodes: Vec<_> = relevant_nodes
549 .iter()
550 .filter(|n| in_degree.get(*n).unwrap_or(&0) > &0)
551 .cloned()
552 .collect();
553 error!(
554 "Nodes potentially involved in cycle (relevant, in-degree > 0): {:?}",
555 cyclic_nodes
556 );
557 return Err(SpsError::DependencyError(
558 "Circular dependency detected".to_string(),
559 ));
560 }
561
562 debug!(
563 "Topological sort successful. {} relevant nodes in sorted list.",
564 sorted_list.len()
565 );
566 Ok(sorted_list)
567 }
568
569 fn should_consider_dependency(&self, dep: &Dependency) -> bool {
570 let tags = dep.tags;
571 if tags.contains(DependencyTag::TEST) && !self.context.include_test {
572 return false;
573 }
574 if tags.contains(DependencyTag::OPTIONAL) && !self.context.include_optional {
575 return false;
576 }
577 if tags.contains(DependencyTag::RECOMMENDED) && self.context.skip_recommended {
578 return false;
579 }
580 true
581 }
582}
583
584impl Formula {
585 fn placeholder(name: &str) -> Self {
586 Self {
587 name: name.to_string(),
588 stable_version_str: "0.0.0".to_string(),
589 version_semver: semver::Version::new(0, 0, 0),
590 revision: 0,
591 desc: Some("Placeholder for unresolved formula".to_string()),
592 homepage: None,
593 url: String::new(),
594 sha256: String::new(),
595 mirrors: Vec::new(),
596 bottle: Default::default(),
597 dependencies: Vec::new(),
598 requirements: Vec::new(),
599 resources: Vec::new(),
600 install_keg_path: None,
601 }
602 }
603}