use super::ProgressReporter;
use super::config::Config;
use crate::Result;
use crate::expr::evaluate;
use crate::facts::{Collector, CrateFacts, CrateRef, ProviderResult};
use crate::metrics::flatten;
use crate::reports::ReportableCrate;
use crate::reports::{generate_console, generate_csv, generate_html, generate_json, generate_xlsx};
use camino::Utf8PathBuf;
use cargo_metadata::MetadataCommand;
use chrono::{Local, Utc};
use clap::Args;
use clap::ValueEnum;
use core::time::Duration;
use directories::BaseDirs;
use ohno::IntoAppError;
use std::fs;
use std::io::Write;
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ColorMode {
Always,
Never,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum LogLevel {
None,
Error,
Warn,
Info,
Debug,
Trace,
}
#[derive(Args, Debug)]
pub struct CommonArgs {
#[arg(long, value_name = "TOKEN", env = "GITHUB_TOKEN")]
pub github_token: Option<String>,
#[arg(long, value_name = "TOKEN", env = "CODEBERG_TOKEN")]
pub codeberg_token: Option<String>,
#[arg(long, default_value = "Cargo.toml", value_name = "PATH")]
pub manifest_path: Utf8PathBuf,
#[arg(long, short = 'c', value_name = "PATH")]
pub config: Option<Utf8PathBuf>,
#[arg(long, value_name = "WHEN", default_value = "auto")]
pub color: ColorMode,
#[arg(long, value_name = "PATH")]
pub cache_dir: Option<Utf8PathBuf>,
#[arg(long, value_name = "LEVEL", default_value = "none", global = true)]
pub log_level: LogLevel,
#[arg(long, value_name = "PATH", help_heading = "Report Output")]
pub excel: Option<Utf8PathBuf>,
#[arg(long, value_name = "PATH", help_heading = "Report Output")]
pub html: Option<Utf8PathBuf>,
#[arg(long, value_name = "PATH", help_heading = "Report Output")]
pub csv: Option<Utf8PathBuf>,
#[arg(long, value_name = "PATH", help_heading = "Report Output")]
pub json: Option<Utf8PathBuf>,
#[arg(long, help_heading = "Report Output")]
pub console: bool,
#[arg(long)]
pub check: bool,
}
pub struct Common<'a, H: super::Host> {
pub collector: Collector,
pub config: Config,
pub metadata_cmd: MetadataCommand,
host: &'a mut H,
color: ColorMode,
check: bool,
console: bool,
html: Option<Utf8PathBuf>,
excel: Option<Utf8PathBuf>,
csv: Option<Utf8PathBuf>,
json: Option<Utf8PathBuf>,
}
impl<'a, H: super::Host> Common<'a, H> {
pub async fn new(host: &'a mut H, args: &CommonArgs) -> Result<Self> {
Self::init_logging(args.log_level);
let mut metadata_cmd = MetadataCommand::new();
let _ = metadata_cmd.manifest_path(&args.manifest_path);
let metadata = metadata_cmd.exec().into_app_err("unable to retrieve workspace metadata")?;
let config_base_path = metadata.workspace_root;
let config = Config::load(&config_base_path, args.config.as_ref())?;
let cache_dir = if let Some(cache_path) = &args.cache_dir {
cache_path.as_std_path().to_path_buf()
} else {
BaseDirs::new()
.into_app_err("Could not determine cache directory")?
.cache_dir()
.join("cargo-aprz")
};
let delay = if args.log_level == LogLevel::None {
Duration::from_millis(300)
} else {
Duration::from_hours(365 * 24)
};
let progress_reporter = ProgressReporter::new(delay);
let collector = Collector::new(
args.github_token.as_deref(),
args.codeberg_token.as_deref(),
&cache_dir,
config.crates_cache_ttl,
config.hosting_cache_ttl,
config.codebase_cache_ttl,
config.coverage_cache_ttl,
config.advisories_cache_ttl,
Utc::now(),
progress_reporter,
)
.await?;
let mut metadata_cmd = MetadataCommand::new();
let _ = metadata_cmd.manifest_path(&args.manifest_path);
Ok(Self {
collector,
config,
metadata_cmd,
host,
color: args.color,
check: args.check,
console: args.console,
html: args.html.clone(),
excel: args.excel.clone(),
csv: args.csv.clone(),
json: args.json.clone(),
})
}
fn init_logging(log_level: LogLevel) {
if log_level == LogLevel::None {
return;
}
let level = match log_level {
LogLevel::None => return, LogLevel::Error => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
};
let env = env_logger::Env::default().filter_or("RUST_LOG", level);
env_logger::Builder::from_env(env)
.format_timestamp(None)
.format_module_path(false)
.format_target(matches!(log_level, LogLevel::Debug) || matches!(log_level, LogLevel::Trace))
.init();
}
pub async fn process_crates(&self, crates: impl IntoIterator<Item = CrateRef>, suggestions: bool) -> Result<Vec<CrateFacts>> {
let results = self.collector.collect(Utc::now(), crates, suggestions).await;
match results {
Ok(facts_iter) => Ok(facts_iter.collect()),
Err(e) => {
eprintln!("{e:#}");
Err(e)
}
}
}
#[expect(clippy::too_many_lines, reason = "Function handles multiple report formats and evaluation logic")]
pub fn report(&mut self, processed_crates: impl IntoIterator<Item = CrateFacts>) -> Result<()> {
let (analyzable_crates, failed_crates): (Vec<_>, Vec<_>) =
processed_crates.into_iter().partition(|facts| facts.crates_data.is_found());
if !failed_crates.is_empty() {
let _ = writeln!(self.host.error(), "\nUnable to analyze {} crate(s)", failed_crates.len());
for facts in &failed_crates {
match &facts.crates_data {
ProviderResult::CrateNotFound(suggestions) => {
if suggestions.is_empty() {
let _ = writeln!(
self.host.error(),
" Could not find information on crate '{}'",
facts.crate_spec.name()
);
} else {
let suggestion_text = match suggestions.as_ref() {
[single] => format!("Did you mean '{single}'?"),
[first, second] => format!("Did you mean '{first}' or '{second}'?"),
[all_but_last @ .., last] => {
let quoted_suggestions = all_but_last.iter().map(|s| format!("'{s}'")).collect::<Vec<_>>().join(", ");
format!("Did you mean {quoted_suggestions}, or '{last}'?")
}
[] => unreachable!("checked above that suggestions is not empty"),
};
let _ = writeln!(
self.host.error(),
" Could not find information on crate '{}'. {}",
facts.crate_spec.name(),
suggestion_text
);
}
}
ProviderResult::VersionNotFound => {
let _ = writeln!(
self.host.error(),
" Could not find information on version {} of crate `{}`",
facts.crate_spec.version(),
facts.crate_spec.name()
);
}
ProviderResult::Error(err) => {
let _ = writeln!(
self.host.error(),
" Could not gather information for crate '{}': {err:#}",
facts.crate_spec
);
}
ProviderResult::Found(_) => {}
}
}
}
let has_expressions =
!self.config.deny_if_any.is_empty() || !self.config.accept_if_any.is_empty() || !self.config.accept_if_all.is_empty();
let should_eval = has_expressions || self.check;
let mut reportable_crates: Vec<ReportableCrate> = if should_eval {
analyzable_crates
.into_iter()
.map(|facts| {
let metrics: Vec<_> = flatten(&facts).collect();
let evaluation = evaluate(
&self.config.deny_if_any,
&self.config.accept_if_any,
&self.config.accept_if_all,
&metrics,
Local::now(),
)
.map_err(|e| {
let _ = writeln!(
self.host.error(),
"Warning: Could not evaluate crate '{}': {}",
facts.crate_spec.name(),
e
);
e
})
.ok();
ReportableCrate::new(
facts.crate_spec.name().to_string(),
facts.crate_spec.version().clone(),
metrics,
evaluation,
)
})
.collect()
} else {
analyzable_crates
.into_iter()
.map(|facts| {
let metrics: Vec<_> = flatten(&facts).collect();
ReportableCrate::new(
facts.crate_spec.name().to_string(),
facts.crate_spec.version().clone(),
metrics,
None,
)
})
.collect()
};
reportable_crates.sort_by_cached_key(|crate_info| (crate_info.name.clone(), crate_info.version.clone()));
let generating_reports = self.html.is_some() || self.excel.is_some() || self.csv.is_some() || self.json.is_some();
let show_console = self.console || (!generating_reports && !self.check);
if show_console && !reportable_crates.is_empty() {
let mut console_output = String::new();
let use_colors = match self.color {
ColorMode::Always => true,
ColorMode::Never => false,
ColorMode::Auto => {
use std::io::{IsTerminal, stdout};
stdout().is_terminal()
}
};
_ = generate_console(&reportable_crates, use_colors, &mut console_output);
let _ = write!(self.host.output(), "{console_output}");
}
if let Some(filename) = &self.html {
let mut html = String::new();
generate_html(&reportable_crates, Local::now(), &mut html)?;
fs::write(filename, html)?;
}
if let Some(filename) = &self.excel {
let mut file = fs::File::create(filename)?;
generate_xlsx(&reportable_crates, &mut file)?;
}
if let Some(filename) = &self.csv {
let mut csv_output = String::new();
generate_csv(&reportable_crates, &mut csv_output)?;
fs::write(filename, csv_output)?;
}
if let Some(filename) = &self.json {
let mut json_output = String::new();
generate_json(&reportable_crates, &mut json_output)?;
fs::write(filename, json_output)?;
}
if self.check {
let has_rejected = reportable_crates
.iter()
.any(|crate_info| crate_info.evaluation.as_ref().is_some_and(|eval| !eval.accepted));
if has_rejected {
return Err(ohno::AppError::new("one or more crates were not accepted"));
}
}
Ok(())
}
}