From a24cbf805bc29381382c3c6ac2e1fa803e227989 Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Tue, 26 Mar 2024 19:18:02 -0400 Subject: [PATCH 01/14] feat: tree command fixes: #238 Adds a tree command for the CLI that can print out the tree of dependencies based on those defined in pixi.toml. Additionally it can filter the regular dependency tree, or show what tree of packages need a given package. It keeps track of visits for the regular tree, but currently does not for the inverted one. --- src/cli/mod.rs | 3 + src/cli/tree.rs | 335 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 src/cli/tree.rs diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 2e24fb514..06786a277 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -23,6 +23,7 @@ pub mod self_update; pub mod shell; pub mod shell_hook; pub mod task; +pub mod tree; pub mod upload; #[derive(Parser, Debug)] @@ -75,6 +76,7 @@ pub enum Command { Remove(remove::Args), SelfUpdate(self_update::Args), List(list::Args), + Tree(tree::Args), } #[derive(Parser, Debug, Default, Copy, Clone)] @@ -230,6 +232,7 @@ pub async fn execute_command(command: Command) -> miette::Result<()> { Command::Remove(cmd) => remove::execute(cmd).await, Command::SelfUpdate(cmd) => self_update::execute(cmd).await, Command::List(cmd) => list::execute(cmd).await, + Command::Tree(cmd) => tree::execute(cmd).await, } } diff --git a/src/cli/tree.rs b/src/cli/tree.rs new file mode 100644 index 000000000..b2d30be48 --- /dev/null +++ b/src/cli/tree.rs @@ -0,0 +1,335 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use clap::Parser; +use itertools::Itertools; +use rattler_conda_types::Platform; + +use crate::lock_file::UpdateLockFileOptions; +use crate::project::manifest::EnvironmentName; +use crate::Project; + +// Show a tree of project dependencies +#[derive(Debug, Parser)] +#[clap(arg_required_else_help = false)] +pub struct Args { + /// List only packages matching a regular expression + #[arg()] + pub regex: Option, + + /// The platform to list packages for. Defaults to the current platform. + #[arg(long)] + pub platform: Option, + + /// The path to 'pixi.toml' + #[arg(long, env = "PIXI_PROJECT_MANIFEST")] + pub manifest_path: Option, + + /// The environment to list packages for. Defaults to the default environment. + #[arg(short, long)] + pub environment: Option, + + #[clap(flatten)] + pub lock_file_usage: super::LockFileUsageArgs, + + /// Don't install the environment for pypi solving, only update the lock-file if it can solve without installing. + #[arg(long)] + pub no_install: bool, + + /// Invert tree and show what depends on given package + #[arg(short, long)] + pub invert: bool, +} + +struct Symbols { + down: &'static str, + tee: &'static str, + ell: &'static str, + // right: &'static str, + empty: &'static str, +} + +static UTF8_SYMBOLS: Symbols = Symbols { + down: "│ ", + tee: "├──", + ell: "└──", + // right: "───", + empty: " ", +}; + +pub async fn execute(args: Args) -> miette::Result<()> { + if args.invert { + print_inverted_tree(args).await?; + } else { + print_tree(args).await?; + } + Ok(()) +} + +#[derive(Debug)] +struct InvertedPackage { + needed_by: Vec, +} + +// Prints an inverted tree which requires a regex +async fn print_inverted_tree(args: Args) -> Result<(), miette::Error> { + let project = Project::load_or_else_discover(args.manifest_path.as_deref())?; + let environment_name = args + .environment + .map_or_else(|| EnvironmentName::Default, EnvironmentName::Named); + let environment = project + .environment(&environment_name) + .ok_or_else(|| miette::miette!("unknown environment '{environment_name}'"))?; + let lock_file = project + .up_to_date_lock_file(UpdateLockFileOptions { + lock_file_usage: args.lock_file_usage.into(), + no_install: args.no_install, + ..UpdateLockFileOptions::default() + }) + .await?; + let platform = args.platform.unwrap_or_else(Platform::current); + let locked_deps = lock_file + .lock_file + .environment(environment.name().as_str()) + .and_then(|env| env.packages(platform).map(Vec::from_iter)) + .unwrap_or_default(); + let conda_records = locked_deps.iter().filter_map(|d| d.as_conda()); + + let mut needed_map = HashMap::new(); + + for rec in conda_records { + let package_record = rec.package_record(); + + for dep in package_record.depends.iter() { + if let Some((dep_name, _)) = dep.split_once(' ') { + let package = needed_map + .entry(dep_name) + .or_insert(InvertedPackage { needed_by: vec![] }); + package + .needed_by + .push(package_record.name.as_source().to_string()); + } + } + } + + let mut root_package_names: Vec<&&str> = needed_map.keys().collect(); + + let regex = args + .regex + .ok_or("The `-i` flag requires a package name.") + .map_err(|_| miette::miette!("The `-i` flag requires a package name."))?; + let regex = regex::Regex::new(®ex).map_err(|_| miette::miette!("Invalid regex"))?; + root_package_names.retain(|p| regex.is_match(p)); + + if root_package_names.is_empty() { + println!("Nothing depends on the given regular expression"); + return Ok(()); + } + + for pkg_name in root_package_names { + println!("\n{}", pkg_name); + + let package = needed_map.get(pkg_name).unwrap(); + + let needed_count = package.needed_by.len(); + for (index, needed_by) in package.needed_by.iter().enumerate() { + let symbol = if index == needed_count - 1 { + UTF8_SYMBOLS.ell + } else { + UTF8_SYMBOLS.tee + }; + println!("{} {}", symbol, needed_by); + + let prefix = if index == needed_count - 1 { + UTF8_SYMBOLS.empty + } else { + UTF8_SYMBOLS.down + }; + + print_needed_by(needed_by, format!("{} ", prefix), &needed_map); + } + } + + Ok(()) +} + +// Recursively print what a package is needed by as part of an inverted tree +fn print_needed_by( + package_name: &str, + prefix: String, + needed_map: &HashMap<&str, InvertedPackage>, +) { + if let Some(package) = needed_map.get(&package_name) { + let needed_count = package.needed_by.len(); + for (index, needed_by) in package.needed_by.iter().enumerate() { + let symbol = if index == needed_count - 1 { + UTF8_SYMBOLS.ell + } else { + UTF8_SYMBOLS.tee + }; + println!("{}{} {}", prefix, symbol, needed_by); + + let new_prefix = if index == needed_count - 1 { + format!("{}{} ", prefix, UTF8_SYMBOLS.empty) + } else { + format!("{}{} ", prefix, UTF8_SYMBOLS.down) + }; + + print_needed_by(needed_by, new_prefix, needed_map); + } + } +} + +#[derive(Debug)] +struct Dependency { + name: String, +} + +#[derive(Debug)] +struct TreePackage { + dependencies: Vec, + version: String, +} + +// Print a top down dependency tree +async fn print_tree(args: Args) -> Result<(), miette::Error> { + let project = Project::load_or_else_discover(args.manifest_path.as_deref())?; + let environment_name = args + .environment + .map_or_else(|| EnvironmentName::Default, EnvironmentName::Named); + let environment = project + .environment(&environment_name) + .ok_or_else(|| miette::miette!("unknown environment '{environment_name}'"))?; + let lock_file = project + .up_to_date_lock_file(UpdateLockFileOptions { + lock_file_usage: args.lock_file_usage.into(), + no_install: args.no_install, + ..UpdateLockFileOptions::default() + }) + .await?; + let platform = args.platform.unwrap_or_else(Platform::current); + let locked_deps = lock_file + .lock_file + .environment(environment.name().as_str()) + .and_then(|env| env.packages(platform).map(Vec::from_iter)) + .unwrap_or_default(); + let conda_records = locked_deps.iter().filter_map(|d| d.as_conda()); + let mut dependency_map = HashMap::new(); + + for rec in conda_records { + let package_record = rec.package_record(); + + let mut dependencies = Vec::new(); + + for dep in package_record.depends.iter() { + if let Some((dep_name, _)) = dep.split_once(' ') { + dependencies.push(Dependency { + name: dep_name.to_string(), + }); + } + } + + dependency_map.insert( + package_record.name.as_source(), + TreePackage { + dependencies, + version: package_record.version.as_str().to_string(), + }, + ); + } + + let mut project_dependency_names = environment + .dependencies(None, Some(platform)) + .names() + .map(|p| p.as_source().to_string()) + .collect_vec(); + + if let Some(regex) = args.regex { + let regex = regex::Regex::new(®ex).map_err(|_| miette::miette!("Invalid regex"))?; + project_dependency_names.retain(|p| regex.is_match(p)); + + if project_dependency_names.is_empty() { + Err(miette::miette!( + "No top level dependencies matched the given regular expression" + ))?; + } + } + + let mut visited_dependencies = Vec::new(); + let project_dependency_count = project_dependency_names.len(); + for (index, pkg_name) in project_dependency_names.iter().enumerate() { + visited_dependencies.push(pkg_name.to_owned()); + let symbol = if index == project_dependency_count - 1 { + UTF8_SYMBOLS.ell + } else { + UTF8_SYMBOLS.tee + }; + let dep = dependency_map.get(&pkg_name.as_str()).unwrap(); + + println!("{} {} v{}", symbol, pkg_name, dep.version); + + let prefix = if index == project_dependency_count - 1 { + UTF8_SYMBOLS.empty + } else { + UTF8_SYMBOLS.down + }; + print_dependencies( + dep, + format!("{} ", prefix), + &dependency_map, + &mut visited_dependencies, + ); + } + Ok(()) +} + +// Recursively print the dependencies in a regular tree +fn print_dependencies( + package: &TreePackage, + prefix: String, + dependency_map: &HashMap<&str, TreePackage>, + visited_dependencies: &mut Vec, +) { + let dep_count = package.dependencies.len(); + for (index, pkg_name) in package + .dependencies + .iter() + .map(|d| d.name.clone()) + .enumerate() + { + let symbol = if index == dep_count - 1 { + UTF8_SYMBOLS.ell + } else { + UTF8_SYMBOLS.tee + }; + + // Skip virtual packages + if pkg_name.starts_with("__") { + continue; + } + + let dep = dependency_map.get(&pkg_name.as_str()).unwrap(); + let visited = visited_dependencies.contains(&pkg_name); + visited_dependencies.push(pkg_name.as_str().to_owned()); + + println!( + "{}{} {} v{} {}", + prefix, + symbol, + pkg_name, + dep.version, + if visited { "(*)" } else { "" } + ); + + let new_prefix = if index == dep_count - 1 { + format!("{}{} ", prefix, UTF8_SYMBOLS.empty) + } else { + format!("{}{} ", prefix, UTF8_SYMBOLS.down) + }; + + if visited { + continue; + } + print_dependencies(dep, new_prefix, dependency_map, visited_dependencies); + } +} From 00f33d236334cb29c7c4a9d1bca75e8bca654fa9 Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Thu, 28 Mar 2024 08:00:42 -0400 Subject: [PATCH 02/14] Style the project dependencies in green, mark invert to require regex --- src/cli/tree.rs | 56 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/src/cli/tree.rs b/src/cli/tree.rs index b2d30be48..b84c9d3ac 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::path::PathBuf; use clap::Parser; +use console::Color; use itertools::Itertools; use rattler_conda_types::Platform; @@ -36,8 +37,8 @@ pub struct Args { #[arg(long)] pub no_install: bool, - /// Invert tree and show what depends on given package - #[arg(short, long)] + /// Invert tree and show what depends on given package in the regex argument + #[arg(short, long, requires = "regex")] pub invert: bool, } @@ -126,8 +127,19 @@ async fn print_inverted_tree(args: Args) -> Result<(), miette::Error> { return Ok(()); } + // Get the explicit project dependencies + let project_dependency_names = environment + .dependencies(None, Some(platform)) + .names() + .map(|p| p.as_source().to_string()) + .collect_vec(); + for pkg_name in root_package_names { - println!("\n{}", pkg_name); + if project_dependency_names.contains(&pkg_name.to_string()) { + println!("\n{}", console::style(pkg_name).fg(Color::Green).bold()); + } else { + println!("\n{}", pkg_name); + } let package = needed_map.get(pkg_name).unwrap(); @@ -138,7 +150,15 @@ async fn print_inverted_tree(args: Args) -> Result<(), miette::Error> { } else { UTF8_SYMBOLS.tee }; - println!("{} {}", symbol, needed_by); + if project_dependency_names.contains(needed_by) { + println!( + "{} {}", + symbol, + console::style(needed_by).fg(Color::Green).bold() + ); + } else { + println!("{} {}", symbol, needed_by); + } let prefix = if index == needed_count - 1 { UTF8_SYMBOLS.empty @@ -146,7 +166,12 @@ async fn print_inverted_tree(args: Args) -> Result<(), miette::Error> { UTF8_SYMBOLS.down }; - print_needed_by(needed_by, format!("{} ", prefix), &needed_map); + print_needed_by( + needed_by, + format!("{} ", prefix), + &needed_map, + &project_dependency_names, + ); } } @@ -158,6 +183,7 @@ fn print_needed_by( package_name: &str, prefix: String, needed_map: &HashMap<&str, InvertedPackage>, + project_dependency_names: &Vec, ) { if let Some(package) = needed_map.get(&package_name) { let needed_count = package.needed_by.len(); @@ -167,7 +193,16 @@ fn print_needed_by( } else { UTF8_SYMBOLS.tee }; - println!("{}{} {}", prefix, symbol, needed_by); + if project_dependency_names.contains(needed_by) { + println!( + "{}{} {}", + prefix, + symbol, + console::style(needed_by).fg(Color::Green).bold() + ); + } else { + println!("{}{} {}", prefix, symbol, needed_by); + } let new_prefix = if index == needed_count - 1 { format!("{}{} ", prefix, UTF8_SYMBOLS.empty) @@ -175,7 +210,7 @@ fn print_needed_by( format!("{}{} ", prefix, UTF8_SYMBOLS.down) }; - print_needed_by(needed_by, new_prefix, needed_map); + print_needed_by(needed_by, new_prefix, needed_map, project_dependency_names); } } } @@ -266,7 +301,12 @@ async fn print_tree(args: Args) -> Result<(), miette::Error> { }; let dep = dependency_map.get(&pkg_name.as_str()).unwrap(); - println!("{} {} v{}", symbol, pkg_name, dep.version); + println!( + "{} {} v{}", + symbol, + console::style(pkg_name).fg(Color::Green).bold(), + dep.version + ); let prefix = if index == project_dependency_count - 1 { UTF8_SYMBOLS.empty From 856958678e387b6c8c885f97b8814f306c0a72a8 Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Thu, 28 Mar 2024 13:07:09 -0400 Subject: [PATCH 03/14] Rebase tree to stand alone from shell changes --- src/cli/tree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/tree.rs b/src/cli/tree.rs index b84c9d3ac..99dbccd01 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -23,7 +23,7 @@ pub struct Args { pub platform: Option, /// The path to 'pixi.toml' - #[arg(long, env = "PIXI_PROJECT_MANIFEST")] + #[arg(long)] pub manifest_path: Option, /// The environment to list packages for. Defaults to the default environment. From 74cd65800ebdbed41941ec87cf3749798afd9797 Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Fri, 29 Mar 2024 09:58:10 -0400 Subject: [PATCH 04/14] refactor and clean up tree, add pypi deps Does not fully understand normalized names and markers --- src/cli/tree.rs | 491 +++++++++++++++++++++++++----------------------- 1 file changed, 256 insertions(+), 235 deletions(-) diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 99dbccd01..d9a084d01 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -1,16 +1,17 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use clap::Parser; use console::Color; use itertools::Itertools; use rattler_conda_types::Platform; +use rattler_lock::Package; use crate::lock_file::UpdateLockFileOptions; use crate::project::manifest::EnvironmentName; use crate::Project; -// Show a tree of project dependencies +/// Show a tree of project dependencies #[derive(Debug, Parser)] #[clap(arg_required_else_help = false)] pub struct Args { @@ -59,21 +60,6 @@ static UTF8_SYMBOLS: Symbols = Symbols { }; pub async fn execute(args: Args) -> miette::Result<()> { - if args.invert { - print_inverted_tree(args).await?; - } else { - print_tree(args).await?; - } - Ok(()) -} - -#[derive(Debug)] -struct InvertedPackage { - needed_by: Vec, -} - -// Prints an inverted tree which requires a regex -async fn print_inverted_tree(args: Args) -> Result<(), miette::Error> { let project = Project::load_or_else_discover(args.manifest_path.as_deref())?; let environment_name = args .environment @@ -94,282 +80,317 @@ async fn print_inverted_tree(args: Args) -> Result<(), miette::Error> { .environment(environment.name().as_str()) .and_then(|env| env.packages(platform).map(Vec::from_iter)) .unwrap_or_default(); - let conda_records = locked_deps.iter().filter_map(|d| d.as_conda()); - let mut needed_map = HashMap::new(); + let dep_map = generate_dependency_map(&locked_deps); - for rec in conda_records { - let package_record = rec.package_record(); + let direct_deps = direct_dependencies(&environment, &platform); - for dep in package_record.depends.iter() { - if let Some((dep_name, _)) = dep.split_once(' ') { - let package = needed_map - .entry(dep_name) - .or_insert(InvertedPackage { needed_by: vec![] }); - package - .needed_by - .push(package_record.name.as_source().to_string()); - } - } + if args.invert { + print_inverted_dependency_tree(&invert_dep_map(&dep_map), &direct_deps, &args.regex)?; + } else { + print_dependency_tree(&dep_map, &direct_deps, &args.regex)?; } + Ok(()) +} - let mut root_package_names: Vec<&&str> = needed_map.keys().collect(); +/// Filter and print an inverted dependency tree +fn print_inverted_dependency_tree( + inverted_dep_map: &HashMap, + direct_deps: &Vec, + regex: &Option, +) -> Result<(), miette::Error> { + let regex = regex + .as_ref() + .ok_or("") + .map_err(|_| miette::miette!("The -i flag requires a package name."))?; + let regex = regex::Regex::new(regex).map_err(|_| miette::miette!("Invalid regex pattern"))?; + + let mut root_pkg_names = inverted_dep_map.keys().collect_vec(); + root_pkg_names.retain(|p| regex.is_match(p)); + + if root_pkg_names.is_empty() { + Err(miette::miette!( + "Nothing depends on the given regular expression", + ))?; + } - let regex = args - .regex - .ok_or("The `-i` flag requires a package name.") - .map_err(|_| miette::miette!("The `-i` flag requires a package name."))?; - let regex = regex::Regex::new(®ex).map_err(|_| miette::miette!("Invalid regex"))?; - root_package_names.retain(|p| regex.is_match(p)); + for pkg_name in root_pkg_names.iter() { + if let Some(pkg) = inverted_dep_map.get(*pkg_name) { + println!( + "\n{} v{}", + if direct_deps.contains(&pkg.name) { + console::style(pkg.name.clone()).fg(Color::Green).bold() + } else { + console::style(pkg.name.clone()) + }, + pkg.version + ); - if root_package_names.is_empty() { - println!("Nothing depends on the given regular expression"); - return Ok(()); + print_inverted_leaf(pkg, String::from(""), inverted_dep_map, direct_deps); + } } - // Get the explicit project dependencies - let project_dependency_names = environment - .dependencies(None, Some(platform)) - .names() - .map(|p| p.as_source().to_string()) - .collect_vec(); + Ok(()) +} - for pkg_name in root_package_names { - if project_dependency_names.contains(&pkg_name.to_string()) { - println!("\n{}", console::style(pkg_name).fg(Color::Green).bold()); +/// Recursively print inverted dependency tree leaf nodes +fn print_inverted_leaf( + pkg: &InvertedPkg, + prefix: String, + inverted_dep_map: &HashMap, + direct_deps: &Vec, +) { + let needed_count = pkg.needed_by.len(); + for (index, needed_name) in pkg.needed_by.iter().enumerate() { + let last = index == needed_count - 1; + let symbol = if last { + UTF8_SYMBOLS.ell } else { - println!("\n{}", pkg_name); - } + UTF8_SYMBOLS.tee + }; - let package = needed_map.get(pkg_name).unwrap(); + if let Some(needed_pkg) = inverted_dep_map.get(needed_name) { + println!( + "{}{} {} v{}", + prefix, + symbol, + if direct_deps.contains(&needed_pkg.name) { + console::style(needed_pkg.name.clone()) + .fg(Color::Green) + .bold() + } else { + console::style(needed_pkg.name.clone()) + }, + needed_pkg.version, + ); - let needed_count = package.needed_by.len(); - for (index, needed_by) in package.needed_by.iter().enumerate() { - let symbol = if index == needed_count - 1 { - UTF8_SYMBOLS.ell + let new_prefix = if index == needed_count - 1 { + format!("{}{} ", prefix, UTF8_SYMBOLS.empty) } else { - UTF8_SYMBOLS.tee + format!("{}{} ", prefix, UTF8_SYMBOLS.down) }; - if project_dependency_names.contains(needed_by) { - println!( - "{} {}", - symbol, - console::style(needed_by).fg(Color::Green).bold() - ); - } else { - println!("{} {}", symbol, needed_by); - } - let prefix = if index == needed_count - 1 { + print_inverted_leaf(needed_pkg, new_prefix, inverted_dep_map, direct_deps) + } + } +} + +/// Filter and print a top down dependency tree +fn print_dependency_tree( + dep_map: &HashMap, + direct_deps: &[String], + regex: &Option, +) -> Result<(), miette::Error> { + let mut direct_deps = direct_deps.to_owned(); + + if let Some(regex) = regex { + let regex = regex::Regex::new(regex).map_err(|_| miette::miette!("Invalid regex"))?; + direct_deps.retain(|p| regex.is_match(p)); + + if direct_deps.is_empty() { + Err(miette::miette!( + "No top level dependencies matched the given regular expression" + ))?; + } + } + + let mut visited_pkgs = Vec::new(); + let direct_dep_count = direct_deps.len(); + + for (index, pkg_name) in direct_deps.iter().enumerate() { + visited_pkgs.push(pkg_name.to_owned()); + + let last = index == direct_dep_count - 1; + let symbol = if last { + UTF8_SYMBOLS.ell + } else { + UTF8_SYMBOLS.tee + }; + if let Some(pkg) = dep_map.get(pkg_name) { + println!( + "{} {} v{}", + symbol, + console::style(pkg.name.clone()).fg(Color::Green).bold(), + pkg.version + ); + + let prefix = if last { UTF8_SYMBOLS.empty } else { UTF8_SYMBOLS.down }; - - print_needed_by( - needed_by, - format!("{} ", prefix), - &needed_map, - &project_dependency_names, - ); + print_dependency_leaf(pkg, format!("{} ", prefix), dep_map, &mut visited_pkgs) } } - Ok(()) } -// Recursively print what a package is needed by as part of an inverted tree -fn print_needed_by( - package_name: &str, +/// Recursively print top down dependency tree nodes +fn print_dependency_leaf( + pkg: &Pkg, prefix: String, - needed_map: &HashMap<&str, InvertedPackage>, - project_dependency_names: &Vec, + dep_map: &HashMap, + visited_pkgs: &mut Vec, ) { - if let Some(package) = needed_map.get(&package_name) { - let needed_count = package.needed_by.len(); - for (index, needed_by) in package.needed_by.iter().enumerate() { - let symbol = if index == needed_count - 1 { - UTF8_SYMBOLS.ell - } else { - UTF8_SYMBOLS.tee - }; - if project_dependency_names.contains(needed_by) { - println!( - "{}{} {}", - prefix, - symbol, - console::style(needed_by).fg(Color::Green).bold() - ); - } else { - println!("{}{} {}", prefix, symbol, needed_by); + let dep_count = pkg.dependencies.len(); + for (index, dep_name) in pkg.dependencies.iter().enumerate() { + let last = index == dep_count - 1; + let symbol = if last { + UTF8_SYMBOLS.ell + } else { + UTF8_SYMBOLS.tee + }; + + // skip virtual packages + if dep_name.starts_with("__") { + continue; + } + + if let Some(dep) = dep_map.get(dep_name) { + let visited = visited_pkgs.contains(&dep.name); + visited_pkgs.push(dep.name.to_owned()); + + println!( + "{}{} {} v{} {}", + prefix, + symbol, + dep.name, + dep.version, + if visited { "(*)" } else { "" } + ); + + if visited { + continue; } - let new_prefix = if index == needed_count - 1 { + let new_prefix = if last { format!("{}{} ", prefix, UTF8_SYMBOLS.empty) } else { format!("{}{} ", prefix, UTF8_SYMBOLS.down) }; - - print_needed_by(needed_by, new_prefix, needed_map, project_dependency_names); + print_dependency_leaf(dep, new_prefix, dep_map, visited_pkgs); } } } +/// Extract the direct Conda and PyPI dependencies from the environment +fn direct_dependencies( + environment: &crate::project::Environment<'_>, + platform: &Platform, +) -> Vec { + let mut project_dependency_names = environment + .dependencies(None, Some(*platform)) + .names() + .map(|p| p.as_source().to_string()) + .collect_vec(); + project_dependency_names.extend( + environment + .pypi_dependencies(Some(*platform)) + .into_iter() + .map(|(name, _)| name.as_normalized().to_string()), + ); + project_dependency_names +} + #[derive(Debug)] -struct Dependency { +struct Pkg { name: String, + version: String, + dependencies: Vec, } #[derive(Debug)] -struct TreePackage { - dependencies: Vec, +struct InvertedPkg { + name: String, version: String, + needed_by: Vec, } -// Print a top down dependency tree -async fn print_tree(args: Args) -> Result<(), miette::Error> { - let project = Project::load_or_else_discover(args.manifest_path.as_deref())?; - let environment_name = args - .environment - .map_or_else(|| EnvironmentName::Default, EnvironmentName::Named); - let environment = project - .environment(&environment_name) - .ok_or_else(|| miette::miette!("unknown environment '{environment_name}'"))?; - let lock_file = project - .up_to_date_lock_file(UpdateLockFileOptions { - lock_file_usage: args.lock_file_usage.into(), - no_install: args.no_install, - ..UpdateLockFileOptions::default() - }) - .await?; - let platform = args.platform.unwrap_or_else(Platform::current); - let locked_deps = lock_file - .lock_file - .environment(environment.name().as_str()) - .and_then(|env| env.packages(platform).map(Vec::from_iter)) - .unwrap_or_default(); - let conda_records = locked_deps.iter().filter_map(|d| d.as_conda()); - let mut dependency_map = HashMap::new(); - - for rec in conda_records { - let package_record = rec.package_record(); +/// Builds a hashmap of dependencies, with names, versions, and what they depend on +fn generate_dependency_map(locked_deps: &Vec) -> HashMap { + let mut dep_map = HashMap::new(); - let mut dependencies = Vec::new(); + for dep in locked_deps { + let version = dep.version().into_owned(); - for dep in package_record.depends.iter() { - if let Some((dep_name, _)) = dep.split_once(' ') { - dependencies.push(Dependency { - name: dep_name.to_string(), - }); + if let Some(dep) = dep.as_conda() { + let name = dep.package_record().name.as_normalized().to_string(); + let mut dependencies = Vec::new(); + for d in dep.package_record().depends.iter() { + if let Some((dep_name, _)) = d.split_once(' ') { + dependencies.push(dep_name.to_string()) + } } - } - - dependency_map.insert( - package_record.name.as_source(), - TreePackage { - dependencies, - version: package_record.version.as_str().to_string(), - }, - ); - } - let mut project_dependency_names = environment - .dependencies(None, Some(platform)) - .names() - .map(|p| p.as_source().to_string()) - .collect_vec(); - - if let Some(regex) = args.regex { - let regex = regex::Regex::new(®ex).map_err(|_| miette::miette!("Invalid regex"))?; - project_dependency_names.retain(|p| regex.is_match(p)); - - if project_dependency_names.is_empty() { - Err(miette::miette!( - "No top level dependencies matched the given regular expression" - ))?; + dep_map.insert( + name.clone(), + Pkg { + name: name.clone(), + version, + dependencies: unique_deps(dependencies), + }, + ); + } else if let Some(dep) = dep.as_pypi() { + let name = dep.data().package.name.to_string(); + + let mut dependencies = Vec::new(); + for p in dep.data().package.requires_dist.iter() { + if let Some(markers) = &p.marker { + tracing::info!( + "A bunch of markers on {}, skipping for now {:?}", + p.name, + markers + ); + } else { + dependencies.push(p.name.to_string()) + } + } + dep_map.insert( + name.clone(), + Pkg { + name: name.clone(), + version, + dependencies: unique_deps(dependencies), + }, + ); } } + dep_map +} - let mut visited_dependencies = Vec::new(); - let project_dependency_count = project_dependency_names.len(); - for (index, pkg_name) in project_dependency_names.iter().enumerate() { - visited_dependencies.push(pkg_name.to_owned()); - let symbol = if index == project_dependency_count - 1 { - UTF8_SYMBOLS.ell - } else { - UTF8_SYMBOLS.tee - }; - let dep = dependency_map.get(&pkg_name.as_str()).unwrap(); - - println!( - "{} {} v{}", - symbol, - console::style(pkg_name).fg(Color::Green).bold(), - dep.version - ); +/// Only return the unique dependencies +fn unique_deps(dependencies: Vec) -> Vec { + let mut unique_deps = HashSet::new(); - let prefix = if index == project_dependency_count - 1 { - UTF8_SYMBOLS.empty - } else { - UTF8_SYMBOLS.down - }; - print_dependencies( - dep, - format!("{} ", prefix), - &dependency_map, - &mut visited_dependencies, - ); + for d in dependencies { + unique_deps.insert(d); } - Ok(()) + unique_deps.into_iter().collect() } -// Recursively print the dependencies in a regular tree -fn print_dependencies( - package: &TreePackage, - prefix: String, - dependency_map: &HashMap<&str, TreePackage>, - visited_dependencies: &mut Vec, -) { - let dep_count = package.dependencies.len(); - for (index, pkg_name) in package - .dependencies - .iter() - .map(|d| d.name.clone()) - .enumerate() - { - let symbol = if index == dep_count - 1 { - UTF8_SYMBOLS.ell - } else { - UTF8_SYMBOLS.tee - }; - - // Skip virtual packages - if pkg_name.starts_with("__") { - continue; - } - - let dep = dependency_map.get(&pkg_name.as_str()).unwrap(); - let visited = visited_dependencies.contains(&pkg_name); - visited_dependencies.push(pkg_name.as_str().to_owned()); - - println!( - "{}{} {} v{} {}", - prefix, - symbol, - pkg_name, - dep.version, - if visited { "(*)" } else { "" } +/// Given a map of dependencies, invert it so that it has what a package is needed by, +/// rather than what it depends on +fn invert_dep_map(dep_map: &HashMap) -> HashMap { + let mut inverted_deps = HashMap::new(); + + for (pkg_name, pkg) in dep_map { + inverted_deps.insert( + pkg_name.to_string(), + InvertedPkg { + name: pkg.name.clone(), + version: pkg.version.clone(), + needed_by: Vec::new(), + }, ); + } - let new_prefix = if index == dep_count - 1 { - format!("{}{} ", prefix, UTF8_SYMBOLS.empty) - } else { - format!("{}{} ", prefix, UTF8_SYMBOLS.down) - }; - - if visited { - continue; + for pkg in dep_map.values() { + for dep in pkg.dependencies.iter() { + if let Some(idep) = inverted_deps.get_mut(dep) { + idep.needed_by.push(pkg.name.clone()); + } } - print_dependencies(dep, new_prefix, dependency_map, visited_dependencies); } + + inverted_deps } From 114acecef87862063caa23bde36cbf23bbfa85bb Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Sat, 30 Mar 2024 11:52:36 -0400 Subject: [PATCH 05/14] Add docs for tree --- docs/cli.md | 214 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/docs/cli.md b/docs/cli.md index 225e530dd..30f3ae31f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -368,6 +368,220 @@ Output will look like this, where `python` will be green as it is the package th xz 5.2.6 h166bdaf_0 408.6 KiB conda xz-5.2.6-h166bdaf_0.tar.bz2 ``` +## `tree` + +Display the project's packages in a tree. Highlighted packages are those specified in the manifest. + +The package tree can also be inverted (`-i`), to see which packages require a specific dependencies. + +##### Arguments + +- `REGEX` optional regex of which direct dependencies to filter the tree to, or which dependencies to start with when inverting the tree. + +##### Options + +- `--invert (-i)`: Invert the dependency tree, that is given a `REGEX` pattern that matches some packages, show all the packages that depend on those. +- `--platform (-p)`: The platform to list packages for. Defaults to the current platform +- `--manifest-path `: The path to [manifest file](configuration.md), by default it searches for one in the parent directories. +- `--environment`(`-e`): The environment's packages to list, if non is provided the default environment's packages will be listed. +- `--frozen`: Install the environment as defined in the lockfile. Without checking the status of the lockfile. It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). +- `--locked`: Only install if the `pixi.lock` is up-to-date with the [manifest file](configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. +- `--no-install`: Don't install the environment for pypi solving, only update the lock-file if it can solve without installing. (Implied by `--frozen` and `--locked`) + +```shell + +```shell +pixi tree +pixi tree pre-commit +pixi tree -i yaml +pixi tree --environment docs +``` + +!!! warning + `pixi tree`'s support for PyPI packages is still under development, and not all packages are currently being parsed. Add `-v` to increase verbosity to display notices to see which packages are currently unable to be parsed. + +Output will look like this, where direct packages in the [manifest file](configuration.md) will be green. Once a package has been displayed once, the tree won't continue to recurse through it's dependencies (compare the first time `python` appears, vs the rest), and it will instead be marked with a star `(*)`. + +```shell +➜ pixi tree +├── pre-commit v3.3.3 +│ ├── cfgv v3.3.1 +│ │ └── python v3.12.2 +│ │ ├── libsqlite v3.45.2 +│ │ │ └── libzlib v1.2.13 +│ │ ├── libzlib v1.2.13 (*) +│ │ ├── libffi v3.4.2 +│ │ ├── libexpat v2.6.2 +│ │ ├── readline v8.2 +│ │ │ └── ncurses v6.4.20240210 +│ │ ├── xz v5.2.6 +│ │ ├── openssl v3.2.1 +│ │ ├── bzip2 v1.0.8 +│ │ ├── tk v8.6.13 +│ │ │ └── libzlib v1.2.13 (*) +│ │ └── ncurses v6.4.20240210 (*) +│ ├── pyyaml v6.0.1 +│ │ ├── python_abi v3.12 +│ │ ├── python v3.12.2 (*) +│ │ └── yaml v0.2.5 +│ ├── identify v2.5.35 +│ │ └── python v3.12.2 (*) +│ ├── python v3.12.2 (*) +│ ├── virtualenv v20.25.1 +│ │ ├── distlib v0.3.8 +│ │ │ └── python v3.12.2 (*) +│ │ ├── python v3.12.2 (*) +│ │ ├── filelock v3.13.1 +│ │ │ └── python v3.12.2 (*) +│ │ └── platformdirs v4.2.0 +│ │ └── python v3.12.2 (*) +│ └── nodeenv v1.8.0 +│ └── python v3.12.2 (*) +├── rust v1.76.0 +│ └── rust-std-aarch64-apple-darwin v1.76.0 +├── openssl v3.2.1 +├── pkg-config v0.29.2 +│ ├── libglib v2.78.4 +│ │ ├── libiconv v1.17 +│ │ ├── gettext v0.21.1 +│ │ │ └── libiconv v1.17 (*) +│ │ ├── libffi v3.4.2 (*) +│ │ ├── libcxx v16.0.6 +│ │ ├── pcre2 v10.42 +│ │ │ ├── libzlib v1.2.13 (*) +│ │ │ └── bzip2 v1.0.8 (*) +│ │ └── libzlib v1.2.13 (*) +│ └── libiconv v1.17 (*) +├── git v2.42.0 +│ ├── libexpat v2.6.2 (*) +│ ├── libzlib v1.2.13 (*) +│ ├── perl v5.32.1 +│ ├── pcre2 v10.42 (*) +│ ├── openssl v3.2.1 (*) +│ └── libiconv v1.17 (*) +├── cffconvert v2.0.0 +│ ├── ruamel.yaml v0.18.6 +│ │ ├── python_abi v3.12 (*) +│ │ ├── ruamel.yaml.clib v0.2.8 +│ │ │ ├── python_abi v3.12 (*) +│ │ │ └── python v3.12.2 (*) +│ │ └── python v3.12.2 (*) +│ ├── jsonschema v3.2.0 +│ │ ├── python v3.12.2 (*) +│ │ ├── pyrsistent v0.20.0 +│ │ │ ├── python_abi v3.12 (*) +│ │ │ └── python v3.12.2 (*) +│ │ ├── six v1.16.0 +│ │ └── attrs v23.2.0 +│ │ └── python v3.12.2 (*) +│ ├── requests v2.31.0 +│ │ ├── idna v3.6 +│ │ │ └── python v3.12.2 (*) +│ │ ├── python v3.12.2 (*) +│ │ ├── urllib3 v2.2.1 +│ │ │ ├── pysocks v1.7.1 +│ │ │ │ └── python v3.12.2 (*) +│ │ │ ├── python v3.12.2 (*) +│ │ │ └── brotli-python v1.1.0 +│ │ │ ├── python v3.12.2 (*) +│ │ │ ├── python_abi v3.12 (*) +│ │ │ └── libcxx v16.0.6 (*) +│ │ ├── certifi v2024.2.2 +│ │ │ └── python v3.12.2 (*) +│ │ └── charset-normalizer v3.3.2 +│ │ └── python v3.12.2 (*) +│ ├── click v8.1.7 +│ │ └── python v3.12.2 (*) +│ ├── pykwalify v1.8.0 +│ │ ├── python v3.12.2 (*) +│ │ ├── docopt v0.6.2 +│ │ ├── python-dateutil v2.9.0 +│ │ │ ├── python v3.12.2 (*) +│ │ │ └── six v1.16.0 (*) +│ │ └── ruamel.yaml v0.18.6 (*) +│ └── python v3.12.2 (*) +└── tbump v6.9.0 + ├── cli-ui v0.17.2 + │ ├── colorama v0.4.6 + │ │ └── python v3.12.2 (*) + │ ├── python v3.12.2 (*) + │ ├── tabulate v0.9.0 + │ │ └── python v3.12.2 (*) + │ └── unidecode v1.3.8 + │ └── python v3.12.2 (*) + ├── python v3.12.2 (*) + ├── schema v0.7.5 + │ ├── contextlib2 v21.6.0 + │ │ └── python v3.12.2 (*) + │ └── python v3.12.2 (*) + ├── tomlkit v0.12.4 + │ └── python v3.12.2 (*) + └── docopt v0.6.2 (*) +``` + +A regex pattern can be specified to filter the tree to just those that show a specific direct dependency: + +```shell +➜ pixi tree pre-commit +└── pre-commit v3.3.3 + ├── virtualenv v20.25.1 + │ ├── filelock v3.13.1 + │ │ └── python v3.12.2 + │ │ ├── libexpat v2.6.2 + │ │ ├── readline v8.2 + │ │ │ └── ncurses v6.4.20240210 + │ │ ├── libsqlite v3.45.2 + │ │ │ └── libzlib v1.2.13 + │ │ ├── bzip2 v1.0.8 + │ │ ├── libzlib v1.2.13 (*) + │ │ ├── libffi v3.4.2 + │ │ ├── tk v8.6.13 + │ │ │ └── libzlib v1.2.13 (*) + │ │ ├── xz v5.2.6 + │ │ ├── ncurses v6.4.20240210 (*) + │ │ └── openssl v3.2.1 + │ ├── platformdirs v4.2.0 + │ │ └── python v3.12.2 (*) + │ ├── distlib v0.3.8 + │ │ └── python v3.12.2 (*) + │ └── python v3.12.2 (*) + ├── pyyaml v6.0.1 + │ ├── python_abi v3.12 + │ ├── python v3.12.2 (*) + │ └── yaml v0.2.5 + ├── nodeenv v1.8.0 + │ └── python v3.12.2 (*) + ├── python v3.12.2 (*) + ├── cfgv v3.3.1 + │ └── python v3.12.2 (*) + └── identify v2.5.35 + └── python v3.12.2 (*) +``` + +Additionly the tree can be inverted, and it can show which packages depend on a regex pattern. The packages specified in the manifest will also be highlighted (in this case `cffconvert` and `pre-commit` would be). + +```shell +➜ pixi tree -i yaml + +ruamel.yaml v0.18.6 +├── pykwalify v1.8.0 +│ └── cffconvert v2.0.0 +└── cffconvert v2.0.0 + +pyyaml v6.0.1 +└── pre-commit v3.3.3 + +ruamel.yaml.clib v0.2.8 +└── ruamel.yaml v0.18.6 + ├── pykwalify v1.8.0 + │ └── cffconvert v2.0.0 + └── cffconvert v2.0.0 + +yaml v0.2.5 +└── pyyaml v6.0.1 + └── pre-commit v3.3.3 +``` + ## `shell` This command starts a new shell in the project's environment. From c833619316e5cae8c156fd1f56c823e11a48953d Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Sat, 30 Mar 2024 13:25:38 -0400 Subject: [PATCH 06/14] Update docs/cli.md Co-authored-by: Ruben Arts --- docs/cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli.md b/docs/cli.md index 30f3ae31f..244333dad 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -398,7 +398,7 @@ pixi tree --environment docs ``` !!! warning - `pixi tree`'s support for PyPI packages is still under development, and not all packages are currently being parsed. Add `-v` to increase verbosity to display notices to see which packages are currently unable to be parsed. + Use `-v` to show which `pypi` packages are not yet parsed correctly. The `extras` and `markers` parsing is still under development. Output will look like this, where direct packages in the [manifest file](configuration.md) will be green. Once a package has been displayed once, the tree won't continue to recurse through it's dependencies (compare the first time `python` appears, vs the rest), and it will instead be marked with a star `(*)`. From c6920c8a2738d129e679baf80e0d31966ee1603a Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Sat, 30 Mar 2024 14:42:11 -0400 Subject: [PATCH 07/14] Style by package sources, clean up docs and some more native rust idioms --- docs/cli.md | 113 ++++------------------------------------------- src/cli/tree.rs | 114 +++++++++++++++++++++++++++++------------------- 2 files changed, 79 insertions(+), 148 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 244333dad..95eabc569 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -328,8 +328,6 @@ List project's packages. Highlighted packages are explicit dependencies. - `--locked`: Only install if the `pixi.lock` is up-to-date with the [manifest file](configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. - `--no-install`: Don't install the environment for pypi solving, only update the lock-file if it can solve without installing. (Implied by `--frozen` and `--locked`) -```shell - ```shell pixi list pixi list --json-pretty @@ -388,8 +386,6 @@ The package tree can also be inverted (`-i`), to see which packages require a sp - `--locked`: Only install if the `pixi.lock` is up-to-date with the [manifest file](configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. - `--no-install`: Don't install the environment for pypi solving, only update the lock-file if it can solve without installing. (Implied by `--frozen` and `--locked`) -```shell - ```shell pixi tree pixi tree pre-commit @@ -400,7 +396,10 @@ pixi tree --environment docs !!! warning Use `-v` to show which `pypi` packages are not yet parsed correctly. The `extras` and `markers` parsing is still under development. -Output will look like this, where direct packages in the [manifest file](configuration.md) will be green. Once a package has been displayed once, the tree won't continue to recurse through it's dependencies (compare the first time `python` appears, vs the rest), and it will instead be marked with a star `(*)`. +Output will look like this, where direct packages in the [manifest file](configuration.md) will be green. +Once a package has been displayed once, the tree won't continue to recurse through it's dependencies (compare the first time `livzlib` appears, vs the rest), and it will instead be marked with a star `(*)`. + +Version numbers are colored by the package type, yellow for Conda packages and blue for PyPI. ```shell ➜ pixi tree @@ -421,22 +420,7 @@ Output will look like this, where direct packages in the [manifest file](configu │ │ │ └── libzlib v1.2.13 (*) │ │ └── ncurses v6.4.20240210 (*) │ ├── pyyaml v6.0.1 -│ │ ├── python_abi v3.12 -│ │ ├── python v3.12.2 (*) -│ │ └── yaml v0.2.5 -│ ├── identify v2.5.35 -│ │ └── python v3.12.2 (*) -│ ├── python v3.12.2 (*) -│ ├── virtualenv v20.25.1 -│ │ ├── distlib v0.3.8 -│ │ │ └── python v3.12.2 (*) -│ │ ├── python v3.12.2 (*) -│ │ ├── filelock v3.13.1 -│ │ │ └── python v3.12.2 (*) -│ │ └── platformdirs v4.2.0 -│ │ └── python v3.12.2 (*) -│ └── nodeenv v1.8.0 -│ └── python v3.12.2 (*) +... ├── rust v1.76.0 │ └── rust-std-aarch64-apple-darwin v1.76.0 ├── openssl v3.2.1 @@ -445,78 +429,7 @@ Output will look like this, where direct packages in the [manifest file](configu │ │ ├── libiconv v1.17 │ │ ├── gettext v0.21.1 │ │ │ └── libiconv v1.17 (*) -│ │ ├── libffi v3.4.2 (*) -│ │ ├── libcxx v16.0.6 -│ │ ├── pcre2 v10.42 -│ │ │ ├── libzlib v1.2.13 (*) -│ │ │ └── bzip2 v1.0.8 (*) -│ │ └── libzlib v1.2.13 (*) -│ └── libiconv v1.17 (*) -├── git v2.42.0 -│ ├── libexpat v2.6.2 (*) -│ ├── libzlib v1.2.13 (*) -│ ├── perl v5.32.1 -│ ├── pcre2 v10.42 (*) -│ ├── openssl v3.2.1 (*) -│ └── libiconv v1.17 (*) -├── cffconvert v2.0.0 -│ ├── ruamel.yaml v0.18.6 -│ │ ├── python_abi v3.12 (*) -│ │ ├── ruamel.yaml.clib v0.2.8 -│ │ │ ├── python_abi v3.12 (*) -│ │ │ └── python v3.12.2 (*) -│ │ └── python v3.12.2 (*) -│ ├── jsonschema v3.2.0 -│ │ ├── python v3.12.2 (*) -│ │ ├── pyrsistent v0.20.0 -│ │ │ ├── python_abi v3.12 (*) -│ │ │ └── python v3.12.2 (*) -│ │ ├── six v1.16.0 -│ │ └── attrs v23.2.0 -│ │ └── python v3.12.2 (*) -│ ├── requests v2.31.0 -│ │ ├── idna v3.6 -│ │ │ └── python v3.12.2 (*) -│ │ ├── python v3.12.2 (*) -│ │ ├── urllib3 v2.2.1 -│ │ │ ├── pysocks v1.7.1 -│ │ │ │ └── python v3.12.2 (*) -│ │ │ ├── python v3.12.2 (*) -│ │ │ └── brotli-python v1.1.0 -│ │ │ ├── python v3.12.2 (*) -│ │ │ ├── python_abi v3.12 (*) -│ │ │ └── libcxx v16.0.6 (*) -│ │ ├── certifi v2024.2.2 -│ │ │ └── python v3.12.2 (*) -│ │ └── charset-normalizer v3.3.2 -│ │ └── python v3.12.2 (*) -│ ├── click v8.1.7 -│ │ └── python v3.12.2 (*) -│ ├── pykwalify v1.8.0 -│ │ ├── python v3.12.2 (*) -│ │ ├── docopt v0.6.2 -│ │ ├── python-dateutil v2.9.0 -│ │ │ ├── python v3.12.2 (*) -│ │ │ └── six v1.16.0 (*) -│ │ └── ruamel.yaml v0.18.6 (*) -│ └── python v3.12.2 (*) -└── tbump v6.9.0 - ├── cli-ui v0.17.2 - │ ├── colorama v0.4.6 - │ │ └── python v3.12.2 (*) - │ ├── python v3.12.2 (*) - │ ├── tabulate v0.9.0 - │ │ └── python v3.12.2 (*) - │ └── unidecode v1.3.8 - │ └── python v3.12.2 (*) - ├── python v3.12.2 (*) - ├── schema v0.7.5 - │ ├── contextlib2 v21.6.0 - │ │ └── python v3.12.2 (*) - │ └── python v3.12.2 (*) - ├── tomlkit v0.12.4 - │ └── python v3.12.2 (*) - └── docopt v0.6.2 (*) +... ``` A regex pattern can be specified to filter the tree to just those that show a specific direct dependency: @@ -546,19 +459,11 @@ A regex pattern can be specified to filter the tree to just those that show a sp │ │ └── python v3.12.2 (*) │ └── python v3.12.2 (*) ├── pyyaml v6.0.1 - │ ├── python_abi v3.12 - │ ├── python v3.12.2 (*) - │ └── yaml v0.2.5 - ├── nodeenv v1.8.0 - │ └── python v3.12.2 (*) - ├── python v3.12.2 (*) - ├── cfgv v3.3.1 - │ └── python v3.12.2 (*) - └── identify v2.5.35 - └── python v3.12.2 (*) +... ``` -Additionly the tree can be inverted, and it can show which packages depend on a regex pattern. The packages specified in the manifest will also be highlighted (in this case `cffconvert` and `pre-commit` would be). +Additionly the tree can be inverted, and it can show which packages depend on a regex pattern. +The packages specified in the manifest will also be highlighted (in this case `cffconvert` and `pre-commit` would be). ```shell ➜ pixi tree -i yaml diff --git a/src/cli/tree.rs b/src/cli/tree.rs index d9a084d01..9302f423b 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -1,11 +1,10 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::path::PathBuf; use clap::Parser; use console::Color; use itertools::Itertools; use rattler_conda_types::Platform; -use rattler_lock::Package; use crate::lock_file::UpdateLockFileOptions; use crate::project::manifest::EnvironmentName; @@ -13,7 +12,17 @@ use crate::Project; /// Show a tree of project dependencies #[derive(Debug, Parser)] -#[clap(arg_required_else_help = false)] +#[clap(arg_required_else_help = false, long_about=format!( + "\ + Show a tree of project dependencies\n\ + \n\ + Dependency names highlighted in {} are directly specified in the manifest. \ + {} version numbers are Conda packages where as PyPI version numbers are {}. + ", + console::style("green").fg(Color::Green).bold(), + console::style("Yellow").fg(Color::Yellow), + console::style("blue").fg(Color::Blue) +))] pub struct Args { /// List only packages matching a regular expression #[arg()] @@ -95,7 +104,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { /// Filter and print an inverted dependency tree fn print_inverted_dependency_tree( - inverted_dep_map: &HashMap, + inverted_dep_map: &HashMap, direct_deps: &Vec, regex: &Option, ) -> Result<(), miette::Error> { @@ -123,7 +132,10 @@ fn print_inverted_dependency_tree( } else { console::style(pkg.name.clone()) }, - pkg.version + match pkg.source { + PackageSource::Conda => console::style(pkg.version.clone()).fg(Color::Yellow), + PackageSource::Pypi => console::style(pkg.version.clone()).fg(Color::Blue), + }, ); print_inverted_leaf(pkg, String::from(""), inverted_dep_map, direct_deps); @@ -135,9 +147,9 @@ fn print_inverted_dependency_tree( /// Recursively print inverted dependency tree leaf nodes fn print_inverted_leaf( - pkg: &InvertedPkg, + pkg: &InvertedPackage, prefix: String, - inverted_dep_map: &HashMap, + inverted_dep_map: &HashMap, direct_deps: &Vec, ) { let needed_count = pkg.needed_by.len(); @@ -161,7 +173,12 @@ fn print_inverted_leaf( } else { console::style(needed_pkg.name.clone()) }, - needed_pkg.version, + match needed_pkg.source { + PackageSource::Conda => + console::style(needed_pkg.version.clone()).fg(Color::Yellow), + PackageSource::Pypi => + console::style(needed_pkg.version.clone()).fg(Color::Blue), + }, ); let new_prefix = if index == needed_count - 1 { @@ -177,7 +194,7 @@ fn print_inverted_leaf( /// Filter and print a top down dependency tree fn print_dependency_tree( - dep_map: &HashMap, + dep_map: &HashMap, direct_deps: &[String], regex: &Option, ) -> Result<(), miette::Error> { @@ -211,7 +228,10 @@ fn print_dependency_tree( "{} {} v{}", symbol, console::style(pkg.name.clone()).fg(Color::Green).bold(), - pkg.version + match pkg.source { + PackageSource::Conda => console::style(pkg.version.clone()).fg(Color::Yellow), + PackageSource::Pypi => console::style(pkg.version.clone()).fg(Color::Blue), + } ); let prefix = if last { @@ -227,9 +247,9 @@ fn print_dependency_tree( /// Recursively print top down dependency tree nodes fn print_dependency_leaf( - pkg: &Pkg, + pkg: &Package, prefix: String, - dep_map: &HashMap, + dep_map: &HashMap, visited_pkgs: &mut Vec, ) { let dep_count = pkg.dependencies.len(); @@ -255,7 +275,10 @@ fn print_dependency_leaf( prefix, symbol, dep.name, - dep.version, + match dep.source { + PackageSource::Conda => console::style(dep.version.clone()).fg(Color::Yellow), + PackageSource::Pypi => console::style(dep.version.clone()).fg(Color::Blue), + }, if visited { "(*)" } else { "" } ); @@ -292,22 +315,30 @@ fn direct_dependencies( project_dependency_names } +#[derive(Debug, Copy, Clone)] +enum PackageSource { + Conda, + Pypi, +} + #[derive(Debug)] -struct Pkg { +struct Package { name: String, version: String, dependencies: Vec, + source: PackageSource, } #[derive(Debug)] -struct InvertedPkg { +struct InvertedPackage { name: String, version: String, needed_by: Vec, + source: PackageSource, } /// Builds a hashmap of dependencies, with names, versions, and what they depend on -fn generate_dependency_map(locked_deps: &Vec) -> HashMap { +fn generate_dependency_map(locked_deps: &Vec) -> HashMap { let mut dep_map = HashMap::new(); for dep in locked_deps { @@ -324,10 +355,11 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap { dep_map.insert( name.clone(), - Pkg { + Package { name: name.clone(), version, - dependencies: unique_deps(dependencies), + dependencies: dependencies.into_iter().unique().collect(), + source: PackageSource::Conda, }, ); } else if let Some(dep) = dep.as_pypi() { @@ -337,8 +369,9 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap { for p in dep.data().package.requires_dist.iter() { if let Some(markers) = &p.marker { tracing::info!( - "A bunch of markers on {}, skipping for now {:?}", + "Extra and environment markers currently cannot be parsed on {} which is specified by {}, skipping. {:?}", p.name, + name, markers ); } else { @@ -347,10 +380,11 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap { } dep_map.insert( name.clone(), - Pkg { + Package { name: name.clone(), version, - dependencies: unique_deps(dependencies), + dependencies: dependencies.into_iter().unique().collect(), + source: PackageSource::Pypi, }, ); } @@ -358,31 +392,23 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap { dep_map } -/// Only return the unique dependencies -fn unique_deps(dependencies: Vec) -> Vec { - let mut unique_deps = HashSet::new(); - - for d in dependencies { - unique_deps.insert(d); - } - unique_deps.into_iter().collect() -} - /// Given a map of dependencies, invert it so that it has what a package is needed by, /// rather than what it depends on -fn invert_dep_map(dep_map: &HashMap) -> HashMap { - let mut inverted_deps = HashMap::new(); - - for (pkg_name, pkg) in dep_map { - inverted_deps.insert( - pkg_name.to_string(), - InvertedPkg { - name: pkg.name.clone(), - version: pkg.version.clone(), - needed_by: Vec::new(), - }, - ); - } +fn invert_dep_map(dep_map: &HashMap) -> HashMap { + let mut inverted_deps: HashMap = dep_map + .iter() + .map(|(pkg_name, pkg)| { + ( + pkg_name.clone(), + InvertedPackage { + name: pkg.name.clone(), + version: pkg.version.clone(), + needed_by: Vec::new(), + source: pkg.source, + }, + ) + }) + .collect(); for pkg in dep_map.values() { for dep in pkg.dependencies.iter() { From b8f6b1547b2e984f118e93b118deac66ff10693a Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Sat, 30 Mar 2024 15:34:55 -0400 Subject: [PATCH 08/14] Collapse to a single package struct and split filtered and direct deps --- src/cli/tree.rs | 141 +++++++++++++++++++++--------------------------- 1 file changed, 60 insertions(+), 81 deletions(-) diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 9302f423b..9514a947d 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -104,7 +104,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { /// Filter and print an inverted dependency tree fn print_inverted_dependency_tree( - inverted_dep_map: &HashMap, + inverted_dep_map: &HashMap, direct_deps: &Vec, regex: &Option, ) -> Result<(), miette::Error> { @@ -125,17 +125,11 @@ fn print_inverted_dependency_tree( for pkg_name in root_pkg_names.iter() { if let Some(pkg) = inverted_dep_map.get(*pkg_name) { - println!( - "\n{} v{}", - if direct_deps.contains(&pkg.name) { - console::style(pkg.name.clone()).fg(Color::Green).bold() - } else { - console::style(pkg.name.clone()) - }, - match pkg.source { - PackageSource::Conda => console::style(pkg.version.clone()).fg(Color::Yellow), - PackageSource::Pypi => console::style(pkg.version.clone()).fg(Color::Blue), - }, + print_package( + "\n".to_string(), + pkg, + direct_deps.contains(&pkg.name), + false, ); print_inverted_leaf(pkg, String::from(""), inverted_dep_map, direct_deps); @@ -147,9 +141,9 @@ fn print_inverted_dependency_tree( /// Recursively print inverted dependency tree leaf nodes fn print_inverted_leaf( - pkg: &InvertedPackage, + pkg: &Package, prefix: String, - inverted_dep_map: &HashMap, + inverted_dep_map: &HashMap, direct_deps: &Vec, ) { let needed_count = pkg.needed_by.len(); @@ -162,23 +156,11 @@ fn print_inverted_leaf( }; if let Some(needed_pkg) = inverted_dep_map.get(needed_name) { - println!( - "{}{} {} v{}", - prefix, - symbol, - if direct_deps.contains(&needed_pkg.name) { - console::style(needed_pkg.name.clone()) - .fg(Color::Green) - .bold() - } else { - console::style(needed_pkg.name.clone()) - }, - match needed_pkg.source { - PackageSource::Conda => - console::style(needed_pkg.version.clone()).fg(Color::Yellow), - PackageSource::Pypi => - console::style(needed_pkg.version.clone()).fg(Color::Blue), - }, + print_package( + format!("{prefix}{symbol} "), + needed_pkg, + direct_deps.contains(&needed_pkg.name), + false, ); let new_prefix = if index == needed_count - 1 { @@ -195,16 +177,16 @@ fn print_inverted_leaf( /// Filter and print a top down dependency tree fn print_dependency_tree( dep_map: &HashMap, - direct_deps: &[String], + direct_deps: &Vec, regex: &Option, ) -> Result<(), miette::Error> { - let mut direct_deps = direct_deps.to_owned(); + let mut filtered_deps = direct_deps.clone(); if let Some(regex) = regex { let regex = regex::Regex::new(regex).map_err(|_| miette::miette!("Invalid regex"))?; - direct_deps.retain(|p| regex.is_match(p)); + filtered_deps.retain(|p| regex.is_match(p)); - if direct_deps.is_empty() { + if filtered_deps.is_empty() { Err(miette::miette!( "No top level dependencies matched the given regular expression" ))?; @@ -212,9 +194,9 @@ fn print_dependency_tree( } let mut visited_pkgs = Vec::new(); - let direct_dep_count = direct_deps.len(); + let direct_dep_count = filtered_deps.len(); - for (index, pkg_name) in direct_deps.iter().enumerate() { + for (index, pkg_name) in filtered_deps.iter().enumerate() { visited_pkgs.push(pkg_name.to_owned()); let last = index == direct_dep_count - 1; @@ -224,22 +206,20 @@ fn print_dependency_tree( UTF8_SYMBOLS.tee }; if let Some(pkg) = dep_map.get(pkg_name) { - println!( - "{} {} v{}", - symbol, - console::style(pkg.name.clone()).fg(Color::Green).bold(), - match pkg.source { - PackageSource::Conda => console::style(pkg.version.clone()).fg(Color::Yellow), - PackageSource::Pypi => console::style(pkg.version.clone()).fg(Color::Blue), - } - ); + print_package(format!("{symbol} "), pkg, true, false); let prefix = if last { UTF8_SYMBOLS.empty } else { UTF8_SYMBOLS.down }; - print_dependency_leaf(pkg, format!("{} ", prefix), dep_map, &mut visited_pkgs) + print_dependency_leaf( + pkg, + format!("{} ", prefix), + dep_map, + &mut visited_pkgs, + direct_deps, + ) } } Ok(()) @@ -251,6 +231,7 @@ fn print_dependency_leaf( prefix: String, dep_map: &HashMap, visited_pkgs: &mut Vec, + direct_deps: &Vec, ) { let dep_count = pkg.dependencies.len(); for (index, dep_name) in pkg.dependencies.iter().enumerate() { @@ -270,16 +251,11 @@ fn print_dependency_leaf( let visited = visited_pkgs.contains(&dep.name); visited_pkgs.push(dep.name.to_owned()); - println!( - "{}{} {} v{} {}", - prefix, - symbol, - dep.name, - match dep.source { - PackageSource::Conda => console::style(dep.version.clone()).fg(Color::Yellow), - PackageSource::Pypi => console::style(dep.version.clone()).fg(Color::Blue), - }, - if visited { "(*)" } else { "" } + print_package( + format!("{prefix}{symbol} "), + dep, + direct_deps.contains(&dep.name), + visited, ); if visited { @@ -291,11 +267,32 @@ fn print_dependency_leaf( } else { format!("{}{} ", prefix, UTF8_SYMBOLS.down) }; - print_dependency_leaf(dep, new_prefix, dep_map, visited_pkgs); + print_dependency_leaf(dep, new_prefix, dep_map, visited_pkgs, direct_deps); } } } +/// Print package and style by attributes, like if are a direct dependency (name is green and bold), +/// or by the source of the package (yellow version string for Conda, blue for PyPI). +/// Packages that have already been visited and will not be recursed into again are +/// marked with a star (*). +fn print_package(prefix: String, package: &Package, direct: bool, visited: bool) { + println!( + "{}{} v{} {}", + prefix, + if direct { + console::style(&package.name).fg(Color::Green).bold() + } else { + console::style(&package.name) + }, + match package.source { + PackageSource::Conda => console::style(&package.version).fg(Color::Yellow), + PackageSource::Pypi => console::style(&package.version).fg(Color::Blue), + }, + if visited { "(*)" } else { "" } + ); +} + /// Extract the direct Conda and PyPI dependencies from the environment fn direct_dependencies( environment: &crate::project::Environment<'_>, @@ -321,18 +318,11 @@ enum PackageSource { Pypi, } -#[derive(Debug)] +#[derive(Debug, Clone)] struct Package { name: String, version: String, dependencies: Vec, - source: PackageSource, -} - -#[derive(Debug)] -struct InvertedPackage { - name: String, - version: String, needed_by: Vec, source: PackageSource, } @@ -359,6 +349,7 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap< name: name.clone(), version, dependencies: dependencies.into_iter().unique().collect(), + needed_by: Vec::new(), source: PackageSource::Conda, }, ); @@ -384,6 +375,7 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap< name: name.clone(), version, dependencies: dependencies.into_iter().unique().collect(), + needed_by: Vec::new(), source: PackageSource::Pypi, }, ); @@ -394,21 +386,8 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap< /// Given a map of dependencies, invert it so that it has what a package is needed by, /// rather than what it depends on -fn invert_dep_map(dep_map: &HashMap) -> HashMap { - let mut inverted_deps: HashMap = dep_map - .iter() - .map(|(pkg_name, pkg)| { - ( - pkg_name.clone(), - InvertedPackage { - name: pkg.name.clone(), - version: pkg.version.clone(), - needed_by: Vec::new(), - source: pkg.source, - }, - ) - }) - .collect(); +fn invert_dep_map(dep_map: &HashMap) -> HashMap { + let mut inverted_deps = dep_map.clone(); for pkg in dep_map.values() { for dep in pkg.dependencies.iter() { From da6f7bf9debcae708dbfe3048d4cb7307c37d1e2 Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Tue, 2 Apr 2024 10:29:31 -0400 Subject: [PATCH 09/14] Apply suggestions from code review Co-authored-by: Pavel Zwerschke Co-authored-by: Bas Zalmstra --- docs/cli.md | 4 ++-- src/cli/tree.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 95eabc569..385bfded8 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -381,7 +381,7 @@ The package tree can also be inverted (`-i`), to see which packages require a sp - `--invert (-i)`: Invert the dependency tree, that is given a `REGEX` pattern that matches some packages, show all the packages that depend on those. - `--platform (-p)`: The platform to list packages for. Defaults to the current platform - `--manifest-path `: The path to [manifest file](configuration.md), by default it searches for one in the parent directories. -- `--environment`(`-e`): The environment's packages to list, if non is provided the default environment's packages will be listed. +- `--environment (-e)`: The environment's packages to list, if non is provided the default environment's packages will be listed. - `--frozen`: Install the environment as defined in the lockfile. Without checking the status of the lockfile. It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). - `--locked`: Only install if the `pixi.lock` is up-to-date with the [manifest file](configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. - `--no-install`: Don't install the environment for pypi solving, only update the lock-file if it can solve without installing. (Implied by `--frozen` and `--locked`) @@ -397,7 +397,7 @@ pixi tree --environment docs Use `-v` to show which `pypi` packages are not yet parsed correctly. The `extras` and `markers` parsing is still under development. Output will look like this, where direct packages in the [manifest file](configuration.md) will be green. -Once a package has been displayed once, the tree won't continue to recurse through it's dependencies (compare the first time `livzlib` appears, vs the rest), and it will instead be marked with a star `(*)`. +Once a package has been displayed once, the tree won't continue to recurse through its dependencies (compare the first time `libzlib` appears, vs the rest), and it will instead be marked with a star `(*)`. Version numbers are colored by the package type, yellow for Conda packages and blue for PyPI. diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 9514a947d..1178ad0d9 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -17,7 +17,7 @@ use crate::Project; Show a tree of project dependencies\n\ \n\ Dependency names highlighted in {} are directly specified in the manifest. \ - {} version numbers are Conda packages where as PyPI version numbers are {}. + {} version numbers are conda packages, PyPI version numbers are {}. ", console::style("green").fg(Color::Green).bold(), console::style("Yellow").fg(Color::Yellow), @@ -307,7 +307,7 @@ fn direct_dependencies( environment .pypi_dependencies(Some(*platform)) .into_iter() - .map(|(name, _)| name.as_normalized().to_string()), + .map(|(name, _)| name.as_normalized().as_dist_info_name().into_owned()), ); project_dependency_names } From 09e9cba25960da4d530fde6499a58a13f46e4262 Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Tue, 2 Apr 2024 10:34:59 -0400 Subject: [PATCH 10/14] Adjust CLI docs to better show collapsed repeat packages --- docs/cli.md | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 385bfded8..b292b6826 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -323,7 +323,7 @@ List project's packages. Highlighted packages are explicit dependencies. - `--json-pretty`: Whether to output in pretty json format - `--sort-by `: Sorting strategy [default: name] [possible values: size, name, type] - `--manifest-path `: The path to [manifest file](configuration.md), by default it searches for one in the parent directories. -- `--environment`(`-e`): The environment's packages to list, if non is provided the default environment's packages will be listed. +- `--environment (-e)`: The environment's packages to list, if non is provided the default environment's packages will be listed. - `--frozen`: Install the environment as defined in the lockfile. Without checking the status of the lockfile. It can also be controlled by the `PIXI_FROZEN` environment variable (example: `PIXI_FROZEN=true`). - `--locked`: Only install if the `pixi.lock` is up-to-date with the [manifest file](configuration.md)[^1]. It can also be controlled by the `PIXI_LOCKED` environment variable (example: `PIXI_LOCKED=true`). Conflicts with `--frozen`. - `--no-install`: Don't install the environment for pypi solving, only update the lock-file if it can solve without installing. (Implied by `--frozen` and `--locked`) @@ -397,7 +397,7 @@ pixi tree --environment docs Use `-v` to show which `pypi` packages are not yet parsed correctly. The `extras` and `markers` parsing is still under development. Output will look like this, where direct packages in the [manifest file](configuration.md) will be green. -Once a package has been displayed once, the tree won't continue to recurse through its dependencies (compare the first time `libzlib` appears, vs the rest), and it will instead be marked with a star `(*)`. +Once a package has been displayed once, the tree won't continue to recurse through its dependencies (compare the first time `python` appears, vs the rest), and it will instead be marked with a star `(*)`. Version numbers are colored by the package type, yellow for Conda packages and blue for PyPI. @@ -406,30 +406,26 @@ Version numbers are colored by the package type, yellow for Conda packages and b ├── pre-commit v3.3.3 │ ├── cfgv v3.3.1 │ │ └── python v3.12.2 +│ │ ├── bzip2 v1.0.8 +│ │ ├── libexpat v2.6.2 +│ │ ├── libffi v3.4.2 │ │ ├── libsqlite v3.45.2 │ │ │ └── libzlib v1.2.13 │ │ ├── libzlib v1.2.13 (*) -│ │ ├── libffi v3.4.2 -│ │ ├── libexpat v2.6.2 -│ │ ├── readline v8.2 -│ │ │ └── ncurses v6.4.20240210 -│ │ ├── xz v5.2.6 +│ │ ├── ncurses v6.4.20240210 │ │ ├── openssl v3.2.1 -│ │ ├── bzip2 v1.0.8 +│ │ ├── readline v8.2 +│ │ │ └── ncurses v6.4.20240210 (*) │ │ ├── tk v8.6.13 │ │ │ └── libzlib v1.2.13 (*) -│ │ └── ncurses v6.4.20240210 (*) -│ ├── pyyaml v6.0.1 +│ │ └── xz v5.2.6 +│ ├── identify v2.5.35 +│ │ └── python v3.12.2 (*) ... -├── rust v1.76.0 -│ └── rust-std-aarch64-apple-darwin v1.76.0 -├── openssl v3.2.1 -├── pkg-config v0.29.2 -│ ├── libglib v2.78.4 -│ │ ├── libiconv v1.17 -│ │ ├── gettext v0.21.1 -│ │ │ └── libiconv v1.17 (*) +└── tbump v6.9.0 ... + └── tomlkit v0.12.4 + └── python v3.12.2 (*) ``` A regex pattern can be specified to filter the tree to just those that show a specific direct dependency: From dabb4a178eda755d41727b11e1fec0c50acd29ed Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Tue, 2 Apr 2024 15:08:10 -0400 Subject: [PATCH 11/14] Show __ for deps that have otherwise not otherwise in the dep map --- src/cli/tree.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 1178ad0d9..4e0eac49b 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -242,11 +242,6 @@ fn print_dependency_leaf( UTF8_SYMBOLS.tee }; - // skip virtual packages - if dep_name.starts_with("__") { - continue; - } - if let Some(dep) = dep_map.get(dep_name) { let visited = visited_pkgs.contains(&dep.name); visited_pkgs.push(dep.name.to_owned()); @@ -268,6 +263,22 @@ fn print_dependency_leaf( format!("{}{} ", prefix, UTF8_SYMBOLS.down) }; print_dependency_leaf(dep, new_prefix, dep_map, visited_pkgs, direct_deps); + } else { + let visited = visited_pkgs.contains(dep_name); + visited_pkgs.push(dep_name.to_owned()); + + print_package( + format!("{prefix}{symbol}"), + &Package { + name: dep_name.to_owned(), + version: String::from(""), + dependencies: Vec::new(), + needed_by: Vec::new(), + source: PackageSource::Conda, + }, + false, + visited, + ) } } } @@ -278,7 +289,7 @@ fn print_dependency_leaf( /// marked with a star (*). fn print_package(prefix: String, package: &Package, direct: bool, visited: bool) { println!( - "{}{} v{} {}", + "{}{} {} {}", prefix, if direct { console::style(&package.name).fg(Color::Green).bold() @@ -354,7 +365,7 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap< }, ); } else if let Some(dep) = dep.as_pypi() { - let name = dep.data().package.name.to_string(); + let name = dep.data().package.name.as_dist_info_name().into_owned(); let mut dependencies = Vec::new(); for p in dep.data().package.requires_dist.iter() { From 3f237e93dde2c5bce85909f328560549a65f08a5 Mon Sep 17 00:00:00 2001 From: Alex Kerney Date: Tue, 2 Apr 2024 15:31:36 -0400 Subject: [PATCH 12/14] Mismatch between normalized names --- src/cli/tree.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 4e0eac49b..700d21c53 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -268,7 +268,7 @@ fn print_dependency_leaf( visited_pkgs.push(dep_name.to_owned()); print_package( - format!("{prefix}{symbol}"), + format!("{prefix}{symbol} "), &Package { name: dep_name.to_owned(), version: String::from(""), @@ -377,7 +377,7 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap< markers ); } else { - dependencies.push(p.name.to_string()) + dependencies.push(p.name.as_dist_info_name().into_owned()) } } dep_map.insert( From 118b5fabdabf39197d57e6dcc9c26337bda72216 Mon Sep 17 00:00:00 2001 From: Ruben Arts Date: Wed, 3 Apr 2024 13:21:12 +0200 Subject: [PATCH 13/14] fix: missing dependencies in tree, and renaming --- src/cli/tree.rs | 53 ++++++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 700d21c53..a2c151f1b 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -340,21 +340,29 @@ struct Package { /// Builds a hashmap of dependencies, with names, versions, and what they depend on fn generate_dependency_map(locked_deps: &Vec) -> HashMap { - let mut dep_map = HashMap::new(); - - for dep in locked_deps { - let version = dep.version().into_owned(); - - if let Some(dep) = dep.as_conda() { - let name = dep.package_record().name.as_normalized().to_string(); - let mut dependencies = Vec::new(); - for d in dep.package_record().depends.iter() { - if let Some((dep_name, _)) = d.split_once(' ') { - dependencies.push(dep_name.to_string()) - } - } - - dep_map.insert( + let mut package_dependencies_map = HashMap::new(); + + for package in locked_deps { + let version = package.version().into_owned(); + + if let Some(conda_package) = package.as_conda() { + let name = conda_package + .package_record() + .name + .as_normalized() + .to_string(); + // Parse the dependencies of the package + let dependencies: Vec = conda_package + .package_record() + .depends + .iter() + .map(|d| { + d.split_once(' ') + .map_or_else(|| d.to_string(), |(dep_name, _)| dep_name.to_string()) + }) + .collect(); + + package_dependencies_map.insert( name.clone(), Package { name: name.clone(), @@ -364,11 +372,16 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap< source: PackageSource::Conda, }, ); - } else if let Some(dep) = dep.as_pypi() { - let name = dep.data().package.name.as_dist_info_name().into_owned(); + } else if let Some(pypi_package) = package.as_pypi() { + let name = pypi_package + .data() + .package + .name + .as_dist_info_name() + .into_owned(); let mut dependencies = Vec::new(); - for p in dep.data().package.requires_dist.iter() { + for p in pypi_package.data().package.requires_dist.iter() { if let Some(markers) = &p.marker { tracing::info!( "Extra and environment markers currently cannot be parsed on {} which is specified by {}, skipping. {:?}", @@ -380,7 +393,7 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap< dependencies.push(p.name.as_dist_info_name().into_owned()) } } - dep_map.insert( + package_dependencies_map.insert( name.clone(), Package { name: name.clone(), @@ -392,7 +405,7 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap< ); } } - dep_map + package_dependencies_map } /// Given a map of dependencies, invert it so that it has what a package is needed by, From 19f5ba6f9cacae94046334c58294c687faf7a19f Mon Sep 17 00:00:00 2001 From: Ruben Arts Date: Wed, 3 Apr 2024 14:29:58 +0200 Subject: [PATCH 14/14] fix: platform -p cli and docs --- docs/cli.md | 3 ++- src/cli/tree.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index b292b6826..9d2e093da 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -391,6 +391,7 @@ pixi tree pixi tree pre-commit pixi tree -i yaml pixi tree --environment docs +pixi tree --platform win-64 ``` !!! warning @@ -458,7 +459,7 @@ A regex pattern can be specified to filter the tree to just those that show a sp ... ``` -Additionly the tree can be inverted, and it can show which packages depend on a regex pattern. +Additionally, the tree can be inverted, and it can show which packages depend on a regex pattern. The packages specified in the manifest will also be highlighted (in this case `cffconvert` and `pre-commit` would be). ```shell diff --git a/src/cli/tree.rs b/src/cli/tree.rs index a2c151f1b..7cbeb74e3 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -29,7 +29,7 @@ pub struct Args { pub regex: Option, /// The platform to list packages for. Defaults to the current platform. - #[arg(long)] + #[arg(long, short)] pub platform: Option, /// The path to 'pixi.toml'