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 roots = if invert {
383 graph
386 .node_indices()
387 .filter(|index| {
388 graph
389 .edges_directed(*index, Direction::Incoming)
390 .next()
391 .is_none()
392 })
393 .collect::<Vec<_>>()
394 } else {
395 graph
397 .node_indices()
398 .filter(|index| matches!(graph[*index], Node::Root))
399 .collect::<Vec<_>>()
400 };
401
402 roots.sort_by_key(|index| &graph[*index]);
403 roots
404 }
405 };
406
407 Self {
408 graph,
409 roots,
410 latest,
411 depth,
412 no_dedupe,
413 lock,
414 show_sizes,
415 }
416 }
417
418 fn visit(
420 &'env self,
421 cursor: Cursor,
422 visited: &mut FxHashMap<&'env PackageId, Vec<&'env PackageId>>,
423 path: &mut Vec<&'env PackageId>,
424 ) -> Vec<String> {
425 if path.len() > self.depth {
427 return Vec::new();
428 }
429
430 let Node::Package(package_id) = self.graph[cursor.node()] else {
431 return Vec::new();
432 };
433 let edge = cursor.edge().map(|edge_id| &self.graph[edge_id]);
434
435 let line = {
436 let mut line = format!("{}", package_id.name);
437
438 if let Some(extras) = edge.and_then(Edge::extras) {
439 if !extras.is_empty() {
440 line.push('[');
441 line.push_str(extras.iter().join(", ").as_str());
442 line.push(']');
443 }
444 }
445
446 if let Some(version) = package_id.version.as_ref() {
447 line.push(' ');
448 line.push('v');
449 let _ = write!(line, "{version}");
450 }
451
452 if let Some(edge) = edge {
453 match edge {
454 Edge::Prod(_) => {}
455 Edge::Optional(extra, _) => {
456 let _ = write!(line, " (extra: {extra})");
457 }
458 Edge::Dev(group, _) => {
459 let _ = write!(line, " (group: {group})");
460 }
461 }
462 }
463
464 if self.show_sizes {
467 let package = self.lock.find_by_id(package_id);
468 if let Some(size_bytes) = package.wheels.iter().find_map(|wheel| wheel.size) {
469 let (bytes, unit) = human_readable_bytes(size_bytes);
470 line.push(' ');
471 line.push_str(format!("{}", format!("({bytes:.1}{unit})").dimmed()).as_str());
472 }
473 }
474
475 line
476 };
477
478 if let Some(requirements) = visited.get(package_id) {
482 if !self.no_dedupe || path.contains(&package_id) {
483 return if requirements.is_empty() {
484 vec![line]
485 } else {
486 vec![format!("{line} (*)")]
487 };
488 }
489 }
490
491 let line = if let Some(version) = self.latest.get(package_id) {
493 format!("{line} {}", format!("(latest: v{version})").bold().cyan())
494 } else {
495 line
496 };
497
498 let mut dependencies = self
499 .graph
500 .edges_directed(cursor.node(), Direction::Outgoing)
501 .filter_map(|edge| match self.graph[edge.target()] {
502 Node::Root => None,
503 Node::Package(_) => Some(Cursor::new(edge.target(), edge.id())),
504 })
505 .collect::<Vec<_>>();
506 dependencies.sort_by_key(|cursor| {
507 let node = &self.graph[cursor.node()];
508 let edge = cursor
509 .edge()
510 .map(|edge_id| &self.graph[edge_id])
511 .map(Edge::kind);
512 (edge, node)
513 });
514
515 let mut lines = vec![line];
516
517 if path.len() < self.depth {
520 visited.insert(
521 package_id,
522 dependencies
523 .iter()
524 .filter_map(|node| match self.graph[node.node()] {
525 Node::Package(package_id) => Some(package_id),
526 Node::Root => None,
527 })
528 .collect(),
529 );
530 }
531 path.push(package_id);
532
533 for (index, dep) in dependencies.iter().enumerate() {
534 let (prefix_top, prefix_rest) = if dependencies.len() - 1 == index {
554 ("└── ", " ")
555 } else {
556 ("├── ", "│ ")
557 };
558 for (visited_index, visited_line) in self.visit(*dep, visited, path).iter().enumerate()
559 {
560 let prefix = if visited_index == 0 {
561 prefix_top
562 } else {
563 prefix_rest
564 };
565 lines.push(format!("{prefix}{visited_line}"));
566 }
567 }
568
569 path.pop();
570
571 lines
572 }
573
574 fn render(&self) -> Vec<String> {
576 let mut path = Vec::new();
577 let mut lines = Vec::with_capacity(self.graph.node_count());
578 let mut visited =
579 FxHashMap::with_capacity_and_hasher(self.graph.node_count(), FxBuildHasher);
580
581 for node in &self.roots {
582 match self.graph[*node] {
583 Node::Root => {
584 for edge in self.graph.edges_directed(*node, Direction::Outgoing) {
585 let node = edge.target();
586 path.clear();
587 lines.extend(self.visit(
588 Cursor::new(node, edge.id()),
589 &mut visited,
590 &mut path,
591 ));
592 }
593 }
594 Node::Package(_) => {
595 path.clear();
596 lines.extend(self.visit(Cursor::root(*node), &mut visited, &mut path));
597 }
598 }
599 }
600
601 lines
602 }
603}
604
605#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
606enum Node<'env> {
607 Root,
609 Package(&'env PackageId),
611}
612
613#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
614enum Edge<'env> {
615 Prod(Option<&'env BTreeSet<ExtraName>>),
616 Optional(&'env ExtraName, Option<&'env BTreeSet<ExtraName>>),
617 Dev(&'env GroupName, Option<&'env BTreeSet<ExtraName>>),
618}
619
620impl<'env> Edge<'env> {
621 fn extras(&self) -> Option<&'env BTreeSet<ExtraName>> {
622 match self {
623 Self::Prod(extras) => *extras,
624 Self::Optional(_, extras) => *extras,
625 Self::Dev(_, extras) => *extras,
626 }
627 }
628
629 fn kind(&self) -> EdgeKind<'env> {
630 match self {
631 Self::Prod(_) => EdgeKind::Prod,
632 Self::Optional(extra, _) => EdgeKind::Optional(extra),
633 Self::Dev(group, _) => EdgeKind::Dev(group),
634 }
635 }
636}
637
638#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
639enum EdgeKind<'env> {
640 Prod,
641 Optional(&'env ExtraName),
642 Dev(&'env GroupName),
643}
644
645#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
647struct Cursor(NodeIndex, Option<EdgeIndex>);
648
649impl Cursor {
650 fn new(node: NodeIndex, edge: EdgeIndex) -> Self {
652 Self(node, Some(edge))
653 }
654
655 fn root(node: NodeIndex) -> Self {
657 Self(node, None)
658 }
659
660 fn node(&self) -> NodeIndex {
662 self.0
663 }
664
665 fn edge(&self) -> Option<EdgeIndex> {
667 self.1
668 }
669}
670
671impl std::fmt::Display for TreeDisplay<'_> {
672 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
673 use owo_colors::OwoColorize;
674
675 let mut deduped = false;
676 for line in self.render() {
677 deduped |= line.contains('*');
678 writeln!(f, "{line}")?;
679 }
680
681 if deduped {
682 let message = if self.no_dedupe {
683 "(*) Package tree is a cycle and cannot be shown".italic()
684 } else {
685 "(*) Package tree already displayed".italic()
686 };
687 writeln!(f, "{message}")?;
688 }
689
690 Ok(())
691 }
692}