use std::borrow::Cow; use std::collections::BTreeSet; use itertools::Itertools; use rustc_hash::{FxHashMap, FxHashSet}; use uv_configuration::DevMode; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pypi_types::ResolverMarkerEnvironment; use crate::lock::{Dependency, PackageId}; use crate::Lock; #[derive(Debug)] pub struct TreeDisplay<'env> { /// The root nodes in the [`Lock`]. roots: Vec<&'env PackageId>, /// The edges in the [`Lock`]. /// /// While the dependencies exist on the [`Lock`] directly, if `--invert` is enabled, the /// direction must be inverted when constructing the tree. dependencies: FxHashMap<&'env PackageId, Vec>>, optional_dependencies: FxHashMap<&'env PackageId, FxHashMap>>>, dev_dependencies: FxHashMap<&'env PackageId, FxHashMap>>>, /// Maximum display depth of the dependency tree. depth: usize, /// Whether to include development dependencies in the display. dev: DevMode, /// Prune the given packages from the display of the dependency tree. prune: Vec, /// Display only the specified packages. packages: Vec, /// Whether to de-duplicate the displayed dependencies. no_dedupe: bool, } impl<'env> TreeDisplay<'env> { /// Create a new [`DisplayDependencyGraph`] for the set of installed packages. pub fn new( lock: &'env Lock, markers: Option<&'env ResolverMarkerEnvironment>, depth: usize, prune: Vec, packages: Vec, dev: DevMode, no_dedupe: bool, invert: bool, ) -> Self { let mut non_roots = FxHashSet::default(); // Index all the dependencies. We could read these from the `Lock` directly, but we have to // support `--invert`, so we might as well build them up in either case. let mut dependencies: FxHashMap<_, Vec<_>> = FxHashMap::default(); let mut optional_dependencies: FxHashMap<_, FxHashMap<_, Vec<_>>> = FxHashMap::default(); let mut dev_dependencies: FxHashMap<_, FxHashMap<_, Vec<_>>> = FxHashMap::default(); for package in &lock.packages { for dependency in &package.dependencies { // Skip dependencies that don't apply to the current environment. if let Some(environment_markers) = markers { if !dependency .complexified_marker .evaluate(environment_markers, &[]) { non_roots.insert(dependency.package_id.clone()); continue; } } let parent = if invert { &dependency.package_id } else { &package.id }; let child = if invert { Cow::Owned(Dependency { package_id: package.id.clone(), extra: dependency.extra.clone(), simplified_marker: dependency.simplified_marker.clone(), complexified_marker: dependency.complexified_marker.clone(), }) } else { Cow::Borrowed(dependency) }; non_roots.insert(child.package_id.clone()); dependencies.entry(parent).or_default().push(child); } for (extra, dependencies) in &package.optional_dependencies { for dependency in dependencies { // Skip dependencies that don't apply to the current environment. if let Some(environment_markers) = markers { if !dependency .complexified_marker .evaluate(environment_markers, &[]) { non_roots.insert(dependency.package_id.clone()); continue; } } let parent = if invert { &dependency.package_id } else { &package.id }; let child = if invert { Cow::Owned(Dependency { package_id: package.id.clone(), extra: dependency.extra.clone(), simplified_marker: dependency.simplified_marker.clone(), complexified_marker: dependency.complexified_marker.clone(), }) } else { Cow::Borrowed(dependency) }; non_roots.insert(child.package_id.clone()); optional_dependencies .entry(parent) .or_default() .entry(extra.clone()) .or_default() .push(child); } } for (group, dependencies) in &package.dev_dependencies { for dependency in dependencies { // Skip dependencies that don't apply to the current environment. if let Some(environment_markers) = markers { if !dependency .complexified_marker .evaluate(environment_markers, &[]) { non_roots.insert(dependency.package_id.clone()); continue; } } let parent = if invert { &dependency.package_id } else { &package.id }; let child = if invert { Cow::Owned(Dependency { package_id: package.id.clone(), extra: dependency.extra.clone(), simplified_marker: dependency.simplified_marker.clone(), complexified_marker: dependency.complexified_marker.clone(), }) } else { Cow::Borrowed(dependency) }; non_roots.insert(child.package_id.clone()); dev_dependencies .entry(parent) .or_default() .entry(group.clone()) .or_default() .push(child); } } } // Compute the root nodes. let roots = lock .packages .iter() .map(|dist| &dist.id) .filter(|id| !non_roots.contains(*id)) .collect::>(); Self { roots, dependencies, optional_dependencies, dev_dependencies, depth, dev, prune, packages, no_dedupe, } } /// Perform a depth-first traversal of the given package and its dependencies. fn visit( &'env self, node: Node<'env>, visited: &mut FxHashMap<&'env PackageId, Vec<&'env PackageId>>, path: &mut Vec<&'env PackageId>, ) -> Vec { // Short-circuit if the current path is longer than the provided depth. if path.len() > self.depth { return Vec::new(); } let line = { let mut line = format!("{}", node.package_id().name); if let Some(extras) = node.extras().filter(|extras| !extras.is_empty()) { line.push_str(&format!("[{}]", extras.iter().join(","))); } line.push_str(&format!(" v{}", node.package_id().version)); match node { Node::Root(_) => line, Node::Dependency(_) => line, Node::OptionalDependency(extra, _) => format!("{line} (extra: {extra})"), Node::DevDependency(group, _) => format!("{line} (group: {group})"), } }; // Skip the traversal if: // 1. The package is in the current traversal path (i.e., a dependency cycle). // 2. The package has been visited and de-duplication is enabled (default). if let Some(requirements) = visited.get(node.package_id()) { if !self.no_dedupe || path.contains(&node.package_id()) { return if requirements.is_empty() { vec![line] } else { vec![format!("{} (*)", line)] }; } } let dependencies: Vec> = self .dependencies .get(node.package_id()) .filter(|_| self.dev != DevMode::Only) .into_iter() .flatten() .map(|dep| Node::Dependency(dep.as_ref())) .chain( self.optional_dependencies .get(node.package_id()) .filter(|_| self.dev != DevMode::Only) .into_iter() .flatten() .flat_map(|(extra, deps)| { deps.iter() .map(move |dep| Node::OptionalDependency(extra, dep)) }), ) .chain( self.dev_dependencies .get(node.package_id()) .filter(|_| self.dev != DevMode::Exclude) .into_iter() .flatten() .flat_map(|(group, deps)| { deps.iter().map(move |dep| Node::DevDependency(group, dep)) }), ) .filter(|dep| !self.prune.contains(&dep.package_id().name)) .collect::>(); let mut lines = vec![line]; // Keep track of the dependency path to avoid cycles. visited.insert( node.package_id(), dependencies.iter().map(Node::package_id).collect(), ); path.push(node.package_id()); for (index, dep) in dependencies.iter().enumerate() { // For sub-visited packages, add the prefix to make the tree display user-friendly. // The key observation here is you can group the tree as follows when you're at the // root of the tree: // root_package // ├── level_1_0 // Group 1 // │ ├── level_2_0 ... // │ │ ├── level_3_0 ... // │ │ └── level_3_1 ... // │ └── level_2_1 ... // ├── level_1_1 // Group 2 // │ ├── level_2_2 ... // │ └── level_2_3 ... // └── level_1_2 // Group 3 // └── level_2_4 ... // // The lines in Group 1 and 2 have `├── ` at the top and `| ` at the rest while // those in Group 3 have `└── ` at the top and ` ` at the rest. // This observation is true recursively even when looking at the subtree rooted // at `level_1_0`. let (prefix_top, prefix_rest) = if dependencies.len() - 1 == index { ("└── ", " ") } else { ("├── ", "│ ") }; for (visited_index, visited_line) in self.visit(*dep, visited, path).iter().enumerate() { let prefix = if visited_index == 0 { prefix_top } else { prefix_rest }; lines.push(format!("{prefix}{visited_line}")); } } path.pop(); lines } /// Depth-first traverse the nodes to render the tree. fn render(&self) -> Vec { let mut visited = FxHashMap::default(); let mut path = Vec::new(); let mut lines = Vec::new(); if self.packages.is_empty() { for id in &self.roots { path.clear(); lines.extend(self.visit(Node::Root(id), &mut visited, &mut path)); } } else { let by_package: FxHashMap<_, _> = self.roots.iter().map(|id| (&id.name, id)).collect(); for package in &self.packages { if let Some(id) = by_package.get(package) { path.clear(); lines.extend(self.visit(Node::Root(id), &mut visited, &mut path)); } } } lines } } #[derive(Debug, Copy, Clone)] enum Node<'env> { Root(&'env PackageId), Dependency(&'env Dependency), OptionalDependency(&'env ExtraName, &'env Dependency), DevDependency(&'env GroupName, &'env Dependency), } impl<'env> Node<'env> { fn package_id(&self) -> &'env PackageId { match self { Self::Root(id) => id, Self::Dependency(dep) => &dep.package_id, Self::OptionalDependency(_, dep) => &dep.package_id, Self::DevDependency(_, dep) => &dep.package_id, } } fn extras(&self) -> Option<&BTreeSet> { match self { Self::Root(_) => None, Self::Dependency(dep) => Some(&dep.extra), Self::OptionalDependency(_, dep) => Some(&dep.extra), Self::DevDependency(_, dep) => Some(&dep.extra), } } } impl std::fmt::Display for TreeDisplay<'_> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { use owo_colors::OwoColorize; let mut deduped = false; for line in self.render() { deduped |= line.contains('*'); writeln!(f, "{line}")?; } if deduped { let message = if self.no_dedupe { "(*) Package tree is a cycle and cannot be shown".italic() } else { "(*) Package tree already displayed".italic() }; writeln!(f, "{message}")?; } Ok(()) } }