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, Copy, PartialEq, Eq)]
17pub enum NodeInstallStrategy {
18 BottlePreferred,
19 SourceOnly,
20 BottleOrFail,
21}
22
23#[derive(Debug, Clone, Default)]
25pub struct PerTargetInstallPreferences {
26 pub force_source_build_targets: HashSet<String>,
27 pub force_bottle_only_targets: HashSet<String>,
28}
29
30pub struct ResolutionContext<'a> {
32 pub formulary: &'a Formulary,
33 pub keg_registry: &'a KegRegistry,
34 pub sps_prefix: &'a Path,
35 pub include_optional: bool,
36 pub include_test: bool,
37 pub skip_recommended: bool,
38 pub initial_target_preferences: &'a PerTargetInstallPreferences,
39 pub build_all_from_source: bool,
40 pub cascade_source_preference_to_dependencies: bool,
41 pub has_bottle_for_current_platform: fn(&Formula) -> bool,
43}
44
45#[derive(Debug, Clone)]
47pub struct ResolvedDependency {
48 pub formula: Arc<Formula>,
49 pub keg_path: Option<PathBuf>,
50 pub opt_path: Option<PathBuf>,
51 pub status: ResolutionStatus,
52 pub accumulated_tags: DependencyTag,
53 pub determined_install_strategy: NodeInstallStrategy,
54 pub failure_reason: Option<String>,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum ResolutionStatus {
59 Installed,
60 Missing,
61 Requested,
62 SkippedOptional,
63 NotFound,
64 Failed,
65}
66
67#[derive(Debug, Clone)]
68pub struct ResolvedGraph {
69 pub install_plan: Vec<ResolvedDependency>,
70 pub build_dependency_opt_paths: Vec<PathBuf>,
71 pub runtime_dependency_opt_paths: Vec<PathBuf>,
72 pub resolution_details: HashMap<String, ResolvedDependency>,
73}
74
75pub struct DependencyResolver<'a> {
76 context: ResolutionContext<'a>,
77 formula_cache: HashMap<String, Arc<Formula>>,
78 visiting: HashSet<String>,
79 resolution_details: HashMap<String, ResolvedDependency>,
80 errors: HashMap<String, Arc<SpsError>>,
82}
83
84impl<'a> DependencyResolver<'a> {
85 pub fn new(context: ResolutionContext<'a>) -> Self {
86 Self {
87 context,
88 formula_cache: HashMap::new(),
89 visiting: HashSet::new(),
90 resolution_details: HashMap::new(),
91 errors: HashMap::new(),
92 }
93 }
94
95 fn determine_node_install_strategy(
96 &self,
97 formula_name: &str,
98 formula_arc: &Arc<Formula>,
99 is_initial_target: bool,
100 requesting_parent_strategy: Option<NodeInstallStrategy>,
101 ) -> NodeInstallStrategy {
102 if is_initial_target {
104 if self
105 .context
106 .initial_target_preferences
107 .force_source_build_targets
108 .contains(formula_name)
109 {
110 return NodeInstallStrategy::SourceOnly;
111 }
112 if self
113 .context
114 .initial_target_preferences
115 .force_bottle_only_targets
116 .contains(formula_name)
117 {
118 return NodeInstallStrategy::BottleOrFail;
119 }
120 }
121
122 if self.context.build_all_from_source {
124 return NodeInstallStrategy::SourceOnly;
125 }
126
127 if self.context.cascade_source_preference_to_dependencies
129 && matches!(
130 requesting_parent_strategy,
131 Some(NodeInstallStrategy::SourceOnly)
132 )
133 {
134 return NodeInstallStrategy::SourceOnly;
135 }
136 if matches!(
137 requesting_parent_strategy,
138 Some(NodeInstallStrategy::BottleOrFail)
139 ) {
140 return NodeInstallStrategy::BottleOrFail;
141 }
142
143 let strategy = if (self.context.has_bottle_for_current_platform)(formula_arc) {
145 NodeInstallStrategy::BottlePreferred
146 } else {
147 NodeInstallStrategy::SourceOnly
148 };
149
150 debug!(
151 "Install strategy for '{formula_name}': {:?} (initial_target={is_initial_target}, parent={:?}, bottle_available={})",
152 strategy,
153 requesting_parent_strategy,
154 (self.context.has_bottle_for_current_platform)(formula_arc)
155 );
156 strategy
157 }
158
159 pub fn resolve_targets(&mut self, targets: &[String]) -> Result<ResolvedGraph> {
160 debug!("Starting dependency resolution for targets: {:?}", targets);
161 self.visiting.clear();
162 self.resolution_details.clear();
163 self.errors.clear();
164
165 for target_name in targets {
166 if let Err(e) = self.resolve_recursive(target_name, DependencyTag::RUNTIME, true, None)
167 {
168 self.errors.insert(target_name.clone(), Arc::new(e));
169 warn!(
170 "Resolution failed for target '{}', but continuing for others.",
171 target_name
172 );
173 }
174 }
175
176 debug!(
177 "Raw resolved map after initial pass: {:?}",
178 self.resolution_details
179 .iter()
180 .map(|(k, v)| (k.clone(), v.status, v.accumulated_tags))
181 .collect::<Vec<_>>()
182 );
183
184 let sorted_list = match self.topological_sort() {
185 Ok(list) => list,
186 Err(e @ SpsError::DependencyError(_)) => {
187 error!("Topological sort failed due to dependency cycle: {}", e);
188 return Err(e);
189 }
190 Err(e) => {
191 error!("Topological sort failed: {}", e);
192 return Err(e);
193 }
194 };
195
196 let install_plan: Vec<ResolvedDependency> = sorted_list
197 .into_iter()
198 .filter(|dep| {
199 matches!(
200 dep.status,
201 ResolutionStatus::Missing | ResolutionStatus::Requested
202 )
203 })
204 .collect();
205
206 let mut build_paths = Vec::new();
207 let mut runtime_paths = Vec::new();
208 let mut seen_build_paths = HashSet::new();
209 let mut seen_runtime_paths = HashSet::new();
210
211 for dep in self.resolution_details.values() {
212 if matches!(
213 dep.status,
214 ResolutionStatus::Installed
215 | ResolutionStatus::Requested
216 | ResolutionStatus::Missing
217 ) {
218 if let Some(opt_path) = &dep.opt_path {
219 if dep.accumulated_tags.contains(DependencyTag::BUILD)
220 && seen_build_paths.insert(opt_path.clone())
221 {
222 debug!("Adding build dep path: {}", opt_path.display());
223 build_paths.push(opt_path.clone());
224 }
225 if dep.accumulated_tags.intersects(
226 DependencyTag::RUNTIME
227 | DependencyTag::RECOMMENDED
228 | DependencyTag::OPTIONAL,
229 ) && seen_runtime_paths.insert(opt_path.clone())
230 {
231 debug!("Adding runtime dep path: {}", opt_path.display());
232 runtime_paths.push(opt_path.clone());
233 }
234 } else if dep.status != ResolutionStatus::NotFound
235 && dep.status != ResolutionStatus::Failed
236 {
237 debug!(
238 "Warning: No opt_path found for resolved dependency {} ({:?})",
239 dep.formula.name(),
240 dep.status
241 );
242 }
243 }
244 }
245
246 if !self.errors.is_empty() {
247 warn!(
248 "Resolution encountered errors for specific targets: {:?}",
249 self.errors
250 .iter()
251 .map(|(k, v)| (k, v.to_string()))
252 .collect::<HashMap<_, _>>()
253 );
254 }
255
256 debug!(
257 "Final installation plan (needs install/build): {:?}",
258 install_plan
259 .iter()
260 .map(|d| (d.formula.name(), d.status))
261 .collect::<Vec<_>>()
262 );
263 debug!(
264 "Collected build dependency paths: {:?}",
265 build_paths.iter().map(|p| p.display()).collect::<Vec<_>>()
266 );
267 debug!(
268 "Collected runtime dependency paths: {:?}",
269 runtime_paths
270 .iter()
271 .map(|p| p.display())
272 .collect::<Vec<_>>()
273 );
274
275 Ok(ResolvedGraph {
276 install_plan,
277 build_dependency_opt_paths: build_paths,
278 runtime_dependency_opt_paths: runtime_paths,
279 resolution_details: self.resolution_details.clone(),
280 })
281 }
282
283 fn resolve_recursive(
285 &mut self,
286 name: &str,
287 tags_from_parent_edge: DependencyTag,
288 is_initial_target: bool,
289 requesting_parent_strategy: Option<NodeInstallStrategy>,
290 ) -> Result<()> {
291 debug!(
292 "Resolving: {} (requested as {:?}, is_target: {})",
293 name, tags_from_parent_edge, is_initial_target
294 );
295
296 if self.visiting.contains(name) {
298 error!("Dependency cycle detected involving: {}", name);
299 return Err(SpsError::DependencyError(format!(
300 "Dependency cycle detected involving '{name}'"
301 )));
302 }
303
304 if let Some(existing) = self.resolution_details.get_mut(name) {
306 let original_status = existing.status;
321 let original_tags = existing.accumulated_tags;
322
323 let mut new_status = original_status;
324 if is_initial_target && new_status == ResolutionStatus::Missing {
325 new_status = ResolutionStatus::Requested;
326 }
327 if new_status == ResolutionStatus::SkippedOptional
328 && (tags_from_parent_edge.contains(DependencyTag::RUNTIME)
329 || tags_from_parent_edge.contains(DependencyTag::BUILD)
330 || (tags_from_parent_edge.contains(DependencyTag::RECOMMENDED)
331 && !self.context.skip_recommended)
332 || (is_initial_target && self.context.include_optional))
333 {
334 new_status = if existing.keg_path.is_some() {
335 ResolutionStatus::Installed
336 } else if is_initial_target {
337 ResolutionStatus::Requested
338 } else {
339 ResolutionStatus::Missing
340 };
341 }
342
343 let mut needs_revisit = false;
345 if new_status != original_status {
346 debug!(
347 "Updating status for '{name}' from {:?} to {:?}",
348 original_status, new_status
349 );
350 existing.status = new_status;
351 needs_revisit = true;
352 }
353
354 let combined_tags = original_tags | tags_from_parent_edge;
355 if combined_tags != original_tags {
356 debug!(
357 "Updating tags for '{name}' from {:?} to {:?}",
358 original_tags, combined_tags
359 );
360 existing.accumulated_tags = combined_tags;
361 needs_revisit = true;
362 }
363
364 if !needs_revisit {
366 debug!("'{}' already resolved with compatible status/tags.", name);
367 return Ok(());
368 }
369
370 debug!(
371 "Re-evaluating dependencies for '{}' due to status/tag update",
372 name
373 );
374 }
375 else {
377 self.visiting.insert(name.to_string());
378
379 let formula_arc = match self.formula_cache.get(name) {
381 Some(f) => f.clone(),
382 None => {
383 debug!("Loading formula definition for '{}'", name);
384 match self.context.formulary.load_formula(name) {
385 Ok(f) => {
386 let arc = Arc::new(f);
387 self.formula_cache.insert(name.to_string(), arc.clone());
388 arc
389 }
390 Err(e) => {
391 error!("Failed to load formula definition for '{}': {}", name, e);
392 let msg = e.to_string();
393 self.resolution_details.insert(
394 name.to_string(),
395 ResolvedDependency {
396 formula: Arc::new(Formula::placeholder(name)),
397 keg_path: None,
398 opt_path: None,
399 status: ResolutionStatus::NotFound,
400 accumulated_tags: tags_from_parent_edge,
401 determined_install_strategy:
402 NodeInstallStrategy::BottlePreferred,
403 failure_reason: Some(msg.clone()),
404 },
405 );
406 self.visiting.remove(name);
407 self.errors
408 .insert(name.to_string(), Arc::new(SpsError::NotFound(msg)));
409 return Ok(()); }
411 }
412 }
413 };
414
415 let current_node_strategy = self.determine_node_install_strategy(
416 name,
417 &formula_arc,
418 is_initial_target,
419 requesting_parent_strategy,
420 );
421
422 let (status, keg_path) = match current_node_strategy {
423 NodeInstallStrategy::SourceOnly => (
424 if is_initial_target {
425 ResolutionStatus::Requested
426 } else {
427 ResolutionStatus::Missing
428 },
429 None,
430 ),
431 NodeInstallStrategy::BottlePreferred | NodeInstallStrategy::BottleOrFail => {
432 if let Some(keg) = self.context.keg_registry.get_installed_keg(name)? {
433 (ResolutionStatus::Installed, Some(keg.path))
434 } else {
435 (
436 if is_initial_target {
437 ResolutionStatus::Requested
438 } else {
439 ResolutionStatus::Missing
440 },
441 None,
442 )
443 }
444 }
445 };
446
447 debug!(
448 "Initial status for '{}': {:?}, keg: {:?}, opt: {}",
449 name,
450 status,
451 keg_path,
452 self.context.keg_registry.get_opt_path(name).display()
453 );
454
455 self.resolution_details.insert(
456 name.to_string(),
457 ResolvedDependency {
458 formula: formula_arc.clone(),
459 keg_path,
460 opt_path: Some(self.context.keg_registry.get_opt_path(name)),
461 status,
462 accumulated_tags: tags_from_parent_edge,
463 determined_install_strategy: current_node_strategy,
464 failure_reason: None,
465 },
466 );
467 }
468
469 let dep_snapshot = self
471 .resolution_details
472 .get(name)
473 .expect("just inserted")
474 .clone();
475
476 if matches!(
478 dep_snapshot.status,
479 ResolutionStatus::Failed | ResolutionStatus::NotFound
480 ) {
481 self.visiting.remove(name);
482 return Ok(());
483 }
484
485 for dep in dep_snapshot.formula.dependencies()? {
487 let dep_name = &dep.name;
488 let dep_tags = dep.tags;
489 let parent_name = dep_snapshot.formula.name();
490 let parent_strategy = dep_snapshot.determined_install_strategy;
491
492 debug!(
493 "RESOLVER: Evaluating edge: parent='{}' ({:?}), child='{}' ({:?})",
494 parent_name, parent_strategy, dep_name, dep_tags
495 );
496
497 if !self.should_consider_dependency(&dep) {
499 if !self.resolution_details.contains_key(dep_name.as_str()) {
500 debug!("RESOLVER: Child '{}' of '{}' globally SKIPPED (e.g. optional/test not included). Tags: {:?}", dep_name, parent_name, dep_tags);
501 }
503 continue;
504 }
505
506 let should_process = self.context.should_process_dependency_edge(
508 &dep_snapshot.formula,
509 dep_tags,
510 parent_strategy,
511 );
512
513 if !should_process {
514 debug!(
515 "RESOLVER: Edge from '{}' (Strategy: {:?}) to child '{}' (Tags: {:?}) was SKIPPED by should_process_dependency_edge.",
516 parent_name, parent_strategy, dep_name, dep_tags
517 );
518 continue;
520 }
521
522 debug!(
523 "RESOLVER: Edge from '{}' (Strategy: {:?}) to child '{}' (Tags: {:?}) WILL BE PROCESSED. Recursing.",
524 parent_name, parent_strategy, dep_name, dep_tags
525 );
526
527 if let Err(_e) =
529 self.resolve_recursive(dep_name, dep_tags, false, Some(parent_strategy))
530 {
531 }
533 }
534
535 self.visiting.remove(name);
536 debug!("Finished resolving '{}'", name);
537 Ok(())
538 }
539
540 fn topological_sort(&self) -> Result<Vec<ResolvedDependency>> {
541 let mut in_degree: HashMap<String, usize> = HashMap::new();
542 let mut adj: HashMap<String, HashSet<String>> = HashMap::new();
543 let mut sorted_list = Vec::new();
544 let mut queue = VecDeque::new();
545
546 let relevant_nodes_map: HashMap<String, &ResolvedDependency> = self
547 .resolution_details
548 .iter()
549 .filter(|(_, dep)| {
550 !matches!(
551 dep.status,
552 ResolutionStatus::NotFound | ResolutionStatus::Failed
553 )
554 })
555 .map(|(k, v)| (k.clone(), v))
556 .collect();
557
558 for (parent_name, parent_rd) in &relevant_nodes_map {
559 adj.entry(parent_name.clone()).or_default();
560 in_degree.entry(parent_name.clone()).or_default();
561
562 let parent_strategy = parent_rd.determined_install_strategy;
563 if let Ok(dependencies) = parent_rd.formula.dependencies() {
564 for child_edge in dependencies {
565 let child_name = &child_edge.name;
566 if relevant_nodes_map.contains_key(child_name)
567 && self.context.should_process_dependency_edge(
568 &parent_rd.formula,
569 child_edge.tags,
570 parent_strategy,
571 )
572 && adj
573 .entry(parent_name.clone())
574 .or_default()
575 .insert(child_name.clone())
576 {
577 *in_degree.entry(child_name.clone()).or_default() += 1;
578 }
579 }
580 }
581 }
582
583 for name in relevant_nodes_map.keys() {
584 if *in_degree.get(name).unwrap_or(&1) == 0 {
585 queue.push_back(name.clone());
586 }
587 }
588
589 while let Some(u_name) = queue.pop_front() {
590 if let Some(resolved_dep) = relevant_nodes_map.get(&u_name) {
591 sorted_list.push((**resolved_dep).clone());
592 }
593 if let Some(neighbors) = adj.get(&u_name) {
594 for v_name in neighbors {
595 if relevant_nodes_map.contains_key(v_name) {
596 if let Some(degree) = in_degree.get_mut(v_name) {
597 *degree = degree.saturating_sub(1);
598 if *degree == 0 {
599 queue.push_back(v_name.clone());
600 }
601 }
602 }
603 }
604 }
605 }
606
607 let install_plan: Vec<ResolvedDependency> = sorted_list
608 .into_iter()
609 .filter(|dep| {
610 matches!(
611 dep.status,
612 ResolutionStatus::Missing | ResolutionStatus::Requested
613 )
614 })
615 .collect();
616
617 let expected_installable_count = relevant_nodes_map
618 .values()
619 .filter(|dep| {
620 matches!(
621 dep.status,
622 ResolutionStatus::Missing | ResolutionStatus::Requested
623 )
624 })
625 .count();
626
627 #[cfg(debug_assertions)]
628 {
629 use std::collections::HashMap;
630 let index_map: HashMap<&str, usize> = install_plan
632 .iter()
633 .enumerate()
634 .map(|(i, d)| (d.formula.name(), i))
635 .collect();
636
637 for (parent_name, parent_rd) in &relevant_nodes_map {
638 if let Ok(edges) = parent_rd.formula.dependencies() {
639 for edge in edges {
640 let child = &edge.name;
641 if relevant_nodes_map.contains_key(child)
642 && self.context.should_process_dependency_edge(
643 &parent_rd.formula,
644 edge.tags,
645 parent_rd.determined_install_strategy,
646 )
647 {
648 if let (Some(&p_idx), Some(&c_idx)) = (
649 index_map.get(parent_name.as_str()),
650 index_map.get(child.as_str()),
651 ) {
652 debug_assert!(
653 p_idx > c_idx,
654 "Topological order violation: parent '{parent_name}' appears before child '{child}'"
655 );
656 }
657 }
658 }
659 }
660 }
661 }
662
663 if install_plan.len() != expected_installable_count && expected_installable_count > 0 {
664 error!(
665 "Cycle detected! Sorted count ({}) != Relevant node count ({}).",
666 install_plan.len(),
667 expected_installable_count
668 );
669 return Err(SpsError::DependencyError(
670 "Circular dependency detected".to_string(),
671 ));
672 }
673 Ok(install_plan)
674 }
675
676 fn should_consider_dependency(&self, dep: &Dependency) -> bool {
677 let tags = dep.tags;
678 if tags.contains(DependencyTag::TEST) && !self.context.include_test {
679 return false;
680 }
681 if tags.contains(DependencyTag::OPTIONAL) && !self.context.include_optional {
682 return false;
683 }
684 if tags.contains(DependencyTag::RECOMMENDED) && self.context.skip_recommended {
685 return false;
686 }
687 true
688 }
689}
690
691impl Formula {
692 fn placeholder(name: &str) -> Self {
693 Self {
694 name: name.to_string(),
695 stable_version_str: "0.0.0".to_string(),
696 version_semver: semver::Version::new(0, 0, 0),
697 revision: 0,
698 desc: Some("Placeholder for unresolved formula".to_string()),
699 homepage: None,
700 url: String::new(),
701 sha256: String::new(),
702 mirrors: Vec::new(),
703 bottle: Default::default(),
704 dependencies: Vec::new(),
705 requirements: Vec::new(),
706 resources: Vec::new(),
707 install_keg_path: None,
708 }
709 }
710}
711
712impl<'a> ResolutionContext<'a> {
714 pub fn should_process_dependency_edge(
715 &self,
716 parent_formula_for_logging: &Arc<Formula>,
717 edge_tags: DependencyTag,
718 parent_node_determined_strategy: NodeInstallStrategy,
719 ) -> bool {
720 debug!(
721 "should_process_dependency_edge: Parent='{}', EdgeTags={:?}, ParentStrategy={:?}",
722 parent_formula_for_logging.name(),
723 edge_tags,
724 parent_node_determined_strategy
725 );
726
727 if edge_tags.contains(DependencyTag::TEST) && !self.include_test {
728 debug!(
729 "Edge with tags {:?} skipped: TEST dependencies excluded by context.",
730 edge_tags
731 );
732 return false;
733 }
734 if edge_tags.contains(DependencyTag::OPTIONAL) && !self.include_optional {
735 debug!(
736 "Edge with tags {:?} skipped: OPTIONAL dependencies excluded by context.",
737 edge_tags
738 );
739 return false;
740 }
741 if edge_tags.contains(DependencyTag::RECOMMENDED) && self.skip_recommended {
742 debug!(
743 "Edge with tags {:?} skipped: RECOMMENDED dependencies excluded by context.",
744 edge_tags
745 );
746 return false;
747 }
748 match parent_node_determined_strategy {
749 NodeInstallStrategy::BottlePreferred | NodeInstallStrategy::BottleOrFail => {
750 let is_purely_build_dependency = edge_tags.contains(DependencyTag::BUILD)
751 && !edge_tags.intersects(
752 DependencyTag::RUNTIME
753 | DependencyTag::RECOMMENDED
754 | DependencyTag::OPTIONAL,
755 );
756 debug!(
757 "Parent is bottled. For edge with tags {:?}, is_purely_build_dependency: {}",
758 edge_tags, is_purely_build_dependency
759 );
760 if is_purely_build_dependency {
761 debug!("Edge with tags {:?} SKIPPED: Pure BUILD dependency of a bottle-installed parent '{}'.", edge_tags, parent_formula_for_logging.name());
762 return false;
763 }
764 }
765 NodeInstallStrategy::SourceOnly => {
766 debug!("Parent is SourceOnly. Edge with tags {:?} will be processed (unless globally skipped).", edge_tags);
767 }
768 }
769 debug!(
770 "Edge with tags {:?} WILL BE PROCESSED for parent '{}' with strategy {:?}.",
771 edge_tags,
772 parent_formula_for_logging.name(),
773 parent_node_determined_strategy
774 );
775 true
776 }
777
778 pub fn should_consider_edge_globally(&self, edge_tags: DependencyTag) -> bool {
779 if edge_tags.contains(DependencyTag::TEST) && !self.include_test {
780 return false;
781 }
782 if edge_tags.contains(DependencyTag::OPTIONAL) && !self.include_optional {
783 return false;
784 }
785 if edge_tags.contains(DependencyTag::RECOMMENDED) && self.skip_recommended {
786 return false;
787 }
788 true
789 }
790}