#![allow(clippy::exit)]
use anyhow::{bail, Context, Error};
use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
mod check;
mod common;
mod fetch;
mod init;
mod list;
mod stats;
#[derive(Subcommand, Debug)]
enum Command {
#[clap(name = "check")]
Check(check::Args),
#[clap(name = "fetch")]
Fetch(fetch::Args),
#[clap(name = "init")]
Init(init::Args),
#[clap(name = "list")]
List(list::Args),
}
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
pub enum Format {
Human,
Json,
}
#[derive(ValueEnum, Copy, Clone, Debug)]
pub enum Color {
Auto,
Always,
Never,
}
fn parse_level(s: &str) -> Result<log::LevelFilter, Error> {
s.parse::<log::LevelFilter>()
.with_context(|| format!("failed to parse level '{}'", s))
}
#[derive(Parser)]
#[clap(rename_all = "kebab-case")]
pub(crate) struct GraphContext {
#[clap(long, action)]
pub(crate) manifest_path: Option<PathBuf>,
#[clap(long, action)]
pub(crate) workspace: bool,
#[clap(long, action)]
pub(crate) exclude: Vec<String>,
#[clap(short, long, action)]
pub(crate) target: Vec<String>,
#[clap(long, action)]
pub(crate) all_features: bool,
#[clap(long, action)]
pub(crate) no_default_features: bool,
#[clap(long, use_value_delimiter = true, action)]
pub(crate) features: Vec<String>,
#[clap(long, action)]
pub(crate) frozen: bool,
#[clap(long, action)]
pub(crate) locked: bool,
#[clap(long, action)]
pub(crate) offline: bool,
}
#[derive(Parser)]
#[clap(author, version, about, long_about = None, rename_all = "kebab-case", max_term_width = 80)]
struct Opts {
#[clap(
short = 'L',
long = "log-level",
default_value = "warn",
value_parser = parse_level,
long_help = "The log level for messages
Only log messages at or above the level will be emitted.
Possible values:
* off
* error
* warn
* info
* debug
* trace
")]
log_level: log::LevelFilter,
#[clap(short, long, default_value = "human", value_enum, action)]
format: Format,
#[clap(
short,
long,
default_value = "auto",
value_enum,
env = "CARGO_TERM_COLOR",
action
)]
color: Color,
#[clap(flatten)]
ctx: GraphContext,
#[clap(subcommand)]
cmd: Command,
}
fn setup_logger(
level: log::LevelFilter,
format: Format,
color: bool,
) -> Result<(), fern::InitError> {
use log::Level::{Debug, Error, Info, Trace, Warn};
use nu_ansi_term::Color::{Blue, Green, Purple, Red, Yellow};
let now = time::OffsetDateTime::now_utc();
match format {
Format::Human => {
const HUMAN: &[time::format_description::FormatItem<'static>] =
time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
if color {
fern::Dispatch::new()
.level(level)
.format(move |out, message, record| {
out.finish(format_args!(
"{date} [{level}] {message}\x1B[0m",
date = now.format(&HUMAN).unwrap(),
level = match record.level() {
Error => Red.paint("ERROR"),
Warn => Yellow.paint("WARN"),
Info => Green.paint("INFO"),
Debug => Blue.paint("DEBUG"),
Trace => Purple.paint("TRACE"),
},
message = message,
));
})
.chain(std::io::stderr())
.apply()?;
} else {
fern::Dispatch::new()
.level(level)
.format(move |out, message, record| {
out.finish(format_args!(
"{date} [{level}] {message}",
date = now.format(&HUMAN).unwrap(),
level = match record.level() {
Error => "ERROR",
Warn => "WARN",
Info => "INFO",
Debug => "DEBUG",
Trace => "TRACE",
},
message = message,
));
})
.chain(std::io::stderr())
.apply()?;
}
}
Format::Json => {
fern::Dispatch::new()
.level(level)
.format(move |out, message, record| {
out.finish(format_args!(
"{}",
serde_json::json! {{
"type": "log",
"fields": {
"timestamp": now.format(&time::format_description::well_known::Rfc3339).unwrap(),
"level": match record.level() {
Error => "ERROR",
Warn => "WARN",
Info => "INFO",
Debug => "DEBUG",
Trace => "TRACE",
},
"message": message,
}
}}
));
})
.chain(std::io::stderr())
.apply()?;
}
}
Ok(())
}
fn real_main() -> Result<(), Error> {
let args =
Opts::parse_from({
std::env::args().enumerate().filter_map(|(i, a)| {
if i == 1 && a == "deny" {
None
} else {
Some(a)
}
})
});
let log_level = args.log_level;
let color = match args.color {
Color::Auto => atty::is(atty::Stream::Stderr),
Color::Always => true,
Color::Never => false,
};
setup_logger(log_level, args.format, color)?;
let manifest_path = if let Some(mpath) = args.ctx.manifest_path {
mpath
} else {
let cwd =
std::env::current_dir().context("unable to determine current working directory")?;
if !cwd.exists() {
bail!("current working directory {} was not found", cwd.display());
}
if !cwd.is_dir() {
bail!(
"current working directory {} is not a directory",
cwd.display()
);
}
let man_path = cwd.join("Cargo.toml");
if !man_path.exists() {
bail!(
"the directory {} doesn't contain a Cargo.toml file",
cwd.display()
);
}
man_path
};
if manifest_path.file_name() != Some(std::ffi::OsStr::new("Cargo.toml"))
|| !manifest_path.is_file()
{
bail!("--manifest-path must point to a Cargo.toml file");
}
if !manifest_path.exists() {
bail!("unable to find cargo manifest {}", manifest_path.display());
}
let krate_ctx = common::KrateContext {
manifest_path,
workspace: args.ctx.workspace,
exclude: args.ctx.exclude,
targets: args.ctx.target,
no_default_features: args.ctx.no_default_features,
all_features: args.ctx.all_features,
features: args.ctx.features,
frozen: args.ctx.frozen,
locked: args.ctx.locked,
offline: args.ctx.offline,
};
let log_ctx = crate::common::LogContext {
color: args.color,
format: args.format,
log_level: args.log_level,
};
match args.cmd {
Command::Check(mut cargs) => {
let show_stats = cargs.show_stats;
if args.ctx.offline {
log::info!("network access disabled via --offline flag, disabling advisory database fetching");
cargs.disable_fetch = true;
}
let stats = check::cmd(log_ctx, cargs, krate_ctx)?;
let errors = stats.total_errors();
stats::print_stats(stats, show_stats, log_level, args.format, args.color);
if errors > 0 {
std::process::exit(1);
} else {
Ok(())
}
}
Command::Fetch(fargs) => fetch::cmd(log_ctx, fargs, krate_ctx),
Command::Init(iargs) => init::cmd(iargs, krate_ctx),
Command::List(largs) => list::cmd(log_ctx, largs, krate_ctx),
}
}
fn main() {
match real_main() {
Ok(_) => {}
Err(e) => {
log::error!("{:#}", e);
std::process::exit(1);
}
}
}
#[cfg(test)]
mod test {
use clap::ColorChoice;
use clap::Command;
fn snapshot_test_cli_command(app: Command, cmd_name: String) {
let mut app = app
.color(ColorChoice::Never)
.version("0.0.0")
.long_version("0.0.0");
let arg_names = app
.get_arguments()
.filter_map(|a| {
let id = a.get_id();
if id != "version" && id != "help" {
Some(id.clone())
} else {
None
}
})
.collect::<Vec<_>>();
for arg_name in arg_names {
app = app.mut_arg(arg_name, |arg| arg.hide_env_values(true));
}
let mut buffer = Vec::new();
app.write_long_help(&mut buffer).unwrap();
let help_text = std::str::from_utf8(&buffer).unwrap();
insta::_macro_support::assert_snapshot(
cmd_name.clone().into(),
help_text,
env!("CARGO_MANIFEST_DIR"),
"cli-cmd",
module_path!(),
file!(),
line!(),
"help_text",
)
.unwrap();
for app in app.get_subcommands() {
if app.get_name() == "help" {
continue;
}
snapshot_test_cli_command(app.clone(), format!("{cmd_name}-{}", app.get_name()));
}
}
#[test]
fn cli_snapshot() {
use clap::CommandFactory;
insta::with_settings!({
snapshot_path => "../../tests/snapshots",
}, {
snapshot_test_cli_command(
super::Opts::command().name("cargo_deny"),
"cargo_deny".to_owned(),
);
});
}
}