diff --git a/src/args.rs b/src/args.rs index 10b1dce..6bdf275 100644 --- a/src/args.rs +++ b/src/args.rs @@ -54,6 +54,9 @@ pub struct Args { #[clap(long = "all", short = 'a')] /// Don't truncate dependencies that have already been displayed pub all: bool, + #[clap(long = "json")] + /// Print package information as machine-readable output + pub json: bool, #[clap(long = "duplicate", short = 'd')] /// Show only dependencies which come in multiple versions (implies -i) pub duplicates: bool, diff --git a/src/errors.rs b/src/errors.rs index c1c9b46..c8a0e33 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,2 +1,2 @@ -pub use anyhow::{bail, Context, Error, Result}; +pub use anyhow::{anyhow, bail, Context as _, Error, Result}; pub use log::{debug, info}; diff --git a/src/format/human.rs b/src/format/human.rs new file mode 100644 index 0000000..5e6df20 --- /dev/null +++ b/src/format/human.rs @@ -0,0 +1,80 @@ +use crate::errors::*; +use crate::format::{Chunk, Pattern, Pkg}; +use colored::Colorize; +use std::fmt::Write; + +pub fn display(pattern: &Pattern, package: &Pkg) -> Result { + let mut fmt = String::new(); + + for chunk in &pattern.0 { + match *chunk { + Chunk::Raw(ref s) => fmt.write_str(s)?, + Chunk::Package => { + let pkg = format!("{} v{}", package.name, package.version); + if let Some(deb) = &package.debinfo { + if deb.in_unstable { + if deb.compatible { + write!(fmt, "{} ({} in debian)", pkg.green(), deb.version.yellow())?; + } else if deb.outdated { + write!( + fmt, + "{} (outdated, {} in debian)", + pkg.yellow(), + deb.version.red() + )?; + } else { + write!(fmt, "{} (in debian)", pkg.green())?; + } + } else if deb.in_new { + if deb.compatible { + write!( + fmt, + "{} ({} in debian NEW queue)", + pkg.blue(), + deb.version.yellow() + )?; + } else if deb.outdated { + write!( + fmt, + "{}, (outdated, {} in debian NEW queue)", + pkg.blue(), + deb.version.red() + )?; + } else { + write!(fmt, "{} (in debian NEW queue)", pkg.blue())?; + } + } else if deb.outdated { + write!(fmt, "{} (outdated, {})", pkg.red(), deb.version.red())?; + } else { + write!(fmt, "{pkg}")?; + } + } else { + write!(fmt, "{pkg}")?; + } + + match &package.source { + Some(source) if !source.is_crates_io() => write!(fmt, " ({source})")?, + // https://github.com/rust-lang/cargo/issues/7483 + None => write!( + fmt, + " ({})", + package.manifest_path.parent().unwrap().display() + )?, + _ => {} + } + } + Chunk::License => { + if let Some(license) = &package.license { + write!(fmt, "{license}")? + } + } + Chunk::Repository => { + if let Some(repository) = &package.repository { + write!(fmt, "{repository}")? + } + } + } + } + + Ok(fmt) +} diff --git a/src/format/json.rs b/src/format/json.rs new file mode 100644 index 0000000..2739d5f --- /dev/null +++ b/src/format/json.rs @@ -0,0 +1,49 @@ +use crate::errors::*; +use crate::format::Pkg; + +#[derive(Debug, serde::Serialize)] +pub struct Json { + name: String, + cargo_lock_version: String, + repository: Option, + license: Option, + debian: Option, + depth: usize, +} + +#[derive(Debug, serde::Serialize)] +pub struct DebianJson { + version: String, + compatible: bool, + exact_match: bool, + in_new: bool, + in_unstable: bool, + outdated: bool, +} + +impl Json { + pub fn new(pkg: &Pkg, depth: usize) -> Self { + let debian = pkg.debinfo.as_ref().map(|deb| DebianJson { + version: deb.version.clone(), + compatible: deb.compatible, + exact_match: deb.exact_match, + in_new: deb.in_new, + in_unstable: deb.in_unstable, + outdated: deb.outdated, + }); + + Json { + name: pkg.name.clone(), + cargo_lock_version: pkg.version.to_string(), + repository: pkg.repository.clone(), + license: pkg.license.clone(), + debian, + depth, + } + } +} + +pub fn display(package: &Pkg, depth: usize) -> Result { + let json = serde_json::to_string(&Json::new(package, depth))?; + Ok(json) +} diff --git a/src/format/mod.rs b/src/format/mod.rs index a25d48d..022a702 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -1,9 +1,9 @@ use crate::debian::Pkg; +use crate::errors::*; use crate::format::parse::{Parser, RawChunk}; -use anyhow::{anyhow, Error}; -use colored::Colorize; -use std::fmt; +pub mod human; +pub mod json; mod parse; enum Chunk { @@ -35,97 +35,4 @@ impl Pattern { Ok(Pattern(chunks)) } - - pub fn display<'a>(&'a self, package: &'a Pkg) -> Display<'a> { - Display { - pattern: self, - package, - } - } -} - -pub struct Display<'a> { - pattern: &'a Pattern, - package: &'a Pkg, -} - -impl fmt::Display for Display<'_> { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - for chunk in &self.pattern.0 { - match *chunk { - Chunk::Raw(ref s) => fmt.write_str(s)?, - Chunk::Package => { - let pkg = format!("{} v{}", self.package.name, self.package.version); - if let Some(deb) = &self.package.debinfo { - if deb.in_unstable { - if deb.compatible { - write!( - fmt, - "{} ({} in debian)", - pkg.green(), - deb.version.yellow() - )?; - } else if deb.outdated { - write!( - fmt, - "{} (outdated, {} in debian)", - pkg.yellow(), - deb.version.red() - )?; - } else { - write!(fmt, "{} (in debian)", pkg.green())?; - } - } else if deb.in_new { - if deb.compatible { - write!( - fmt, - "{} ({} in debian NEW queue)", - pkg.blue(), - deb.version.yellow() - )?; - } else if deb.outdated { - write!( - fmt, - "{}, (outdated, {} in debian NEW queue)", - pkg.blue(), - deb.version.red() - )?; - } else { - write!(fmt, "{} (in debian NEW queue)", pkg.blue())?; - } - } else if deb.outdated { - write!(fmt, "{} (outdated, {})", pkg.red(), deb.version.red())?; - } else { - write!(fmt, "{pkg}")?; - } - } else { - write!(fmt, "{pkg}")?; - } - - match &self.package.source { - Some(source) if !source.is_crates_io() => write!(fmt, " ({source})")?, - // https://github.com/rust-lang/cargo/issues/7483 - None => write!( - fmt, - " ({})", - self.package.manifest_path.parent().unwrap().display() - )?, - _ => {} - } - } - Chunk::License => { - if let Some(ref license) = self.package.license { - write!(fmt, "{license}")? - } - } - Chunk::Repository => { - if let Some(ref repository) = self.package.repository { - write!(fmt, "{repository}")? - } - } - } - } - - Ok(()) - } } diff --git a/src/graph.rs b/src/graph.rs index a9dc266..5e8de32 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,6 +1,6 @@ use crate::args::Args; use crate::debian::Pkg; -use anyhow::{anyhow, Error}; +use crate::errors::*; use cargo_metadata::{DependencyKind, Metadata, PackageId}; use petgraph::graph::NodeIndex; use petgraph::stable_graph::StableGraph; diff --git a/src/metadata.rs b/src/metadata.rs index a52595e..ba450ec 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -1,5 +1,5 @@ use crate::args::Args; -use anyhow::{anyhow, Context, Error}; +use crate::errors::*; use cargo_metadata::Metadata; use std::env; use std::ffi::OsString; diff --git a/src/tree.rs b/src/tree.rs index 0ce7e3e..6db6edd 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -2,9 +2,9 @@ use crate::args::{Args, Charset}; use crate::debian::Pkg; -use crate::format::Pattern; +use crate::errors::*; +use crate::format::{self, Pattern}; use crate::graph::Graph; -use anyhow::{anyhow, Context, Error}; use cargo_metadata::{DependencyKind, PackageId}; use petgraph::visit::EdgeRef; use petgraph::EdgeDirection; @@ -68,7 +68,9 @@ pub fn print(args: &Args, graph: &Graph) -> Result<(), Error> { } let root = &graph.graph[graph.nodes[*package]]; - print_tree(graph, root, &format, direction, symbols, prefix, args.all); + print_tree( + graph, root, &format, direction, symbols, prefix, args.all, args.json, + )?; } } else { let root = match &args.package { @@ -79,7 +81,9 @@ pub fn print(args: &Args, graph: &Graph) -> Result<(), Error> { }; let root = &graph.graph[graph.nodes[root]]; - print_tree(graph, root, &format, direction, symbols, prefix, args.all); + print_tree( + graph, root, &format, direction, symbols, prefix, args.all, args.json, + )?; } Ok(()) @@ -158,7 +162,8 @@ fn print_tree<'a>( symbols: &Symbols, prefix: Prefix, all: bool, -) { + json: bool, +) -> Result<(), Error> { let mut visited_deps = HashSet::new(); let mut levels_continue = vec![]; @@ -170,9 +175,10 @@ fn print_tree<'a>( symbols, prefix, all, + json, &mut visited_deps, &mut levels_continue, - ); + ) } fn print_package<'a>( @@ -183,9 +189,10 @@ fn print_package<'a>( symbols: &Symbols, prefix: Prefix, all: bool, + json: bool, visited_deps: &mut HashSet<&'a PackageId>, levels_continue: &mut Vec, -) { +) -> Result<(), Error> { let treeline = { let mut line = "".to_string(); line.push_str(&format!(" {} ", &package.packaging_status())); @@ -211,11 +218,15 @@ fn print_package<'a>( line }; - let pkg_status_s = format.display(package).to_string(); - println!("{}{}", treeline, pkg_status_s); + if json { + println!("{}", format::json::display(package, levels_continue.len())?); + } else { + let pkg_status_s = format::human::display(format, package)?; + println!("{}{}", treeline, pkg_status_s); + } if !all && !package.show_dependencies() && !levels_continue.is_empty() { - return; + return Ok(()); } for kind in &[ @@ -231,11 +242,14 @@ fn print_package<'a>( symbols, prefix, all, + json, visited_deps, levels_continue, *kind, - ); + )?; } + + Ok(()) } fn print_dependencies<'a>( @@ -246,10 +260,11 @@ fn print_dependencies<'a>( symbols: &Symbols, prefix: Prefix, all: bool, + json: bool, visited_deps: &mut HashSet<&'a PackageId>, levels_continue: &mut Vec, kind: DependencyKind, -) { +) -> Result<(), Error> { let idx = graph.nodes[&package.id]; let mut deps = vec![]; for edge in graph.graph.edges_directed(idx, direction) { @@ -265,32 +280,34 @@ fn print_dependencies<'a>( } if deps.is_empty() { - return; + return Ok(()); } // ensure a consistent output ordering deps.sort_by_key(|p| &p.id); - let name = match kind { - DependencyKind::Normal => None, - DependencyKind::Build => Some("[build-dependencies]"), - DependencyKind::Development => Some("[dev-dependencies]"), - _ => unreachable!(), - }; + if !json { + let name = match kind { + DependencyKind::Normal => None, + DependencyKind::Build => Some("[build-dependencies]"), + DependencyKind::Development => Some("[dev-dependencies]"), + _ => unreachable!(), + }; - if let Prefix::Indent = prefix { - if let Some(name) = name { - // start with padding used by packaging status icons - print!(" "); + if let Prefix::Indent = prefix { + if let Some(name) = name { + // start with padding used by packaging status icons + print!(" "); - // print tree graph parts - for continues in &**levels_continue { - let c = if *continues { symbols.down } else { " " }; - print!("{c} "); - } + // print tree graph parts + for continues in &**levels_continue { + let c = if *continues { symbols.down } else { " " }; + print!("{c} "); + } - // print the actual texts - println!("{name}"); + // print the actual texts + println!("{name}"); + } } } @@ -305,9 +322,12 @@ fn print_dependencies<'a>( symbols, prefix, all, + json, visited_deps, levels_continue, - ); + )?; levels_continue.pop(); } + + Ok(()) }