1use std::collections::{BTreeSet, VecDeque};
2use std::fmt::Write;
3
4use either::Either;
5use itertools::Itertools;
6use owo_colors::OwoColorize;
7use petgraph::graph::{EdgeIndex, NodeIndex};
8use petgraph::prelude::EdgeRef;
9use petgraph::{Direction, Graph};
10use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
11
12use uv_configuration::DependencyGroupsWithDefaults;
13use uv_console::human_readable_bytes;
14use uv_normalize::{ExtraName, GroupName, PackageName};
15use uv_pep440::Version;
16use uv_pep508::MarkerTree;
17use uv_pypi_types::ResolverMarkerEnvironment;
18
19use crate::lock::PackageId;
20use crate::{Lock, PackageMap};
21
22#[derive(Debug)]
23pub struct TreeDisplay<'env> {
24 graph: petgraph::graph::Graph<Node<'env>, Edge<'env>, petgraph::Directed>,
26 roots: Vec<NodeIndex>,
28 latest: &'env PackageMap<Version>,
30 depth: usize,
32 no_dedupe: bool,
34 lock: &'env Lock,
36 show_sizes: bool,
38}
39
40impl<'env> TreeDisplay<'env> {
41 pub fn new(
43 lock: &'env Lock,
44 markers: Option<&'env ResolverMarkerEnvironment>,
45 latest: &'env PackageMap<Version>,
46 depth: usize,
47 prune: &[PackageName],
48 packages: &[PackageName],
49 groups: &DependencyGroupsWithDefaults,
50 no_dedupe: bool,
51 invert: bool,
52 show_sizes: bool,
53 ) -> Self {
54 let members: BTreeSet<&PackageId> = if lock.members().is_empty() {
62 lock.root().into_iter().map(|package| &package.id).collect()
63 } else {
64 lock.packages
65 .iter()
66 .filter_map(|package| {
67 if lock.members().contains(&package.id.name) {
68 Some(&package.id)
69 } else {
70 None
71 }
72 })
73 .collect()
74 };
75
76 let size_guess = lock.packages.len();
78 let mut graph =
79 Graph::<Node, Edge, petgraph::Directed>::with_capacity(size_guess, size_guess);
80 let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
81 let mut queue: VecDeque<(&PackageId, Option<&ExtraName>)> = VecDeque::new();
82 let mut seen = FxHashSet::default();
83
84 let root = graph.add_node(Node::Root);
85
86 for id in members.iter().copied() {
88 if prune.contains(&id.name) {
89 continue;
90 }
91
92 let dist = lock.find_by_id(id);
93
94 let index = *inverse
98 .entry(id)
99 .or_insert_with(|| graph.add_node(Node::Package(id)));
100
101 graph.add_edge(root, index, Edge::Prod(None));
103
104 if groups.prod() {
105 if seen.insert((id, None)) {
107 queue.push_back((id, None));
108 }
109
110 for extra in dist.optional_dependencies.keys() {
112 if seen.insert((id, Some(extra))) {
113 queue.push_back((id, Some(extra)));
114 }
115 }
116 }
117
118 for (group, dep) in dist
120 .dependency_groups
121 .iter()
122 .filter_map(|(group, deps)| {
123 if groups.contains(group) {
124 Some(deps.iter().map(move |dep| (group, dep)))
125 } else {
126 None
127 }
128 })
129 .flatten()
130 {
131 if prune.contains(&dep.package_id.name) {
132 continue;
133 }
134
135 if markers
136 .is_some_and(|markers| !dep.complexified_marker.evaluate_no_extras(markers))
137 {
138 continue;
139 }
140
141 let dep_index = *inverse
143 .entry(&dep.package_id)
144 .or_insert_with(|| graph.add_node(Node::Package(&dep.package_id)));
145
146 graph.add_edge(index, dep_index, Edge::Dev(group, Some(&dep.extra)));
148
149 if seen.insert((&dep.package_id, None)) {
151 queue.push_back((&dep.package_id, None));
152 }
153 for extra in &dep.extra {
154 if seen.insert((&dep.package_id, Some(extra))) {
155 queue.push_back((&dep.package_id, Some(extra)));
156 }
157 }
158 }
159 }
160
161 {
170 let by_name: FxHashMap<_, Vec<_>> = {
172 lock.packages().iter().fold(
173 FxHashMap::with_capacity_and_hasher(lock.len(), FxBuildHasher),
174 |mut map, package| {
175 map.entry(&package.id.name).or_default().push(package);
176 map
177 },
178 )
179 };
180
181 for requirement in lock.requirements() {
183 for package in by_name.get(&requirement.name).into_iter().flatten() {
184 let marker = if package.fork_markers.is_empty() {
187 requirement.marker
188 } else {
189 let mut combined = MarkerTree::FALSE;
190 for fork_marker in &package.fork_markers {
191 combined.or(fork_marker.pep508());
192 }
193 combined.and(requirement.marker);
194 combined
195 };
196 if marker.is_false() {
197 continue;
198 }
199 if markers.is_some_and(|markers| !marker.evaluate(markers, &[])) {
200 continue;
201 }
202 let index = inverse
204 .entry(&package.id)
205 .or_insert_with(|| graph.add_node(Node::Package(&package.id)));
206
207 graph.add_edge(root, *index, Edge::Prod(None));
209
210 if seen.insert((&package.id, None)) {
212 queue.push_back((&package.id, None));
213 }
214 }
215 }
216
217 for (group, requirements) in lock.dependency_groups() {
219 for requirement in requirements {
220 for package in by_name.get(&requirement.name).into_iter().flatten() {
221 let marker = if package.fork_markers.is_empty() {
224 requirement.marker
225 } else {
226 let mut combined = MarkerTree::FALSE;
227 for fork_marker in &package.fork_markers {
228 combined.or(fork_marker.pep508());
229 }
230 combined.and(requirement.marker);
231 combined
232 };
233 if marker.is_false() {
234 continue;
235 }
236 if markers.is_some_and(|markers| !marker.evaluate(markers, &[])) {
237 continue;
238 }
239 let index = inverse
241 .entry(&package.id)
242 .or_insert_with(|| graph.add_node(Node::Package(&package.id)));
243
244 graph.add_edge(root, *index, Edge::Dev(group, None));
246
247 if seen.insert((&package.id, None)) {
249 queue.push_back((&package.id, None));
250 }
251 }
252 }
253 }
254 }
255
256 while let Some((id, extra)) = queue.pop_front() {
258 let index = inverse[&id];
259 let package = lock.find_by_id(id);
260
261 let deps = if let Some(extra) = extra {
262 Either::Left(
263 package
264 .optional_dependencies
265 .get(extra)
266 .into_iter()
267 .flatten(),
268 )
269 } else {
270 Either::Right(package.dependencies.iter())
271 };
272
273 for dep in deps {
274 if prune.contains(&dep.package_id.name) {
275 continue;
276 }
277
278 if markers
279 .is_some_and(|markers| !dep.complexified_marker.evaluate_no_extras(markers))
280 {
281 continue;
282 }
283
284 let dep_index = *inverse
286 .entry(&dep.package_id)
287 .or_insert_with(|| graph.add_node(Node::Package(&dep.package_id)));
288
289 graph.add_edge(
291 index,
292 dep_index,
293 if let Some(extra) = extra {
294 Edge::Optional(extra, Some(&dep.extra))
295 } else {
296 Edge::Prod(Some(&dep.extra))
297 },
298 );
299
300 if seen.insert((&dep.package_id, None)) {
302 queue.push_back((&dep.package_id, None));
303 }
304 for extra in &dep.extra {
305 if seen.insert((&dep.package_id, Some(extra))) {
306 queue.push_back((&dep.package_id, Some(extra)));
307 }
308 }
309 }
310 }
311
312 {
314 let mut reachable = graph
315 .node_indices()
316 .filter(|index| match graph[*index] {
317 Node::Package(package_id) => members.contains(package_id),
318 Node::Root => true,
319 })
320 .collect::<FxHashSet<_>>();
321 let mut stack = reachable.iter().copied().collect::<VecDeque<_>>();
322 while let Some(node) = stack.pop_front() {
323 for edge in graph.edges_directed(node, Direction::Outgoing) {
324 if reachable.insert(edge.target()) {
325 stack.push_back(edge.target());
326 }
327 }
328 }
329
330 graph.retain_nodes(|_, index| reachable.contains(&index));
332 }
333
334 if invert {
336 graph.reverse();
337 }
338
339 if !packages.is_empty() {
341 let mut reachable = graph
342 .node_indices()
343 .filter(|index| {
344 let Node::Package(package_id) = graph[*index] else {
345 return false;
346 };
347 packages.contains(&package_id.name)
348 })
349 .collect::<FxHashSet<_>>();
350 let mut stack = reachable.iter().copied().collect::<VecDeque<_>>();
351 while let Some(node) = stack.pop_front() {
352 for edge in graph.edges_directed(node, Direction::Outgoing) {
353 if reachable.insert(edge.target()) {
354 stack.push_back(edge.target());
355 }
356 }
357 }
358
359 graph.retain_nodes(|_, index| reachable.contains(&index));
361 }
362
363 let roots = {
365 if !packages.is_empty() {
367 let mut roots = graph
368 .node_indices()
369 .filter(|index| {
370 let Node::Package(package_id) = graph[*index] else {
371 return false;
372 };
373 packages.contains(&package_id.name)
374 })
375 .collect::<Vec<_>>();
376
377 roots.sort_by_key(|index| &graph[*index]);
379
380 roots
381 } else {
382 let mut edges = vec![];
383
384 let feedback_set: Vec<EdgeIndex> = petgraph::algo::greedy_feedback_arc_set(&graph)
386 .map(|e| e.id())
387 .collect();
388 for edge_id in feedback_set {
389 if let Some((source, target)) = graph.edge_endpoints(edge_id) {
390 if let Some(weight) = graph.remove_edge(edge_id) {
391 edges.push((source, target, weight));
392 }
393 }
394 }
395
396 let mut roots = graph
398 .node_indices()
399 .filter(|index| {
400 graph
401 .edges_directed(*index, Direction::Incoming)
402 .next()
403 .is_none()
404 })
405 .collect::<Vec<_>>();
406
407 roots.sort_by_key(|index| &graph[*index]);
409
410 for (source, target, weight) in edges {
412 graph.add_edge(source, target, weight);
413 }
414
415 roots
416 }
417 };
418
419 Self {
420 graph,
421 roots,
422 latest,
423 depth,
424 no_dedupe,
425 lock,
426 show_sizes,
427 }
428 }
429
430 fn visit(
432 &'env self,
433 cursor: Cursor,
434 visited: &mut FxHashMap<&'env PackageId, Vec<&'env PackageId>>,
435 path: &mut Vec<&'env PackageId>,
436 ) -> Vec<String> {
437 if path.len() > self.depth {
439 return Vec::new();
440 }
441
442 let Node::Package(package_id) = self.graph[cursor.node()] else {
443 return Vec::new();
444 };
445 let edge = cursor.edge().map(|edge_id| &self.graph[edge_id]);
446
447 let line = {
448 let mut line = format!("{}", package_id.name);
449
450 if let Some(extras) = edge.and_then(Edge::extras) {
451 if !extras.is_empty() {
452 line.push('[');
453 line.push_str(extras.iter().join(", ").as_str());
454 line.push(']');
455 }
456 }
457
458 if let Some(version) = package_id.version.as_ref() {
459 line.push(' ');
460 line.push('v');
461 let _ = write!(line, "{version}");
462 }
463
464 if let Some(edge) = edge {
465 match edge {
466 Edge::Prod(_) => {}
467 Edge::Optional(extra, _) => {
468 let _ = write!(line, " (extra: {extra})");
469 }
470 Edge::Dev(group, _) => {
471 let _ = write!(line, " (group: {group})");
472 }
473 }
474 }
475
476 if self.show_sizes {
479 let package = self.lock.find_by_id(package_id);
480 if let Some(size_bytes) = package.wheels.iter().find_map(|wheel| wheel.size) {
481 let (bytes, unit) = human_readable_bytes(size_bytes);
482 line.push(' ');
483 line.push_str(format!("{}", format!("({bytes:.1}{unit})").dimmed()).as_str());
484 }
485 }
486
487 line
488 };
489
490 if let Some(requirements) = visited.get(package_id) {
494 if !self.no_dedupe || path.contains(&package_id) {
495 return if requirements.is_empty() {
496 vec![line]
497 } else {
498 vec![format!("{line} (*)")]
499 };
500 }
501 }
502
503 let line = if let Some(version) = self.latest.get(package_id) {
505 format!("{line} {}", format!("(latest: v{version})").bold().cyan())
506 } else {
507 line
508 };
509
510 let mut dependencies = self
511 .graph
512 .edges_directed(cursor.node(), Direction::Outgoing)
513 .filter_map(|edge| match self.graph[edge.target()] {
514 Node::Root => None,
515 Node::Package(_) => Some(Cursor::new(edge.target(), edge.id())),
516 })
517 .collect::<Vec<_>>();
518 dependencies.sort_by_key(|cursor| {
519 let node = &self.graph[cursor.node()];
520 let edge = cursor
521 .edge()
522 .map(|edge_id| &self.graph[edge_id])
523 .map(Edge::kind);
524 (edge, node)
525 });
526
527 let mut lines = vec![line];
528
529 visited.insert(
531 package_id,
532 dependencies
533 .iter()
534 .filter_map(|node| match self.graph[node.node()] {
535 Node::Package(package_id) => Some(package_id),
536 Node::Root => None,
537 })
538 .collect(),
539 );
540 path.push(package_id);
541
542 for (index, dep) in dependencies.iter().enumerate() {
543 let (prefix_top, prefix_rest) = if dependencies.len() - 1 == index {
563 ("└── ", " ")
564 } else {
565 ("├── ", "│ ")
566 };
567 for (visited_index, visited_line) in self.visit(*dep, visited, path).iter().enumerate()
568 {
569 let prefix = if visited_index == 0 {
570 prefix_top
571 } else {
572 prefix_rest
573 };
574 lines.push(format!("{prefix}{visited_line}"));
575 }
576 }
577
578 path.pop();
579
580 lines
581 }
582
583 fn render(&self) -> Vec<String> {
585 let mut path = Vec::new();
586 let mut lines = Vec::with_capacity(self.graph.node_count());
587 let mut visited =
588 FxHashMap::with_capacity_and_hasher(self.graph.node_count(), FxBuildHasher);
589
590 for node in &self.roots {
591 match self.graph[*node] {
592 Node::Root => {
593 for edge in self.graph.edges_directed(*node, Direction::Outgoing) {
594 let node = edge.target();
595 path.clear();
596 lines.extend(self.visit(
597 Cursor::new(node, edge.id()),
598 &mut visited,
599 &mut path,
600 ));
601 }
602 }
603 Node::Package(_) => {
604 path.clear();
605 lines.extend(self.visit(Cursor::root(*node), &mut visited, &mut path));
606 }
607 }
608 }
609
610 lines
611 }
612}
613
614#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
615enum Node<'env> {
616 Root,
618 Package(&'env PackageId),
620}
621
622#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
623enum Edge<'env> {
624 Prod(Option<&'env BTreeSet<ExtraName>>),
625 Optional(&'env ExtraName, Option<&'env BTreeSet<ExtraName>>),
626 Dev(&'env GroupName, Option<&'env BTreeSet<ExtraName>>),
627}
628
629impl<'env> Edge<'env> {
630 fn extras(&self) -> Option<&'env BTreeSet<ExtraName>> {
631 match self {
632 Self::Prod(extras) => *extras,
633 Self::Optional(_, extras) => *extras,
634 Self::Dev(_, extras) => *extras,
635 }
636 }
637
638 fn kind(&self) -> EdgeKind<'env> {
639 match self {
640 Self::Prod(_) => EdgeKind::Prod,
641 Self::Optional(extra, _) => EdgeKind::Optional(extra),
642 Self::Dev(group, _) => EdgeKind::Dev(group),
643 }
644 }
645}
646
647#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
648enum EdgeKind<'env> {
649 Prod,
650 Optional(&'env ExtraName),
651 Dev(&'env GroupName),
652}
653
654#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
656struct Cursor(NodeIndex, Option<EdgeIndex>);
657
658impl Cursor {
659 fn new(node: NodeIndex, edge: EdgeIndex) -> Self {
661 Self(node, Some(edge))
662 }
663
664 fn root(node: NodeIndex) -> Self {
666 Self(node, None)
667 }
668
669 fn node(&self) -> NodeIndex {
671 self.0
672 }
673
674 fn edge(&self) -> Option<EdgeIndex> {
676 self.1
677 }
678}
679
680impl std::fmt::Display for TreeDisplay<'_> {
681 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
682 use owo_colors::OwoColorize;
683
684 let mut deduped = false;
685 for line in self.render() {
686 deduped |= line.contains('*');
687 writeln!(f, "{line}")?;
688 }
689
690 if deduped {
691 let message = if self.no_dedupe {
692 "(*) Package tree is a cycle and cannot be shown".italic()
693 } else {
694 "(*) Package tree already displayed".italic()
695 };
696 writeln!(f, "{message}")?;
697 }
698
699 Ok(())
700 }
701}