mirror of https://github.com/astral-sh/uv
392 lines
14 KiB
Rust
392 lines
14 KiB
Rust
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<Cow<'env, Dependency>>>,
|
|
optional_dependencies:
|
|
FxHashMap<&'env PackageId, FxHashMap<ExtraName, Vec<Cow<'env, Dependency>>>>,
|
|
dev_dependencies: FxHashMap<&'env PackageId, FxHashMap<GroupName, Vec<Cow<'env, Dependency>>>>,
|
|
/// 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<PackageName>,
|
|
/// Display only the specified packages.
|
|
packages: Vec<PackageName>,
|
|
/// 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<PackageName>,
|
|
packages: Vec<PackageName>,
|
|
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::<Vec<_>>();
|
|
|
|
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<String> {
|
|
// 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<Node<'env>> = 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::<Vec<_>>();
|
|
|
|
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<String> {
|
|
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<ExtraName>> {
|
|
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(())
|
|
}
|
|
}
|