1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet, VecDeque};
3use std::fmt::Write;
4use std::path::Path;
5
6use either::Either;
7use itertools::Itertools;
8use owo_colors::OwoColorize;
9use petgraph::algo::tarjan_scc;
10use petgraph::graph::{EdgeIndex, NodeIndex};
11use petgraph::prelude::EdgeRef;
12use petgraph::{Direction, Graph};
13use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
14use serde::Serialize;
15
16use uv_configuration::DependencyGroupsWithDefaults;
17use uv_console::human_readable_bytes;
18use uv_fs::PortablePathBuf;
19use uv_normalize::{ExtraName, GroupName, PackageName};
20use uv_pep440::Version;
21use uv_pep508::MarkerTree;
22use uv_pypi_types::ResolverMarkerEnvironment;
23
24use crate::lock::export::{
25 MetadataNode, MetadataNodeId, MetadataNodeKind, MetadataScript, MetadataWorkspace,
26 MetadataWorkspaceMember,
27};
28use crate::lock::{Package, PackageId, PackageIndex};
29use crate::{ConflictMarker, Lock, PackageMap, UniversalMarker};
30
31#[derive(Debug, Clone, Copy)]
32pub enum TreeJsonTarget<'a> {
33 Workspace(&'a Path),
34 Script(&'a Path),
35}
36
37impl<'a> TreeJsonTarget<'a> {
38 fn root(self) -> &'a Path {
39 match self {
40 Self::Workspace(root) => root,
41 Self::Script(script) => script.parent().unwrap_or_else(|| Path::new("")),
42 }
43 }
44}
45
46#[derive(Debug)]
47pub struct TreeDisplay<'env> {
48 graph: petgraph::graph::Graph<Node, Edge<'env>, petgraph::Directed>,
50 roots: Vec<NodeIndex>,
52 latest: &'env PackageMap<Version>,
54 depth: usize,
56 no_dedupe: bool,
58 invert: bool,
60 prod: bool,
62 groups: DependencyGroupsWithDefaults,
64 lock: &'env Lock,
66 show_sizes: bool,
68 conflict_marker: UniversalMarker,
70}
71
72impl<'env> TreeDisplay<'env> {
73 pub fn new(
75 lock: &'env Lock,
76 markers: Option<&'env ResolverMarkerEnvironment>,
77 latest: &'env PackageMap<Version>,
78 depth: usize,
79 prune: &[PackageName],
80 packages: &[PackageName],
81 groups: &DependencyGroupsWithDefaults,
82 no_dedupe: bool,
83 invert: bool,
84 show_sizes: bool,
85 ) -> Self {
86 let members: BTreeSet<&PackageId> = if lock.members().is_empty() {
94 lock.root().into_iter().map(|package| &package.id).collect()
95 } else {
96 lock.packages
97 .iter()
98 .filter_map(|package| {
99 if lock.members().contains(&package.id.name) {
100 Some(&package.id)
101 } else {
102 None
103 }
104 })
105 .collect()
106 };
107
108 let conflict_marker = UniversalMarker::new(
111 MarkerTree::TRUE,
112 ConflictMarker::from_conflicts(lock.conflicts()),
113 );
114
115 let size_guess = lock.packages.len();
117 let mut graph =
118 Graph::<Node, Edge, petgraph::Directed>::with_capacity(size_guess, size_guess);
119 let mut inverse = vec![None; size_guess];
120 let mut queue: VecDeque<(PackageIndex, Option<&ExtraName>)> = VecDeque::new();
121 let mut seen = FxHashSet::default();
122
123 let root = graph.add_node(Node::Root);
124
125 for id in members.iter().copied() {
127 if prune.contains(&id.name) {
128 continue;
129 }
130
131 let package_index = lock.by_id[id];
132 let dist = lock.package(package_index);
133
134 let index = *inverse[package_index.0]
138 .get_or_insert_with(|| graph.add_node(Node::Package(package_index)));
139
140 graph.add_edge(root, index, Edge::Prod(None, UniversalMarker::TRUE));
142
143 if groups.prod() {
144 if seen.insert((package_index, None)) {
146 queue.push_back((package_index, None));
147 }
148
149 for extra in dist.optional_dependencies.keys() {
151 if seen.insert((package_index, Some(extra))) {
152 queue.push_back((package_index, Some(extra)));
153 }
154 }
155 }
156
157 for (group, dep) in dist
159 .dependency_groups
160 .iter()
161 .filter_map(|(group, deps)| {
162 if groups.contains(group) {
163 Some(deps.iter().map(move |dep| (group, dep)))
164 } else {
165 None
166 }
167 })
168 .flatten()
169 {
170 if prune.contains(&dep.package_id.name) {
171 continue;
172 }
173
174 if markers
175 .is_some_and(|markers| !dep.complexified_marker.evaluate_no_extras(markers))
176 {
177 continue;
178 }
179
180 let dep_index = *inverse[dep.index.0]
182 .get_or_insert_with(|| graph.add_node(Node::Package(dep.index)));
183
184 graph.add_edge(
186 index,
187 dep_index,
188 Edge::Dev(
189 group,
190 Some(RequestedExtras::Dependency(&dep.extra)),
191 dep.complexified_marker,
192 ),
193 );
194
195 if seen.insert((dep.index, None)) {
197 queue.push_back((dep.index, None));
198 }
199 for extra in &dep.extra {
200 if seen.insert((dep.index, Some(extra))) {
201 queue.push_back((dep.index, Some(extra)));
202 }
203 }
204 }
205 }
206
207 {
216 let by_name: FxHashMap<_, Vec<_>> = {
218 lock.packages().iter().enumerate().fold(
219 FxHashMap::with_capacity_and_hasher(lock.len(), FxBuildHasher),
220 |mut map, (index, package)| {
221 map.entry(&package.id.name)
222 .or_default()
223 .push(PackageIndex(index));
224 map
225 },
226 )
227 };
228
229 for requirement in lock.requirements() {
231 for &package_index in by_name.get(&requirement.name).into_iter().flatten() {
232 let package = lock.package(package_index);
233 let marker = if package.fork_markers.is_empty() {
236 requirement.marker
237 } else {
238 let mut combined = MarkerTree::FALSE;
239 for fork_marker in &package.fork_markers {
240 combined = combined.or(fork_marker.pep508());
241 }
242 combined = combined.and(requirement.marker);
243 combined
244 };
245 if marker.is_false() {
246 continue;
247 }
248 if markers.is_some_and(|markers| !marker.evaluate(markers, &[])) {
249 continue;
250 }
251 let index = *inverse[package_index.0]
253 .get_or_insert_with(|| graph.add_node(Node::Package(package_index)));
254
255 graph.add_edge(
257 root,
258 index,
259 Edge::Prod(
260 Some(RequestedExtras::Requirement(requirement.extras.as_ref())),
261 UniversalMarker::from_combined(marker),
262 ),
263 );
264
265 if seen.insert((package_index, None)) {
267 queue.push_back((package_index, None));
268 }
269 for extra in &*requirement.extras {
270 if seen.insert((package_index, Some(extra))) {
271 queue.push_back((package_index, Some(extra)));
272 }
273 }
274 }
275 }
276
277 for (group, requirements) in lock.dependency_groups() {
279 if !groups.contains(group) {
280 continue;
281 }
282 for requirement in requirements {
283 for &package_index in by_name.get(&requirement.name).into_iter().flatten() {
284 let package = lock.package(package_index);
285 let marker = if package.fork_markers.is_empty() {
288 requirement.marker
289 } else {
290 let mut combined = MarkerTree::FALSE;
291 for fork_marker in &package.fork_markers {
292 combined = combined.or(fork_marker.pep508());
293 }
294 combined = combined.and(requirement.marker);
295 combined
296 };
297 if marker.is_false() {
298 continue;
299 }
300 if markers.is_some_and(|markers| !marker.evaluate(markers, &[])) {
301 continue;
302 }
303 let index = *inverse[package_index.0]
305 .get_or_insert_with(|| graph.add_node(Node::Package(package_index)));
306
307 graph.add_edge(
309 root,
310 index,
311 Edge::Dev(
312 group,
313 Some(RequestedExtras::Requirement(requirement.extras.as_ref())),
314 UniversalMarker::from_combined(marker),
315 ),
316 );
317
318 if seen.insert((package_index, None)) {
320 queue.push_back((package_index, None));
321 }
322 for extra in &*requirement.extras {
323 if seen.insert((package_index, Some(extra))) {
324 queue.push_back((package_index, Some(extra)));
325 }
326 }
327 }
328 }
329 }
330 }
331
332 while let Some((package_index, extra)) = queue.pop_front() {
334 let index = inverse[package_index.0].expect("queued package has a graph node");
335 let package = lock.package(package_index);
336
337 let deps = if let Some(extra) = extra {
338 Either::Left(
339 package
340 .optional_dependencies
341 .get(extra)
342 .into_iter()
343 .flatten(),
344 )
345 } else {
346 Either::Right(package.dependencies.iter())
347 };
348
349 for dep in deps {
350 if prune.contains(&dep.package_id.name) {
351 continue;
352 }
353
354 if markers
355 .is_some_and(|markers| !dep.complexified_marker.evaluate_no_extras(markers))
356 {
357 continue;
358 }
359
360 let dep_index = *inverse[dep.index.0]
362 .get_or_insert_with(|| graph.add_node(Node::Package(dep.index)));
363
364 graph.add_edge(
366 index,
367 dep_index,
368 if let Some(extra) = extra {
369 Edge::Optional(
370 extra,
371 Some(RequestedExtras::Dependency(&dep.extra)),
372 dep.complexified_marker,
373 )
374 } else {
375 Edge::Prod(
376 Some(RequestedExtras::Dependency(&dep.extra)),
377 dep.complexified_marker,
378 )
379 },
380 );
381
382 if seen.insert((dep.index, None)) {
384 queue.push_back((dep.index, None));
385 }
386 for extra in &dep.extra {
387 if seen.insert((dep.index, Some(extra))) {
388 queue.push_back((dep.index, Some(extra)));
389 }
390 }
391 }
392 }
393
394 {
396 let mut reachable = graph
397 .node_indices()
398 .filter(|index| match graph[*index] {
399 Node::Package(package_index) => {
400 members.contains(&lock.package(package_index).id)
401 }
402 Node::Root => true,
403 })
404 .collect::<FxHashSet<_>>();
405 let mut stack = reachable.iter().copied().collect::<VecDeque<_>>();
406 while let Some(node) = stack.pop_front() {
407 for edge in graph.edges_directed(node, Direction::Outgoing) {
408 if reachable.insert(edge.target()) {
409 stack.push_back(edge.target());
410 }
411 }
412 }
413
414 graph.retain_nodes(|_, index| reachable.contains(&index));
416 }
417
418 if invert {
420 graph.reverse();
421 }
422
423 if !packages.is_empty() {
425 let mut reachable = graph
426 .node_indices()
427 .filter(|index| {
428 let Node::Package(package_index) = graph[*index] else {
429 return false;
430 };
431 packages.contains(&lock.package(package_index).id.name)
432 })
433 .collect::<FxHashSet<_>>();
434 let mut stack = reachable.iter().copied().collect::<VecDeque<_>>();
435 while let Some(node) = stack.pop_front() {
436 for edge in graph.edges_directed(node, Direction::Outgoing) {
437 if reachable.insert(edge.target()) {
438 stack.push_back(edge.target());
439 }
440 }
441 }
442
443 graph.retain_nodes(|_, index| reachable.contains(&index));
445 }
446
447 let roots = {
449 if !packages.is_empty() {
451 let mut roots = graph
452 .node_indices()
453 .filter(|index| {
454 let Node::Package(package_index) = graph[*index] else {
455 return false;
456 };
457 packages.contains(&lock.package(package_index).id.name)
458 })
459 .collect::<Vec<_>>();
460
461 roots.sort_by_key(|index| graph[*index].sort_key(lock));
463
464 roots
465 } else {
466 let mut roots = if invert {
467 let components = tarjan_scc(&graph);
471 let component_by_node = components
472 .iter()
473 .enumerate()
474 .flat_map(|(component_index, component)| {
475 component.iter().map(move |node| (*node, component_index))
476 })
477 .collect::<FxHashMap<_, _>>();
478
479 components
480 .iter()
481 .enumerate()
482 .filter(|(component_index, component)| {
484 component.iter().all(|node| {
485 graph
487 .edges_directed(*node, Direction::Incoming)
488 .all(|edge| {
489 component_by_node[&edge.source()] == *component_index
490 })
491 })
492 })
493 .filter_map(|(_, component)| {
495 component
496 .iter()
497 .min_by_key(|index| graph[**index].sort_key(lock))
498 })
499 .copied()
500 .collect::<Vec<_>>()
501 } else {
502 graph
504 .node_indices()
505 .filter(|index| matches!(graph[*index], Node::Root))
506 .collect::<Vec<_>>()
507 };
508
509 roots.sort_by_key(|index| graph[*index].sort_key(lock));
510 roots
511 }
512 };
513
514 Self {
515 graph,
516 roots,
517 latest,
518 depth,
519 no_dedupe,
520 invert,
521 prod: groups.prod(),
522 groups: groups.clone(),
523 lock,
524 show_sizes,
525 conflict_marker,
526 }
527 }
528
529 fn visit(
531 &'env self,
532 cursor: Cursor,
533 visited: &mut FxHashMap<VisitedNode<'env>, Vec<PackageIndex>>,
534 path: &mut Vec<VisitedNode<'env>>,
535 ) -> Vec<String> {
536 if path.len() > self.depth {
538 return Vec::new();
539 }
540
541 let Node::Package(package_index) = self.graph[cursor.node()] else {
542 return Vec::new();
543 };
544 let edge = cursor.edge().map(|edge_id| &self.graph[edge_id]);
545 let package = self.lock.package(package_index);
546 let package_id = &package.id;
547
548 let expanded_extras = self.expanded_extras(package, edge);
549 let visited_node = VisitedNode {
550 package_index,
551 expanded_extras: expanded_extras.clone(),
552 marker: self.invert.then_some(cursor.marker()),
553 };
554
555 let line = {
556 let mut line = format!("{}", package_id.name);
557
558 if let Some(extras) = edge.and_then(Edge::extras) {
559 if !extras.is_empty() {
560 line.push('[');
561 line.push_str(extras.iter().join(", ").as_str());
562 line.push(']');
563 }
564 }
565
566 if let Some(version) = package_id.version.as_ref() {
567 line.push(' ');
568 line.push('v');
569 let _ = write!(line, "{version}");
570 }
571
572 if let Some(edge) = edge {
573 match edge {
574 Edge::Prod(..) => {}
575 Edge::Optional(extra, ..) => {
576 let _ = write!(line, " (extra: {extra})");
577 }
578 Edge::Dev(group, ..) => {
579 let _ = write!(line, " (group: {group})");
580 }
581 }
582 }
583
584 if self.show_sizes {
587 if let Some(size_bytes) = package.wheels.iter().find_map(|wheel| wheel.size) {
588 let bytes = human_readable_bytes(size_bytes);
589 line.push(' ');
590 line.push_str(format!("{}", format!("({bytes:.1})").dimmed()).as_str());
591 }
592 }
593
594 line
595 };
596
597 if path.contains(&visited_node) {
601 return vec![format!("{line} (*)")];
602 }
603 if !self.no_dedupe
604 && let Some(requirements) = visited.get(&visited_node)
605 {
606 return if requirements.is_empty() {
607 vec![line]
608 } else {
609 vec![format!("{line} (*)")]
610 };
611 }
612
613 let line = if let Some(version) = self.latest.get(package_id) {
615 format!("{line} {}", format!("(latest: v{version})").bold().cyan())
616 } else {
617 line
618 };
619
620 let mut dependencies = if self.invert && edge.is_some_and(Edge::is_dev) {
621 Vec::new()
624 } else {
625 self.graph
626 .edges_directed(cursor.node(), Direction::Outgoing)
627 .filter_map(|edge| match self.graph[edge.target()] {
628 Node::Root => None,
629 Node::Package(_) => {
630 let edge_kind = &self.graph[edge.id()];
631
632 if self.invert {
633 if !expanded_extras.is_empty()
636 && edge_kind.extras().is_none_or(|extras| {
637 !expanded_extras.iter().all(|extra| extras.contains(extra))
638 })
639 {
640 return None;
641 }
642
643 let mut marker = cursor.marker();
646 marker.and(edge_kind.marker());
647 if marker.is_false() {
648 return None;
649 }
650 Some(Cursor::new(edge.target(), edge.id(), marker))
651 } else {
652 if let Edge::Optional(required_extra, ..) = edge_kind
655 && !expanded_extras.contains(required_extra)
656 {
657 return None;
658 }
659 Some(Cursor::new(edge.target(), edge.id(), UniversalMarker::TRUE))
660 }
661 }
662 })
663 .collect::<Vec<_>>()
664 };
665 dependencies.sort_by_key(|cursor| {
666 let node = self.graph[cursor.node()].sort_key(self.lock);
667 let edge = cursor
668 .edge()
669 .map(|edge_id| &self.graph[edge_id])
670 .map(Edge::kind);
671 (edge, node)
672 });
673
674 let mut lines = vec![line];
675
676 if path.len() < self.depth {
679 visited.insert(
680 visited_node.clone(),
681 dependencies
682 .iter()
683 .filter_map(|node| match self.graph[node.node()] {
684 Node::Package(package_index) => Some(package_index),
685 Node::Root => None,
686 })
687 .collect(),
688 );
689 }
690 path.push(visited_node);
691
692 for (index, dep) in dependencies.iter().enumerate() {
693 let (prefix_top, prefix_rest) = if dependencies.len() - 1 == index {
713 ("└── ", " ")
714 } else {
715 ("├── ", "│ ")
716 };
717 for (visited_index, visited_line) in self.visit(*dep, visited, path).iter().enumerate()
718 {
719 let prefix = if visited_index == 0 {
720 prefix_top
721 } else {
722 prefix_rest
723 };
724 lines.push(format!("{prefix}{visited_line}"));
725 }
726 }
727
728 path.pop();
729
730 lines
731 }
732
733 fn render(&self) -> Vec<String> {
735 let mut path = Vec::new();
736 let mut lines = Vec::with_capacity(self.graph.node_count());
737 let mut visited =
738 FxHashMap::with_capacity_and_hasher(self.graph.node_count(), FxBuildHasher);
739
740 for node in &self.roots {
741 match self.graph[*node] {
742 Node::Root => {
743 for edge in self.graph.edges_directed(*node, Direction::Outgoing) {
744 let node = edge.target();
745 path.clear();
746 lines.extend(self.visit(
747 Cursor::new(node, edge.id(), self.conflict_marker),
748 &mut visited,
749 &mut path,
750 ));
751 }
752 }
753 Node::Package(_) => {
754 path.clear();
755 lines.extend(self.visit(
756 Cursor::root(*node, self.conflict_marker),
757 &mut visited,
758 &mut path,
759 ));
760 }
761 }
762 }
763
764 lines
765 }
766
767 fn expanded_extras(
769 &self,
770 package: &'env Package,
771 edge: Option<&Edge<'env>>,
772 ) -> BTreeSet<&'env ExtraName> {
773 if self.invert {
774 return edge.and_then(Edge::required_extra).into_iter().collect();
777 }
778
779 let Some(requested_extras) = edge.and_then(Edge::extras) else {
780 return package.optional_dependencies.keys().collect();
782 };
783
784 requested_extras
785 .iter()
786 .filter(|extra| package.optional_dependencies.contains_key(*extra))
787 .collect()
788 }
789
790 pub fn to_json(&self, target: TreeJsonTarget<'_>) -> Result<String, serde_json::Error> {
792 serde_json::to_string_pretty(&JsonGraph::new(self, target))
793 }
794
795 fn json_traversal(&self) -> JsonTraversal {
805 let mut distances = FxHashMap::default();
806 let mut queue = VecDeque::new();
807 let mut nodes = FxHashSet::default();
808 let mut edges = FxHashSet::default();
809
810 for root in &self.roots {
811 match self.graph[*root] {
812 Node::Root => {
813 for edge in self.graph.edges_directed(*root, Direction::Outgoing) {
814 let Node::Package(package_index) = self.graph[edge.target()] else {
815 continue;
816 };
817 let state = JsonTraversalNode {
818 index: edge.target(),
819 expanded_extras: self.expanded_extras(
820 self.lock.package(package_index),
821 Some(edge.weight()),
822 ),
823 marker: UniversalMarker::TRUE,
824 reached_via_dependency_group: false,
825 };
826 nodes.insert(state.index);
827 if distances.insert(state.clone(), 0).is_none() {
828 queue.push_back(state);
829 }
830 }
831 }
832 Node::Package(package_index) => {
833 let state = JsonTraversalNode {
834 index: *root,
835 expanded_extras: self
836 .expanded_extras(self.lock.package(package_index), None),
837 marker: if self.invert {
838 self.conflict_marker
839 } else {
840 UniversalMarker::TRUE
841 },
842 reached_via_dependency_group: false,
843 };
844 nodes.insert(state.index);
845 if distances.insert(state.clone(), 0).is_none() {
846 queue.push_back(state);
847 }
848 }
849 }
850 }
851
852 while let Some(source) = queue.pop_front() {
853 let distance = distances[&source];
854 if distance >= self.depth || self.invert && source.reached_via_dependency_group {
855 continue;
856 }
857
858 for edge in self.graph.edges_directed(source.index, Direction::Outgoing) {
859 let edge_kind = edge.weight();
860 let marker = if self.invert {
861 if !source.expanded_extras.is_empty()
864 && edge_kind.extras().is_none_or(|extras| {
865 !source
866 .expanded_extras
867 .iter()
868 .all(|extra| extras.contains(extra))
869 })
870 {
871 continue;
872 }
873
874 let mut marker = source.marker;
877 marker.and(edge_kind.marker());
878 if marker.is_false() {
879 continue;
880 }
881 marker
882 } else {
883 if let Edge::Optional(required_extra, ..) = edge_kind
886 && !source.expanded_extras.contains(required_extra)
887 {
888 continue;
889 }
890 UniversalMarker::TRUE
891 };
892
893 let target = edge.target();
894 if matches!(self.graph[target], Node::Root) {
895 edges.insert(edge.id());
896 continue;
897 }
898 let Node::Package(package_index) = self.graph[target] else {
899 continue;
900 };
901 let state = JsonTraversalNode {
902 index: target,
903 expanded_extras: self
904 .expanded_extras(self.lock.package(package_index), Some(edge.weight())),
905 marker,
906 reached_via_dependency_group: self.invert && edge_kind.is_dev(),
907 };
908 nodes.insert(state.index);
909 edges.insert(edge.id());
910 if !distances.contains_key(&state) {
911 distances.insert(state.clone(), distance + 1);
912 queue.push_back(state);
913 }
914 }
915 }
916
917 JsonTraversal { nodes, edges }
918 }
919}
920
921#[derive(Debug)]
922struct JsonTraversal {
923 nodes: FxHashSet<NodeIndex>,
924 edges: FxHashSet<EdgeIndex>,
925}
926
927#[derive(Debug, Clone, PartialEq, Eq, Hash)]
928struct JsonTraversalNode<'env> {
929 index: NodeIndex,
930 expanded_extras: BTreeSet<&'env ExtraName>,
931 marker: UniversalMarker,
932 reached_via_dependency_group: bool,
933}
934
935#[derive(Debug, Serialize)]
1033struct JsonGraph {
1034 schema: JsonSchema,
1035 workspace_root: PortablePathBuf,
1036 #[serde(skip_serializing_if = "Option::is_none")]
1037 script: Option<MetadataScript>,
1038 #[serde(skip_serializing_if = "Option::is_none")]
1039 workspace: Option<MetadataWorkspace>,
1040 roots: Vec<JsonRoot>,
1041 inverted: bool,
1042 #[serde(skip_serializing_if = "Vec::is_empty")]
1044 members: Vec<MetadataWorkspaceMember>,
1045 resolution: BTreeMap<String, MetadataNode>,
1046}
1047
1048impl JsonGraph {
1049 fn new(tree: &TreeDisplay<'_>, target: TreeJsonTarget<'_>) -> Self {
1050 let traversal = tree.json_traversal();
1051 let workspace_root = PortablePathBuf::from(target.root());
1052 let mut builder = JsonGraphBuilder::new(tree, workspace_root.clone());
1053
1054 for index in traversal.nodes.iter().copied() {
1055 let Node::Package(package_index) = tree.graph[index] else {
1056 continue;
1057 };
1058 builder.ensure_package(package_index, MetadataNodeKind::Package);
1059 }
1060
1061 for edge in tree
1062 .graph
1063 .edge_references()
1064 .filter(|edge| traversal.edges.contains(&edge.id()))
1065 {
1066 builder.add_package_edge(edge.source(), edge.target(), edge.weight());
1067 }
1068
1069 builder.add_target_edges(target, &traversal);
1070 let (script, workspace) = match target {
1071 TreeJsonTarget::Script(path) => {
1072 let path = PortablePathBuf::from(path);
1073 let id = builder.ensure_script(path.as_ref());
1074 (Some(MetadataScript::new(path, id)), None)
1075 }
1076 TreeJsonTarget::Workspace(path) => {
1077 let path = PortablePathBuf::from(path);
1078 let id = builder.ensure_workspace();
1079 (None, Some(MetadataWorkspace::new(path, id)))
1080 }
1081 };
1082 let roots = builder.roots(target);
1083 let members = builder.members(target);
1084 let resolution = builder.finish();
1085
1086 Self {
1087 schema: JsonSchema {
1088 version: JsonSchemaVersion::Preview,
1089 },
1090 workspace_root,
1091 script,
1092 workspace,
1093 roots,
1094 inverted: tree.invert,
1095 members,
1096 resolution,
1097 }
1098 }
1099}
1100
1101struct JsonGraphBuilder<'tree, 'env> {
1102 tree: &'tree TreeDisplay<'env>,
1103 workspace_root: PortablePathBuf,
1104 resolution: BTreeMap<String, MetadataNode>,
1105}
1106
1107impl<'tree, 'env> JsonGraphBuilder<'tree, 'env> {
1108 fn new(tree: &'tree TreeDisplay<'env>, workspace_root: PortablePathBuf) -> Self {
1109 Self {
1110 tree,
1111 workspace_root,
1112 resolution: BTreeMap::new(),
1113 }
1114 }
1115
1116 fn ensure_node(&mut self, identity: MetadataNodeId) -> String {
1117 let id = identity.to_flat();
1118 self.resolution
1119 .entry(id.clone())
1120 .or_insert_with(|| MetadataNode::new(identity));
1121 id
1122 }
1123
1124 fn ensure_package(&mut self, package_index: PackageIndex, kind: MetadataNodeKind) -> String {
1125 let package = self.tree.lock.package(package_index);
1126 let package_id = &package.id;
1127 let is_package = matches!(kind, MetadataNodeKind::Package);
1128 let id = MetadataNodeId::from_package_id(&self.workspace_root, package_id, kind.clone())
1129 .to_flat();
1130 self.resolution.entry(id.clone()).or_insert_with(|| {
1131 let mut node = MetadataNode::from_package_id(&self.workspace_root, package_id, kind);
1132 if is_package {
1133 node.set_latest_version(self.tree.latest.get(package_id).cloned());
1134 node.set_wheels_from_package(&self.workspace_root, package);
1135 }
1136 node
1137 });
1138 id
1139 }
1140
1141 fn ensure_extra(&mut self, package_index: PackageIndex, extra: &ExtraName) -> String {
1142 let package = self.ensure_package(package_index, MetadataNodeKind::Package);
1143 let extra_id = self.ensure_package(package_index, MetadataNodeKind::Extra(extra.clone()));
1144 self.add_link(
1145 package.clone(),
1146 extra_id.clone(),
1147 JsonLink::Optional(extra.clone()),
1148 );
1149 self.add_link(extra_id.clone(), package, JsonLink::Dependency(None));
1150 extra_id
1151 }
1152
1153 fn ensure_group(&mut self, package_index: PackageIndex, group: &GroupName) -> String {
1154 let package = self.ensure_package(package_index, MetadataNodeKind::Package);
1155 let group_id = self.ensure_package(package_index, MetadataNodeKind::Group(group.clone()));
1156 self.add_link(package, group_id.clone(), JsonLink::Group(group.clone()));
1157 group_id
1158 }
1159
1160 fn ensure_workspace(&mut self) -> String {
1161 self.ensure_node(MetadataNodeId::from_workspace(self.workspace_root.clone()))
1162 }
1163
1164 fn ensure_workspace_group(&mut self, group: &GroupName) -> String {
1165 let workspace = self.ensure_workspace();
1166 let group_id = self.ensure_node(MetadataNodeId::from_workspace_group(
1167 self.workspace_root.clone(),
1168 group.clone(),
1169 ));
1170 self.add_link(workspace, group_id.clone(), JsonLink::Group(group.clone()));
1171 group_id
1172 }
1173
1174 fn ensure_script(&mut self, path: &Path) -> String {
1175 self.ensure_node(MetadataNodeId::from_script(PortablePathBuf::from(path)))
1176 }
1177
1178 fn dependency_targets(
1179 &mut self,
1180 package_index: PackageIndex,
1181 extras: Option<RequestedExtras<'env>>,
1182 ) -> Vec<String> {
1183 let Some(extras) = extras.filter(|extras| !extras.is_empty()) else {
1184 return vec![self.ensure_package(package_index, MetadataNodeKind::Package)];
1185 };
1186 extras
1187 .iter()
1188 .map(|extra| self.ensure_extra(package_index, extra))
1189 .collect()
1190 }
1191
1192 fn add_package_edge(&mut self, source: NodeIndex, target: NodeIndex, edge: &Edge<'env>) {
1193 let (source, target) = if self.tree.invert {
1194 (target, source)
1195 } else {
1196 (source, target)
1197 };
1198 let (Node::Package(source), Node::Package(target)) =
1199 (&self.tree.graph[source], &self.tree.graph[target])
1200 else {
1201 return;
1202 };
1203 let (source, target) = (*source, *target);
1204
1205 let source = match edge {
1206 Edge::Prod(..) => self.ensure_package(source, MetadataNodeKind::Package),
1207 Edge::Optional(extra, ..) => self.ensure_extra(source, extra),
1208 Edge::Dev(group, ..) => self.ensure_group(source, group),
1209 };
1210 let marker = self.marker(edge);
1211 for target in self.dependency_targets(target, edge.extras()) {
1212 self.add_link(source.clone(), target, JsonLink::Dependency(marker.clone()));
1213 }
1214 }
1215
1216 fn add_target_edges(&mut self, target: TreeJsonTarget<'_>, traversal: &JsonTraversal) {
1217 let edges = self
1221 .tree
1222 .graph
1223 .edge_references()
1224 .filter(|edge| !self.tree.invert || traversal.edges.contains(&edge.id()))
1225 .filter_map(|edge| {
1226 let package = match (
1227 &self.tree.graph[edge.source()],
1228 &self.tree.graph[edge.target()],
1229 ) {
1230 (Node::Root, Node::Package(package)) | (Node::Package(package), Node::Root) => {
1231 *package
1232 }
1233 (Node::Root, Node::Root) | (Node::Package(_), Node::Package(_)) => return None,
1234 };
1235 Some((package, edge.weight()))
1236 })
1237 .collect::<Vec<_>>();
1238
1239 match target {
1240 TreeJsonTarget::Script(path) => {
1241 let script = self.ensure_script(path);
1242 for (package, edge) in edges {
1243 let marker = self.marker(edge);
1244 for package in self.dependency_targets(package, edge.extras()) {
1245 self.add_link(
1246 script.clone(),
1247 package,
1248 JsonLink::Dependency(marker.clone()),
1249 );
1250 }
1251 }
1252 }
1253 TreeJsonTarget::Workspace(_) => {
1254 self.ensure_workspace();
1255 for (package, edge) in edges {
1256 let Edge::Dev(group, ..) = edge else {
1257 continue;
1258 };
1259 let group = self.ensure_workspace_group(group);
1260 let marker = self.marker(edge);
1261 for package in self.dependency_targets(package, edge.extras()) {
1262 self.add_link(group.clone(), package, JsonLink::Dependency(marker.clone()));
1263 }
1264 }
1265 }
1266 }
1267 }
1268
1269 fn marker(&self, edge: &Edge<'_>) -> Option<String> {
1270 self.tree
1271 .lock
1272 .simplify_environment(edge.marker().pep508())
1273 .try_to_string()
1274 }
1275
1276 fn add_link(&mut self, source: String, target: String, link: JsonLink) {
1277 let (source, target) = if self.tree.invert && matches!(&link, JsonLink::Dependency(_)) {
1281 (target, source)
1282 } else {
1283 (source, target)
1284 };
1285 let Some(node) = self.resolution.get_mut(&source) else {
1286 return;
1287 };
1288 match link {
1289 JsonLink::Dependency(marker) => {
1290 node.add_resolution_dependency(target, marker);
1291 }
1292 JsonLink::Optional(name) => {
1293 node.add_optional_dependency(name, target);
1294 }
1295 JsonLink::Group(name) => {
1296 node.add_dependency_group(name, target);
1297 }
1298 }
1299 }
1300
1301 fn add_package_roots(&mut self, roots: &mut Vec<JsonRoot>, package_index: PackageIndex) {
1302 let package = self.tree.lock.package(package_index);
1303 let package_id = &package.id;
1304 let extras = package
1305 .optional_dependencies
1306 .keys()
1307 .cloned()
1308 .collect::<Vec<_>>();
1309 let groups = package
1310 .dependency_groups
1311 .keys()
1312 .filter(|group| self.tree.groups.contains(group))
1313 .cloned()
1314 .collect::<Vec<_>>();
1315
1316 if self.tree.prod {
1317 roots.push(JsonRoot {
1318 id: self.ensure_package(package_index, MetadataNodeKind::Package),
1319 });
1320 for extra in &extras {
1321 let id = MetadataNodeId::from_package_id(
1322 &self.workspace_root,
1323 package_id,
1324 MetadataNodeKind::Extra(extra.clone()),
1325 )
1326 .to_flat();
1327 if self.resolution.contains_key(&id) {
1328 roots.push(JsonRoot { id });
1329 }
1330 }
1331 }
1332
1333 for group in &groups {
1334 let id = MetadataNodeId::from_package_id(
1335 &self.workspace_root,
1336 package_id,
1337 MetadataNodeKind::Group(group.clone()),
1338 )
1339 .to_flat();
1340 if self.resolution.contains_key(&id) {
1341 roots.push(JsonRoot { id });
1342 }
1343 }
1344 }
1345
1346 fn roots(&mut self, target: TreeJsonTarget<'_>) -> Vec<JsonRoot> {
1347 let mut roots = Vec::new();
1348 for root in &self.tree.roots {
1349 match self.tree.graph[*root] {
1350 Node::Package(package_index) => {
1351 if self.tree.invert {
1352 roots.push(JsonRoot {
1353 id: self.ensure_package(package_index, MetadataNodeKind::Package),
1354 });
1355 } else {
1356 self.add_package_roots(&mut roots, package_index);
1357 }
1358 }
1359 Node::Root => match target {
1360 TreeJsonTarget::Script(path) => {
1361 let script = self.ensure_script(path);
1362 roots.push(JsonRoot { id: script });
1363 }
1364 TreeJsonTarget::Workspace(_) => {
1365 let packages = self
1366 .tree
1367 .graph
1368 .edges_directed(*root, Direction::Outgoing)
1369 .filter(|edge| matches!(edge.weight(), Edge::Prod(..)))
1370 .filter_map(|edge| {
1371 let Node::Package(package_index) = self.tree.graph[edge.target()]
1372 else {
1373 return None;
1374 };
1375 Some(package_index)
1376 })
1377 .collect::<Vec<_>>();
1378 for package_index in packages {
1379 self.add_package_roots(&mut roots, package_index);
1380 }
1381 let groups = self
1382 .tree
1383 .lock
1384 .dependency_groups()
1385 .keys()
1386 .filter(|group| self.tree.groups.contains(group))
1387 .cloned()
1388 .collect::<Vec<_>>();
1389 for group in groups {
1390 let id = MetadataNodeId::from_workspace_group(
1391 self.workspace_root.clone(),
1392 group,
1393 )
1394 .to_flat();
1395 if self.resolution.contains_key(&id) {
1396 roots.push(JsonRoot { id });
1397 }
1398 }
1399 }
1400 },
1401 }
1402 }
1403 roots.sort();
1404 roots.dedup();
1405 roots
1406 }
1407
1408 fn members(&self, target: TreeJsonTarget<'_>) -> Vec<MetadataWorkspaceMember> {
1409 if matches!(target, TreeJsonTarget::Script(_)) {
1410 return Vec::new();
1411 }
1412
1413 let packages = if self.tree.lock.members().is_empty() {
1414 self.tree.lock.root().into_iter().collect::<Vec<_>>()
1415 } else {
1416 self.tree
1417 .lock
1418 .packages()
1419 .iter()
1420 .filter(|package| self.tree.lock.members().contains(&package.id.name))
1421 .collect::<Vec<_>>()
1422 };
1423
1424 packages
1425 .into_iter()
1426 .filter(|package| {
1427 let id = MetadataNodeId::from_package_id(
1428 &self.workspace_root,
1429 &package.id,
1430 MetadataNodeKind::Package,
1431 )
1432 .to_flat();
1433 self.resolution.contains_key(&id)
1434 })
1435 .filter_map(|package| {
1436 MetadataWorkspaceMember::from_locked_package(&self.workspace_root, &package.id)
1437 })
1438 .collect()
1439 }
1440
1441 fn finish(mut self) -> BTreeMap<String, MetadataNode> {
1442 for node in self.resolution.values_mut() {
1443 node.normalize_resolution();
1444 }
1445 self.resolution
1446 }
1447}
1448
1449enum JsonLink {
1450 Dependency(Option<String>),
1451 Optional(ExtraName),
1452 Group(GroupName),
1453}
1454
1455#[derive(Debug, Serialize)]
1456struct JsonSchema {
1457 version: JsonSchemaVersion,
1458}
1459
1460#[derive(Debug, Serialize)]
1461#[serde(rename_all = "snake_case")]
1462enum JsonSchemaVersion {
1463 Preview,
1464}
1465
1466#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1467struct JsonRoot {
1468 id: String,
1469}
1470
1471#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1472struct VisitedNode<'env> {
1473 package_index: PackageIndex,
1474 expanded_extras: BTreeSet<&'env ExtraName>,
1475 marker: Option<UniversalMarker>,
1476}
1477
1478#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1479enum Node {
1480 Root,
1482 Package(PackageIndex),
1484}
1485
1486impl Node {
1487 fn sort_key(self, lock: &Lock) -> Option<&PackageId> {
1489 match self {
1490 Self::Root => None,
1491 Self::Package(index) => Some(&lock.package(index).id),
1492 }
1493 }
1494}
1495
1496#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
1497enum Edge<'env> {
1498 Prod(Option<RequestedExtras<'env>>, UniversalMarker),
1499 Optional(
1500 &'env ExtraName,
1501 Option<RequestedExtras<'env>>,
1502 UniversalMarker,
1503 ),
1504 Dev(
1505 &'env GroupName,
1506 Option<RequestedExtras<'env>>,
1507 UniversalMarker,
1508 ),
1509}
1510
1511impl<'env> Edge<'env> {
1512 fn extras(&self) -> Option<RequestedExtras<'env>> {
1513 match self {
1514 Self::Prod(extras, _) => *extras,
1515 Self::Optional(_, extras, _) => *extras,
1516 Self::Dev(_, extras, _) => *extras,
1517 }
1518 }
1519
1520 fn required_extra(&self) -> Option<&'env ExtraName> {
1521 match self {
1522 Self::Optional(extra, ..) => Some(extra),
1523 Self::Prod(..) | Self::Dev(..) => None,
1524 }
1525 }
1526
1527 fn marker(&self) -> UniversalMarker {
1528 match self {
1529 Self::Prod(_, marker) | Self::Optional(_, _, marker) | Self::Dev(_, _, marker) => {
1530 *marker
1531 }
1532 }
1533 }
1534
1535 fn is_dev(&self) -> bool {
1536 matches!(self, Self::Dev(..))
1537 }
1538
1539 fn kind(&self) -> EdgeKind<'env> {
1540 match self {
1541 Self::Prod(..) => EdgeKind::Prod,
1542 Self::Optional(extra, ..) => EdgeKind::Optional(extra),
1543 Self::Dev(group, ..) => EdgeKind::Dev(group),
1544 }
1545 }
1546}
1547
1548#[derive(Debug, Copy, Clone)]
1549enum RequestedExtras<'env> {
1550 Dependency(&'env BTreeSet<ExtraName>),
1551 Requirement(&'env [ExtraName]),
1552}
1553
1554impl PartialEq for RequestedExtras<'_> {
1555 fn eq(&self, other: &Self) -> bool {
1556 self.iter().eq(other.iter())
1557 }
1558}
1559
1560impl Eq for RequestedExtras<'_> {}
1561
1562impl PartialOrd for RequestedExtras<'_> {
1563 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1564 Some(self.cmp(other))
1565 }
1566}
1567
1568impl Ord for RequestedExtras<'_> {
1569 fn cmp(&self, other: &Self) -> Ordering {
1570 self.iter().cmp(other.iter())
1571 }
1572}
1573
1574impl<'env> RequestedExtras<'env> {
1575 fn contains(self, extra: &ExtraName) -> bool {
1576 match self {
1577 Self::Dependency(extras) => extras.contains(extra),
1578 Self::Requirement(extras) => extras.contains(extra),
1579 }
1580 }
1581
1582 fn is_empty(self) -> bool {
1583 match self {
1584 Self::Dependency(extras) => extras.is_empty(),
1585 Self::Requirement(extras) => extras.is_empty(),
1586 }
1587 }
1588
1589 fn iter(self) -> impl Iterator<Item = &'env ExtraName> {
1590 match self {
1591 Self::Dependency(extras) => Either::Left(extras.iter()),
1592 Self::Requirement(extras) => Either::Right(extras.iter()),
1593 }
1594 }
1595}
1596
1597#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
1598enum EdgeKind<'env> {
1599 Prod,
1600 Optional(&'env ExtraName),
1601 Dev(&'env GroupName),
1602}
1603
1604#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
1606struct Cursor(NodeIndex, Option<EdgeIndex>, UniversalMarker);
1607
1608impl Cursor {
1609 fn new(node: NodeIndex, edge: EdgeIndex, marker: UniversalMarker) -> Self {
1611 Self(node, Some(edge), marker)
1612 }
1613
1614 fn root(node: NodeIndex, marker: UniversalMarker) -> Self {
1616 Self(node, None, marker)
1617 }
1618
1619 fn node(&self) -> NodeIndex {
1621 self.0
1622 }
1623
1624 fn edge(&self) -> Option<EdgeIndex> {
1626 self.1
1627 }
1628
1629 fn marker(&self) -> UniversalMarker {
1631 self.2
1632 }
1633}
1634
1635impl std::fmt::Display for TreeDisplay<'_> {
1636 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1637 use owo_colors::OwoColorize;
1638
1639 let mut deduped = false;
1640 for line in self.render() {
1641 deduped |= line.contains('*');
1642 writeln!(f, "{line}")?;
1643 }
1644
1645 if deduped {
1646 let message = if self.no_dedupe {
1647 "(*) Package tree is a cycle and cannot be shown".italic()
1648 } else {
1649 "(*) Package tree already displayed".italic()
1650 };
1651 writeln!(f, "{message}")?;
1652 }
1653
1654 Ok(())
1655 }
1656}