1use std::collections::{BTreeMap, BTreeSet};
7use std::time::{Duration, Instant};
8
9use crate::deps::parse::parse_dep_spec;
10use crate::deps::srcinfo::{GraphSrcinfoData, SrcinfoPackage, parse_srcinfo_graph};
11use crate::deps::version::{compare_versions, version_satisfies};
12use crate::error::{ArchToolkitError, Result};
13use crate::types::dependency::{
14 DependencyConstraintRange, DependencyGraphConfig, DependencyGraphDiagnostic,
15 DependencyGraphDiagnosticKind, DependencyGraphEdge, DependencyGraphNode,
16 DependencyGraphNodeStatus, DependencyGraphResolution, DependencyMetadata,
17 DependencyMetadataResponse, DependencyProvenance, DependencyVersionBound, PackageRef,
18};
19
20pub trait DependencyMetadataProvider: Send + Sync {
34 fn fetch_metadata(
47 &self,
48 requested_names: &[String],
49 timeout: Duration,
50 ) -> Vec<DependencyMetadataResponse>;
51}
52
53#[derive(Clone, Debug)]
65struct PendingRequest {
66 parent: Option<String>,
68 requested_name: String,
70 version_req: String,
72 depth: usize,
74 path: Vec<String>,
76}
77
78fn validate_graph_config(config: &DependencyGraphConfig) -> Result<()> {
90 if config.max_nodes == 0 {
91 return Err(ArchToolkitError::InvalidInput(
92 "dependency graph max_nodes must be greater than zero".to_string(),
93 ));
94 }
95 if config.metadata_timeout.is_zero() {
96 return Err(ArchToolkitError::InvalidInput(
97 "dependency graph metadata_timeout must be greater than zero".to_string(),
98 ));
99 }
100 if config.max_concurrency == 0 {
101 return Err(ArchToolkitError::InvalidInput(
102 "dependency graph max_concurrency must be greater than zero".to_string(),
103 ));
104 }
105 Ok(())
106}
107
108fn sort_pending(pending: &mut [PendingRequest]) {
120 pending.sort_by(|left, right| {
121 left.depth
122 .cmp(&right.depth)
123 .then_with(|| left.requested_name.cmp(&right.requested_name))
124 .then_with(|| left.parent.cmp(&right.parent))
125 .then_with(|| left.version_req.cmp(&right.version_req))
126 });
127}
128
129fn response_requested_name(response: &DependencyMetadataResponse) -> &str {
140 match response {
141 DependencyMetadataResponse::Found(metadata) => &metadata.requested_name,
142 DependencyMetadataResponse::Missing { requested_name, .. }
143 | DependencyMetadataResponse::Failure { requested_name, .. } => requested_name,
144 }
145}
146
147fn push_diagnostic(
162 diagnostics: &mut Vec<DependencyGraphDiagnostic>,
163 kind: DependencyGraphDiagnosticKind,
164 package: impl Into<String>,
165 related_package: Option<String>,
166 message: impl Into<String>,
167) {
168 diagnostics.push(DependencyGraphDiagnostic {
169 kind,
170 package: package.into(),
171 related_package,
172 message: message.into(),
173 });
174}
175
176fn cache_batch_responses(
191 requests: &[String],
192 responses: Vec<DependencyMetadataResponse>,
193 cache: &mut BTreeMap<String, DependencyMetadataResponse>,
194 diagnostics: &mut Vec<DependencyGraphDiagnostic>,
195) {
196 let requested = requests.iter().collect::<BTreeSet<_>>();
197 let mut seen = BTreeSet::new();
198 for response in responses {
199 let response_name = response_requested_name(&response).to_string();
200 if !requested.contains(&response_name) {
201 push_diagnostic(
202 diagnostics,
203 DependencyGraphDiagnosticKind::MetadataProtocol,
204 response_name,
205 None,
206 "metadata provider returned a response for an unrequested name",
207 );
208 continue;
209 }
210 if !seen.insert(response_name.clone()) {
211 push_diagnostic(
212 diagnostics,
213 DependencyGraphDiagnosticKind::MetadataProtocol,
214 response_name,
215 None,
216 "metadata provider returned duplicate responses for one request",
217 );
218 continue;
219 }
220 cache.insert(response_name, response);
221 }
222 for request in requests {
223 if !seen.contains(request) {
224 push_diagnostic(
225 diagnostics,
226 DependencyGraphDiagnosticKind::MetadataProtocol,
227 request,
228 None,
229 "metadata provider omitted a response for the request",
230 );
231 cache.insert(
232 request.clone(),
233 DependencyMetadataResponse::Failure {
234 requested_name: request.clone(),
235 message: "metadata provider omitted a response".to_string(),
236 },
237 );
238 }
239 }
240}
241
242fn insert_node_if_allowed(
257 node: DependencyGraphNode,
258 nodes: &mut BTreeMap<String, DependencyGraphNode>,
259 config: &DependencyGraphConfig,
260 diagnostics: &mut Vec<DependencyGraphDiagnostic>,
261) -> bool {
262 if nodes.contains_key(&node.name) {
263 return true;
264 }
265 if nodes.len() >= config.max_nodes {
266 push_diagnostic(
267 diagnostics,
268 DependencyGraphDiagnosticKind::NodeLimit,
269 &node.name,
270 None,
271 format!("dependency graph node limit ({}) reached", config.max_nodes),
272 );
273 return false;
274 }
275 nodes.insert(node.name.clone(), node);
276 true
277}
278
279fn missing_node(
293 name: &str,
294 depth: usize,
295 source: Option<crate::types::dependency::DependencySource>,
296) -> DependencyGraphNode {
297 DependencyGraphNode {
298 name: name.to_string(),
299 pkgbase: None,
300 version: None,
301 provenance: DependencyProvenance {
302 requested_name: name.to_string(),
303 source,
304 provider: None,
305 },
306 status: DependencyGraphNodeStatus::Missing,
307 constraints: DependencyConstraintRange::default(),
308 provides: Vec::new(),
309 conflicts: Vec::new(),
310 depth,
311 }
312}
313
314fn srcinfo_version(data: &GraphSrcinfoData) -> Option<String> {
325 if data.pkgver.is_empty() {
326 return None;
327 }
328 let epoch_prefix = if data.epoch.is_empty() {
329 String::new()
330 } else {
331 format!("{}:", data.epoch)
332 };
333 let pkgrel_suffix = if data.pkgrel.is_empty() {
334 String::new()
335 } else {
336 format!("-{}", data.pkgrel)
337 };
338 Some(format!("{epoch_prefix}{}{pkgrel_suffix}", data.pkgver))
339}
340
341fn provider_satisfies_request(
355 package: &SrcinfoPackage,
356 requested_name: &str,
357 version_req: &str,
358) -> bool {
359 package.provides.iter().any(|provided| {
360 let provided_spec = parse_dep_spec(provided);
361 if provided_spec.name != requested_name {
362 return false;
363 }
364 if version_req.is_empty() {
365 return true;
366 }
367 let Some(version) = provided_spec.version_req.strip_prefix('=') else {
368 return false;
369 };
370 version_satisfies(version, version_req)
371 })
372}
373
374fn intersect_requirement(
387 range: &DependencyConstraintRange,
388 requirement: &str,
389) -> Option<DependencyConstraintRange> {
390 if requirement.is_empty() {
391 return Some(range.clone());
392 }
393 let (operator, version) = [">=", "<=", "=", ">", "<"].iter().find_map(|operator| {
394 requirement
395 .strip_prefix(operator)
396 .map(|version| (*operator, version))
397 })?;
398 if version.is_empty() {
399 return None;
400 }
401 let mut candidate = range.clone();
402 let bound = DependencyVersionBound {
403 version: version.to_string(),
404 inclusive: matches!(operator, ">=" | "<=" | "="),
405 };
406 match operator {
407 ">" | ">=" => update_lower(&mut candidate.lower, bound),
408 "<" | "<=" => update_upper(&mut candidate.upper, bound),
409 "=" => {
410 update_lower(&mut candidate.lower, bound.clone());
411 update_upper(&mut candidate.upper, bound);
412 }
413 _ => return None,
414 }
415 range_is_valid(&candidate).then_some(candidate)
416}
417
418fn update_lower(current: &mut Option<DependencyVersionBound>, candidate: DependencyVersionBound) {
430 let should_replace = current.as_ref().is_none_or(|existing| {
431 matches!(
432 compare_versions(&candidate.version, &existing.version),
433 std::cmp::Ordering::Greater
434 ) || (candidate.version == existing.version && !candidate.inclusive && existing.inclusive)
435 });
436 if should_replace {
437 *current = Some(candidate);
438 }
439}
440
441fn update_upper(current: &mut Option<DependencyVersionBound>, candidate: DependencyVersionBound) {
453 let should_replace = current.as_ref().is_none_or(|existing| {
454 matches!(
455 compare_versions(&candidate.version, &existing.version),
456 std::cmp::Ordering::Less
457 ) || (candidate.version == existing.version && !candidate.inclusive && existing.inclusive)
458 });
459 if should_replace {
460 *current = Some(candidate);
461 }
462}
463
464fn range_is_valid(range: &DependencyConstraintRange) -> bool {
475 let (Some(lower), Some(upper)) = (&range.lower, &range.upper) else {
476 return true;
477 };
478 match compare_versions(&lower.version, &upper.version) {
479 std::cmp::Ordering::Less => true,
480 std::cmp::Ordering::Greater => false,
481 std::cmp::Ordering::Equal => lower.inclusive && upper.inclusive,
482 }
483}
484
485fn merge_node_requirement(
500 node: &mut DependencyGraphNode,
501 requirement: &str,
502 parent: Option<&str>,
503 diagnostics: &mut Vec<DependencyGraphDiagnostic>,
504) {
505 if !requirement_is_well_formed(requirement) {
506 push_diagnostic(
507 diagnostics,
508 DependencyGraphDiagnosticKind::MalformedConstraint,
509 &node.name,
510 parent.map(str::to_string),
511 format!("requirement '{requirement}' has no supported operator and version"),
512 );
513 return;
514 }
515 let Some(range) = intersect_requirement(&node.constraints, requirement) else {
516 push_diagnostic(
517 diagnostics,
518 DependencyGraphDiagnosticKind::IncompatibleConstraints,
519 &node.name,
520 parent.map(str::to_string),
521 format!("requirement '{requirement}' has no compatible intersection"),
522 );
523 return;
524 };
525 node.constraints = range;
526}
527
528fn requirement_is_well_formed(requirement: &str) -> bool {
539 requirement.is_empty()
540 || [">=", "<=", "=", ">", "<"]
541 .iter()
542 .find_map(|operator| requirement.strip_prefix(operator))
543 .is_some_and(|version| !version.is_empty())
544}
545
546fn add_edge(
561 edges: &mut Vec<DependencyGraphEdge>,
562 parent: Option<&str>,
563 child: &str,
564 requested_name: &str,
565 version_req: &str,
566) {
567 let Some(parent) = parent else {
568 return;
569 };
570 let edge = DependencyGraphEdge {
571 from: parent.to_string(),
572 to: child.to_string(),
573 requested_name: requested_name.to_string(),
574 version_req: version_req.to_string(),
575 };
576 if !edges.contains(&edge) {
577 edges.push(edge);
578 }
579}
580
581fn process_unavailable_metadata(
594 pending: &PendingRequest,
595 response: &DependencyMetadataResponse,
596 nodes: &mut BTreeMap<String, DependencyGraphNode>,
597 edges: &mut Vec<DependencyGraphEdge>,
598 roots: &mut BTreeSet<String>,
599 config: &DependencyGraphConfig,
600 diagnostics: &mut Vec<DependencyGraphDiagnostic>,
601) {
602 let (kind, message) = match response {
603 DependencyMetadataResponse::Missing { reason, .. } => (
604 DependencyGraphDiagnosticKind::MissingMetadata,
605 format!("metadata unavailable: {reason}"),
606 ),
607 DependencyMetadataResponse::Failure { message, .. } => (
608 DependencyGraphDiagnosticKind::MetadataFailure,
609 format!("metadata retrieval failed: {message}"),
610 ),
611 DependencyMetadataResponse::Found(_) => return,
612 };
613 push_diagnostic(
614 diagnostics,
615 kind,
616 &pending.requested_name,
617 pending.parent.clone(),
618 message,
619 );
620 let node = missing_node(&pending.requested_name, pending.depth, None);
621 if insert_node_if_allowed(node, nodes, config, diagnostics) {
622 add_edge(
623 edges,
624 pending.parent.as_deref(),
625 &pending.requested_name,
626 &pending.requested_name,
627 &pending.version_req,
628 );
629 if pending.parent.is_none() {
630 roots.insert(pending.requested_name.clone());
631 }
632 }
633}
634
635fn select_srcinfo_package(
649 metadata: &DependencyMetadata,
650 pending: &PendingRequest,
651 diagnostics: &mut Vec<DependencyGraphDiagnostic>,
652) -> Option<(GraphSrcinfoData, String)> {
653 let data = parse_srcinfo_graph(&metadata.srcinfo);
654 if !data
655 .packages
656 .iter()
657 .any(|package| package.name == metadata.package_name)
658 {
659 push_diagnostic(
660 diagnostics,
661 DependencyGraphDiagnosticKind::MalformedSrcinfo,
662 &pending.requested_name,
663 pending.parent.clone(),
664 format!(
665 ".SRCINFO does not contain selected package output '{}'",
666 metadata.package_name
667 ),
668 );
669 return None;
670 }
671 let provider_verified = data
672 .packages
673 .iter()
674 .find(|package| package.name == metadata.package_name)
675 .is_some_and(|package| {
676 provider_satisfies_request(package, &pending.requested_name, &pending.version_req)
677 });
678 if metadata.package_name != pending.requested_name && !provider_verified {
679 push_diagnostic(
680 diagnostics,
681 DependencyGraphDiagnosticKind::MetadataProtocol,
682 &pending.requested_name,
683 pending.parent.clone(),
684 format!(
685 "selected provider '{}' does not verify requested virtual dependency",
686 metadata.package_name
687 ),
688 );
689 return None;
690 }
691 Some((data, metadata.package_name.clone()))
692}
693
694fn selected_dependencies(
707 package: &SrcinfoPackage,
708 include_optdepends: bool,
709 include_makedepends: bool,
710 include_checkdepends: bool,
711) -> Vec<String> {
712 let mut dependencies = package.depends.clone();
713 if include_optdepends {
714 dependencies.extend(package.optdepends.iter().map(|dependency| {
715 dependency.split_once(':').map_or_else(
716 || dependency.clone(),
717 |(_, target)| target.trim().to_string(),
718 )
719 }));
720 }
721 if include_makedepends {
722 dependencies.extend(package.makedepends.clone());
723 }
724 if include_checkdepends {
725 dependencies.extend(package.checkdepends.clone());
726 }
727 dependencies.sort();
728 dependencies.dedup();
729 dependencies
730}
731
732#[allow(clippy::too_many_arguments)]
746fn process_found_metadata(
747 pending: PendingRequest,
748 metadata: DependencyMetadata,
749 include_optdepends: bool,
750 include_makedepends: bool,
751 include_checkdepends: bool,
752 nodes: &mut BTreeMap<String, DependencyGraphNode>,
753 edges: &mut Vec<DependencyGraphEdge>,
754 roots: &mut BTreeSet<String>,
755 config: &DependencyGraphConfig,
756 diagnostics: &mut Vec<DependencyGraphDiagnostic>,
757 pending_requests: &mut Vec<PendingRequest>,
758 expanded_depths: &mut BTreeMap<String, usize>,
759) {
760 let Some((data, selected_name)) = select_srcinfo_package(&metadata, &pending, diagnostics)
761 else {
762 let missing = missing_node(
763 &pending.requested_name,
764 pending.depth,
765 Some(metadata.source.clone()),
766 );
767 if insert_node_if_allowed(missing, nodes, config, diagnostics) {
768 add_edge(
769 edges,
770 pending.parent.as_deref(),
771 &pending.requested_name,
772 &pending.requested_name,
773 &pending.version_req,
774 );
775 if pending.parent.is_none() {
776 roots.insert(pending.requested_name);
777 }
778 }
779 return;
780 };
781 let is_provider = selected_name != pending.requested_name;
782 let node = DependencyGraphNode {
783 name: selected_name.clone(),
784 pkgbase: (!data.pkgbase.is_empty()).then_some(data.pkgbase.clone()),
785 version: srcinfo_version(&data),
786 provenance: DependencyProvenance {
787 requested_name: pending.requested_name.clone(),
788 source: Some(metadata.source),
789 provider: is_provider.then_some(selected_name.clone()),
790 },
791 status: DependencyGraphNodeStatus::Resolved,
792 constraints: DependencyConstraintRange::default(),
793 provides: data
794 .packages
795 .iter()
796 .find(|package| package.name == selected_name)
797 .map_or_else(Vec::new, |package| package.provides.clone()),
798 conflicts: data
799 .packages
800 .iter()
801 .find(|package| package.name == selected_name)
802 .map_or_else(Vec::new, |package| package.conflicts.clone()),
803 depth: pending.depth,
804 };
805 if !insert_node_if_allowed(node, nodes, config, diagnostics) {
806 return;
807 }
808 add_edge(
809 edges,
810 pending.parent.as_deref(),
811 &selected_name,
812 &pending.requested_name,
813 &pending.version_req,
814 );
815 if pending.parent.is_none() {
816 roots.insert(selected_name.clone());
817 }
818 let Some(node) = nodes.get_mut(&selected_name) else {
819 return;
820 };
821 node.depth = node.depth.min(pending.depth);
822 if !is_provider {
823 merge_node_requirement(
824 node,
825 &pending.version_req,
826 pending.parent.as_deref(),
827 diagnostics,
828 );
829 }
830 if pending.path.contains(&selected_name) {
831 push_diagnostic(
832 diagnostics,
833 DependencyGraphDiagnosticKind::Cycle,
834 &selected_name,
835 pending.parent,
836 "dependency cycle detected; branch expansion stopped",
837 );
838 return;
839 }
840 if !mark_expansion(expanded_depths, &selected_name, pending.depth) {
841 return;
842 }
843 if pending.depth >= config.max_depth {
844 let has_dependencies = data
845 .packages
846 .iter()
847 .find(|package| package.name == selected_name)
848 .is_some_and(|package| {
849 !selected_dependencies(
850 package,
851 include_optdepends,
852 include_makedepends,
853 include_checkdepends,
854 )
855 .is_empty()
856 });
857 if has_dependencies {
858 push_diagnostic(
859 diagnostics,
860 DependencyGraphDiagnosticKind::DepthLimit,
861 &selected_name,
862 None,
863 format!(
864 "dependency graph depth limit ({}) reached",
865 config.max_depth
866 ),
867 );
868 }
869 return;
870 }
871 let Some(package) = data
872 .packages
873 .iter()
874 .find(|package| package.name == selected_name)
875 else {
876 return;
877 };
878 let mut next_path = pending.path;
879 next_path.push(selected_name.clone());
880 for dependency in selected_dependencies(
881 package,
882 include_optdepends,
883 include_makedepends,
884 include_checkdepends,
885 ) {
886 let specification = parse_dep_spec(&dependency);
887 if specification.name.is_empty() {
888 continue;
889 }
890 pending_requests.push(PendingRequest {
891 parent: Some(selected_name.clone()),
892 requested_name: specification.name,
893 version_req: specification.version_req,
894 depth: pending.depth + 1,
895 path: next_path.clone(),
896 });
897 }
898}
899
900fn mark_expansion(
915 expanded_depths: &mut BTreeMap<String, usize>,
916 package: &str,
917 depth: usize,
918) -> bool {
919 if expanded_depths
920 .get(package)
921 .is_some_and(|known_depth| *known_depth <= depth)
922 {
923 return false;
924 }
925 expanded_depths.insert(package.to_string(), depth);
926 true
927}
928
929fn conflict_matches_node(conflict: &str, candidate: &DependencyGraphNode) -> bool {
942 let conflict_spec = parse_dep_spec(conflict);
943 if conflict_spec.name.is_empty() {
944 return false;
945 }
946 if candidate.name == conflict_spec.name {
947 return conflict_spec.version_req.is_empty()
948 || candidate
949 .version
950 .as_deref()
951 .is_some_and(|version| version_satisfies(version, &conflict_spec.version_req));
952 }
953 candidate.provides.iter().any(|provided| {
954 let provided_spec = parse_dep_spec(provided);
955 if provided_spec.name != conflict_spec.name {
956 return false;
957 }
958 if conflict_spec.version_req.is_empty() {
959 return true;
960 }
961 provided_spec
962 .version_req
963 .strip_prefix('=')
964 .is_some_and(|version| version_satisfies(version, &conflict_spec.version_req))
965 })
966}
967
968fn apply_conflicts(
981 nodes: &mut BTreeMap<String, DependencyGraphNode>,
982 diagnostics: &mut Vec<DependencyGraphDiagnostic>,
983) {
984 let names = nodes.keys().cloned().collect::<Vec<_>>();
985 let mut matches = Vec::new();
986 for (index, left_name) in names.iter().enumerate() {
987 for right_name in names.iter().skip(index + 1) {
988 let (Some(left), Some(right)) = (nodes.get(left_name), nodes.get(right_name)) else {
989 continue;
990 };
991 if left.status != DependencyGraphNodeStatus::Resolved
992 || right.status != DependencyGraphNodeStatus::Resolved
993 {
994 continue;
995 }
996 if left
997 .conflicts
998 .iter()
999 .any(|conflict| conflict_matches_node(conflict, right))
1000 || right
1001 .conflicts
1002 .iter()
1003 .any(|conflict| conflict_matches_node(conflict, left))
1004 {
1005 matches.push((left_name.clone(), right_name.clone()));
1006 }
1007 }
1008 }
1009 for (left_name, right_name) in matches {
1010 if let Some(left) = nodes.get_mut(&left_name) {
1011 left.status = DependencyGraphNodeStatus::Conflicting;
1012 }
1013 if let Some(right) = nodes.get_mut(&right_name) {
1014 right.status = DependencyGraphNodeStatus::Conflicting;
1015 }
1016 push_diagnostic(
1017 diagnostics,
1018 DependencyGraphDiagnosticKind::Conflict,
1019 left_name,
1020 Some(right_name),
1021 "declared package or virtual conflict matched another resolved graph node",
1022 );
1023 }
1024}
1025
1026#[allow(clippy::too_many_arguments)]
1042pub(super) fn resolve_dependency_graph<P: DependencyMetadataProvider>(
1043 packages: &[PackageRef],
1044 provider: &P,
1045 config: DependencyGraphConfig,
1046 include_optdepends: bool,
1047 include_makedepends: bool,
1048 include_checkdepends: bool,
1049) -> Result<DependencyGraphResolution> {
1050 validate_graph_config(&config)?;
1051 let mut pending_requests = packages
1052 .iter()
1053 .map(|package| PendingRequest {
1054 parent: None,
1055 requested_name: package.name.clone(),
1056 version_req: String::new(),
1057 depth: 0,
1058 path: Vec::new(),
1059 })
1060 .collect::<Vec<_>>();
1061 let mut cache = BTreeMap::new();
1062 let mut nodes = BTreeMap::new();
1063 let mut edges = Vec::new();
1064 let mut roots = BTreeSet::new();
1065 let mut diagnostics = Vec::new();
1066 let mut expanded_depths = BTreeMap::new();
1067
1068 while !pending_requests.is_empty() {
1069 sort_pending(&mut pending_requests);
1070 let request = pending_requests.remove(0);
1071 if !cache.contains_key(&request.requested_name) {
1072 let mut batch = Vec::new();
1073 for pending in std::iter::once(&request).chain(pending_requests.iter()) {
1074 if batch.len() == config.max_concurrency {
1075 break;
1076 }
1077 if !cache.contains_key(&pending.requested_name)
1078 && !batch.contains(&pending.requested_name)
1079 {
1080 batch.push(pending.requested_name.clone());
1081 }
1082 }
1083 let started = Instant::now();
1084 let responses = provider.fetch_metadata(&batch, config.metadata_timeout);
1085 if started.elapsed() > config.metadata_timeout {
1086 for name in &batch {
1087 push_diagnostic(
1088 &mut diagnostics,
1089 DependencyGraphDiagnosticKind::Timeout,
1090 name,
1091 None,
1092 format!(
1093 "metadata provider exceeded timeout of {:?}",
1094 config.metadata_timeout
1095 ),
1096 );
1097 }
1098 cache_batch_responses(
1099 &batch,
1100 batch
1101 .iter()
1102 .map(|name| DependencyMetadataResponse::Failure {
1103 requested_name: name.clone(),
1104 message: "metadata provider timed out".to_string(),
1105 })
1106 .collect(),
1107 &mut cache,
1108 &mut diagnostics,
1109 );
1110 } else {
1111 cache_batch_responses(&batch, responses, &mut cache, &mut diagnostics);
1112 }
1113 }
1114 let Some(response) = cache.get(&request.requested_name).cloned() else {
1115 continue;
1116 };
1117 match response {
1118 DependencyMetadataResponse::Found(metadata) => process_found_metadata(
1119 request,
1120 metadata,
1121 include_optdepends,
1122 include_makedepends,
1123 include_checkdepends,
1124 &mut nodes,
1125 &mut edges,
1126 &mut roots,
1127 &config,
1128 &mut diagnostics,
1129 &mut pending_requests,
1130 &mut expanded_depths,
1131 ),
1132 unavailable => process_unavailable_metadata(
1133 &request,
1134 &unavailable,
1135 &mut nodes,
1136 &mut edges,
1137 &mut roots,
1138 &config,
1139 &mut diagnostics,
1140 ),
1141 }
1142 }
1143
1144 apply_conflicts(&mut nodes, &mut diagnostics);
1145 edges.sort_by(|left, right| {
1146 left.from
1147 .cmp(&right.from)
1148 .then_with(|| left.to.cmp(&right.to))
1149 .then_with(|| left.requested_name.cmp(&right.requested_name))
1150 .then_with(|| left.version_req.cmp(&right.version_req))
1151 });
1152 diagnostics.sort_by(|left, right| {
1153 format!("{:?}", left.kind)
1154 .cmp(&format!("{:?}", right.kind))
1155 .then_with(|| left.package.cmp(&right.package))
1156 .then_with(|| left.related_package.cmp(&right.related_package))
1157 .then_with(|| left.message.cmp(&right.message))
1158 });
1159 Ok(DependencyGraphResolution {
1160 roots: roots.into_iter().collect(),
1161 nodes: nodes.into_values().collect(),
1162 edges,
1163 diagnostics,
1164 })
1165}
1166
1167impl DependencyGraphResolution {
1168 #[must_use]
1180 pub fn render_tree(&self) -> String {
1181 let mut output = String::new();
1182 for (index, root) in self.roots.iter().enumerate() {
1183 let mut path = BTreeSet::new();
1184 render_tree_node(
1185 self,
1186 root,
1187 "",
1188 true,
1189 index + 1 < self.roots.len(),
1190 &mut path,
1191 &mut output,
1192 );
1193 }
1194 output
1195 }
1196}
1197
1198#[allow(clippy::too_many_arguments)]
1210fn render_tree_node(
1211 graph: &DependencyGraphResolution,
1212 name: &str,
1213 prefix: &str,
1214 is_child: bool,
1215 has_next_root: bool,
1216 path: &mut BTreeSet<String>,
1217 output: &mut String,
1218) {
1219 let label = graph
1220 .nodes
1221 .iter()
1222 .find(|node| node.name == name)
1223 .map_or_else(
1224 || name.to_string(),
1225 |node| {
1226 if node.provenance.requested_name == node.name {
1227 node.name.clone()
1228 } else {
1229 format!("{} (for {})", node.name, node.provenance.requested_name)
1230 }
1231 },
1232 );
1233 if is_child {
1234 output.push_str(prefix);
1235 output.push_str(if has_next_root {
1236 "├── "
1237 } else {
1238 "└── "
1239 });
1240 }
1241 output.push_str(&label);
1242 output.push('\n');
1243 if !path.insert(name.to_string()) {
1244 output.push_str(prefix);
1245 output.push_str(" ↺\n");
1246 return;
1247 }
1248 let children = graph
1249 .edges
1250 .iter()
1251 .filter(|edge| edge.from == name)
1252 .map(|edge| edge.to.as_str())
1253 .collect::<BTreeSet<_>>();
1254 for (index, child) in children.iter().enumerate() {
1255 let child_prefix = if is_child {
1256 format!("{prefix}{}", if has_next_root { "│ " } else { " " })
1257 } else {
1258 String::new()
1259 };
1260 render_tree_node(
1261 graph,
1262 child,
1263 &child_prefix,
1264 true,
1265 index + 1 < children.len(),
1266 path,
1267 output,
1268 );
1269 }
1270 path.remove(name);
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275 use super::*;
1276
1277 #[test]
1288 fn intersect_requirement_handles_epoch_and_pkgrel() {
1289 let range = intersect_requirement(&DependencyConstraintRange::default(), ">=1:2.0-3")
1290 .unwrap_or_default();
1291 let range = intersect_requirement(&range, "<=1:3.0-1").unwrap_or_default();
1292 assert_eq!(
1293 range.lower.as_ref().map(|bound| bound.version.as_str()),
1294 Some("1:2.0-3")
1295 );
1296 assert_eq!(
1297 range.upper.as_ref().map(|bound| bound.version.as_str()),
1298 Some("1:3.0-1")
1299 );
1300 assert!(intersect_requirement(&range, "<1:2.0-3").is_none());
1301 }
1302
1303 #[test]
1314 fn validate_graph_config_rejects_zero_mandatory_bounds() {
1315 assert!(
1316 validate_graph_config(&DependencyGraphConfig {
1317 max_nodes: 0,
1318 ..DependencyGraphConfig::default()
1319 })
1320 .is_err()
1321 );
1322 assert!(
1323 validate_graph_config(&DependencyGraphConfig {
1324 metadata_timeout: Duration::ZERO,
1325 ..DependencyGraphConfig::default()
1326 })
1327 .is_err()
1328 );
1329 assert!(
1330 validate_graph_config(&DependencyGraphConfig {
1331 max_concurrency: 0,
1332 ..DependencyGraphConfig::default()
1333 })
1334 .is_err()
1335 );
1336 }
1337
1338 #[test]
1349 fn expansion_memoization_bounds_shared_paths() {
1350 let mut expanded = BTreeMap::new();
1351 assert!(mark_expansion(&mut expanded, "shared", 4));
1352 assert!(!mark_expansion(&mut expanded, "shared", 4));
1353 assert!(!mark_expansion(&mut expanded, "shared", 6));
1354 assert!(mark_expansion(&mut expanded, "shared", 2));
1355 assert!(!mark_expansion(&mut expanded, "shared", 3));
1356 }
1357
1358 #[test]
1369 fn requirement_validation_distinguishes_malformed_constraints() {
1370 assert!(requirement_is_well_formed(""));
1371 assert!(requirement_is_well_formed(">=1.0"));
1372 assert!(!requirement_is_well_formed("1.0"));
1373 assert!(!requirement_is_well_formed(">="));
1374 }
1375}