Skip to main content

uv_resolver/lock/
tree.rs

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    /// The constructed dependency graph.
25    graph: petgraph::graph::Graph<Node<'env>, Edge<'env>, petgraph::Directed>,
26    /// The packages considered as roots of the dependency tree.
27    roots: Vec<NodeIndex>,
28    /// The latest known version of each package.
29    latest: &'env PackageMap<Version>,
30    /// Maximum display depth of the dependency tree.
31    depth: usize,
32    /// Whether to de-duplicate the displayed dependencies.
33    no_dedupe: bool,
34    /// Reference to the lock to look up additional metadata (e.g., wheel sizes).
35    lock: &'env Lock,
36    /// Whether to show sizes in the rendered output.
37    show_sizes: bool,
38}
39
40impl<'env> TreeDisplay<'env> {
41    /// Create a new [`DisplayDependencyGraph`] for the set of installed packages.
42    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        // Identify any workspace members.
55        //
56        // These include:
57        // - The members listed in the lockfile.
58        // - The root package, if it's not in the list of members. (The root package is omitted from
59        //   the list of workspace members for single-member workspaces with a `[project]` section,
60        //   to avoid cluttering the lockfile.
61        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        // Create a graph.
77        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        // Add the root packages to the graph.
87        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            // Add the workspace package to the graph. Under `--only-group`, the workspace member
95            // may not be installed, but it's still relevant for the dependency tree, since we want
96            // to show the connection from the workspace package to the enabled dependency groups.
97            let index = *inverse
98                .entry(id)
99                .or_insert_with(|| graph.add_node(Node::Package(id)));
100
101            // Add an edge from the root.
102            graph.add_edge(root, index, Edge::Prod(None));
103
104            if groups.prod() {
105                // Push its dependencies on the queue.
106                if seen.insert((id, None)) {
107                    queue.push_back((id, None));
108                }
109
110                // Push any extras on the queue.
111                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            // Add any development dependencies.
119            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                // Add the dependency to the graph and get its index.
142                let dep_index = *inverse
143                    .entry(&dep.package_id)
144                    .or_insert_with(|| graph.add_node(Node::Package(&dep.package_id)));
145
146                // Add an edge from the workspace package.
147                graph.add_edge(index, dep_index, Edge::Dev(group, Some(&dep.extra)));
148
149                // Push its dependencies on the queue.
150                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        // Identify any packages that are connected directly to the synthetic root node, i.e.,
162        // requirements that are attached to the workspace itself.
163        //
164        // These include
165        // - `[dependency-groups]` dependencies for workspaces whose roots do not include a
166        //    `[project]` table, since those roots are not workspace members, but they _can_ define
167        //    dependencies.
168        // - `dependencies` in PEP 723 scripts.
169        {
170            // Index the lockfile by name.
171            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            // Identify any requirements attached to the workspace itself.
182            for requirement in lock.requirements() {
183                for package in by_name.get(&requirement.name).into_iter().flatten() {
184                    // Determine whether this entry is "relevant" for the requirement, by intersecting
185                    // the markers.
186                    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                    // Add the package to the graph.
203                    let index = inverse
204                        .entry(&package.id)
205                        .or_insert_with(|| graph.add_node(Node::Package(&package.id)));
206
207                    // Add an edge from the root.
208                    graph.add_edge(root, *index, Edge::Prod(None));
209
210                    // Push its dependencies on the queue.
211                    if seen.insert((&package.id, None)) {
212                        queue.push_back((&package.id, None));
213                    }
214                }
215            }
216
217            // Identify any dependency groups attached to the workspace itself.
218            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                        // Determine whether this entry is "relevant" for the requirement, by intersecting
222                        // the markers.
223                        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                        // Add the package to the graph.
240                        let index = inverse
241                            .entry(&package.id)
242                            .or_insert_with(|| graph.add_node(Node::Package(&package.id)));
243
244                        // Add an edge from the root.
245                        graph.add_edge(root, *index, Edge::Dev(group, None));
246
247                        // Push its dependencies on the queue.
248                        if seen.insert((&package.id, None)) {
249                            queue.push_back((&package.id, None));
250                        }
251                    }
252                }
253            }
254        }
255
256        // Create all the relevant nodes.
257        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                // Add the dependency to the graph.
285                let dep_index = *inverse
286                    .entry(&dep.package_id)
287                    .or_insert_with(|| graph.add_node(Node::Package(&dep.package_id)));
288
289                // Add an edge from the workspace package.
290                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                // Push its dependencies on the queue.
301                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        // Filter the graph to remove any unreachable nodes.
313        {
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            // Remove the unreachable nodes from the graph.
331            graph.retain_nodes(|_, index| reachable.contains(&index));
332        }
333
334        // Reverse the graph.
335        if invert {
336            graph.reverse();
337        }
338
339        // Filter the graph to those nodes reachable from the target packages.
340        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            // Remove the unreachable nodes from the graph.
360            graph.retain_nodes(|_, index| reachable.contains(&index));
361        }
362
363        // Compute the list of roots.
364        let roots = {
365            // If specific packages were requested, use them as roots.
366            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                // Sort the roots.
378                roots.sort_by_key(|index| &graph[*index]);
379
380                roots
381            } else {
382                let mut roots = if invert {
383                    // For inverted trees, find leaf packages (nodes with no incoming
384                    // edges).
385                    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                    // For non-inverted trees, use the root node directly.
396                    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    /// Perform a depth-first traversal of the given package and its dependencies.
419    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        // Short-circuit if the current path is longer than the provided depth.
426        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            // Append compressed wheel size, if available in the lockfile.
465            // Keep it simple: use the first wheel entry that includes a size.
466            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        // Skip the traversal if:
479        // 1. The package is in the current traversal path (i.e., a dependency cycle).
480        // 2. The package has been visited and de-duplication is enabled (default).
481        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        // Incorporate the latest version of the package, if known.
492        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        // Keep track of the dependency path to avoid cycles.
518        // Only mark as visited if we're going to expand children (not at depth limit).
519        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            // For sub-visited packages, add the prefix to make the tree display user-friendly.
535            // The key observation here is you can group the tree as follows when you're at the
536            // root of the tree:
537            // root_package
538            // ├── level_1_0          // Group 1
539            // │   ├── level_2_0      ...
540            // │   │   ├── level_3_0  ...
541            // │   │   └── level_3_1  ...
542            // │   └── level_2_1      ...
543            // ├── level_1_1          // Group 2
544            // │   ├── level_2_2      ...
545            // │   └── level_2_3      ...
546            // └── level_1_2          // Group 3
547            //     └── level_2_4      ...
548            //
549            // The lines in Group 1 and 2 have `├── ` at the top and `|   ` at the rest while
550            // those in Group 3 have `└── ` at the top and `    ` at the rest.
551            // This observation is true recursively even when looking at the subtree rooted
552            // at `level_1_0`.
553            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    /// Depth-first traverse the nodes to render the tree.
575    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    /// The synthetic root node.
608    Root,
609    /// A package in the dependency graph.
610    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/// A node in the dependency graph along with the edge that led to it, or `None` for root nodes.
646#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
647struct Cursor(NodeIndex, Option<EdgeIndex>);
648
649impl Cursor {
650    /// Create a [`Cursor`] representing a node in the dependency tree.
651    fn new(node: NodeIndex, edge: EdgeIndex) -> Self {
652        Self(node, Some(edge))
653    }
654
655    /// Create a [`Cursor`] representing a root node in the dependency tree.
656    fn root(node: NodeIndex) -> Self {
657        Self(node, None)
658    }
659
660    /// Return the [`NodeIndex`] of the node.
661    fn node(&self) -> NodeIndex {
662        self.0
663    }
664
665    /// Return the [`EdgeIndex`] of the edge that led to the node, if any.
666    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}