Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor a bit #23

Merged
merged 28 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
779 changes: 272 additions & 507 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ log = "0.4.22"
assert_cmd = "2.0.14"
predicates = "3.1.0"
serial_test = "3.2.0"
rstest = "0.23.0"
79 changes: 79 additions & 0 deletions src/check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use crate::{fetch_license_infos, license_info::LicenseInfo, CheckOutput, CondaDenyCheckConfig};
use anyhow::{Context, Result};
use colored::Colorize;
use log::debug;
use std::io::Write;

fn check_license_infos(config: &CondaDenyCheckConfig) -> Result<CheckOutput> {
let license_infos = fetch_license_infos(config.lockfile_or_prefix.clone())
.with_context(|| "Fetching license information failed.")?;

if config.osi {
debug!("Checking licenses for OSI compliance");
Ok(license_infos.osi_check())
} else {
debug!("Checking licenses against specified whitelist");
license_infos.check(config)
}
}

pub fn check<W: Write>(check_config: CondaDenyCheckConfig, mut out: W) -> Result<()> {
let (safe_dependencies, unsafe_dependencies) = check_license_infos(&check_config)?;

writeln!(
out,
"{}",
format_check_output(
safe_dependencies,
unsafe_dependencies.clone(),
)
)?;

if !unsafe_dependencies.is_empty() {
Err(anyhow::anyhow!("Unsafe licenses found"))
} else {
Ok(())
}
}

pub fn format_check_output(
safe_dependencies: Vec<LicenseInfo>,
unsafe_dependencies: Vec<LicenseInfo>,
) -> String {
let mut output = String::new();

if !unsafe_dependencies.is_empty() {
output.push_str(
format!(
"\n❌ {}:\n\n",
"The following dependencies are unsafe".red()
)
.as_str(),
);
for license_info in &unsafe_dependencies {
output.push_str(&license_info.pretty_print())
}
}

if unsafe_dependencies.is_empty() {
output.push_str(&format!(
"\n{}",
"✅ No unsafe licenses found! ✅".to_string().green()
));
} else {
output.push_str(&format!(
"\n{}",
"❌ Unsafe licenses found! ❌".to_string().red()
));
}

output.push_str(&format!(
"\nThere were {} safe licenses and {} unsafe licenses.\n",
safe_dependencies.len().to_string().green(),
unsafe_dependencies.len().to_string().red()
));

output.push('\n');

output
}
139 changes: 89 additions & 50 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::path::PathBuf;

use clap::ArgAction;
use clap_verbosity_flag::{ErrorLevel, Verbosity};

use clap::Parser;
use rattler_conda_types::Platform;

#[derive(Parser, Debug)]
#[command(name = "conda-deny", about = "Check and list licenses of pixi and conda environments", version = env!("CARGO_PKG_VERSION"))]
Expand All @@ -10,94 +13,130 @@ pub struct Cli {
pub verbose: Verbosity<ErrorLevel>,

#[command(subcommand)]
pub command: Commands,
pub command: CondaDenyCliConfig,
PaulKMueller marked this conversation as resolved.
Show resolved Hide resolved

/// Path to the conda-deny config file
#[arg(short, long, global = true)]
pub config: Option<String>,

#[arg(long, global = true)]
pub prefix: Option<Vec<String>>,

#[arg(short, long)]
pub lockfile: Option<Vec<String>>,

#[arg(short, long)]
pub platform: Option<Vec<String>>,

#[arg(short, long)]
pub environment: Option<Vec<String>>,
pub config: Option<PathBuf>,
}

#[derive(clap::Subcommand, Debug)]
pub enum Commands {
pub enum CondaDenyCliConfig {
Check {
#[arg(short, long, action = ArgAction::SetTrue)]
include_safe: bool,
/// Path to the pixi lockfile(s)
#[arg(short, long)]
lockfile: Option<Vec<PathBuf>>,

/// Path to the conda prefix(es)
#[arg(long, global = true)]
prefix: Option<Vec<PathBuf>>,

/// Platform(s) to check
#[arg(short, long)]
platform: Option<Vec<Platform>>,

/// Pixi environment(s) to check
#[arg(short, long)]
environment: Option<Vec<String>>,

/// Check against OSI licenses instead of custom license whitelists.
#[arg(short, long, action = ArgAction::SetTrue)]
pavelzw marked this conversation as resolved.
Show resolved Hide resolved
osi: bool,
osi: Option<bool>,

/// Ignore when encountering pypi packages instead of failing.
#[arg(long, action = ArgAction::SetTrue)]
ignore_pypi: bool,
ignore_pypi: Option<bool>,
},
List {
/// Path to the pixi lockfile(s)
#[arg(short, long)]
lockfile: Option<Vec<PathBuf>>,

/// Path to the conda prefix(es)
#[arg(long, global = true)]
prefix: Option<Vec<PathBuf>>,

/// Platform(s) to list
#[arg(short, long)]
platform: Option<Vec<Platform>>,

/// Pixi environment(s) to list
#[arg(short, long)]
environment: Option<Vec<String>>,

/// Ignore when encountering pypi packages instead of failing.
#[arg(long)]
ignore_pypi: Option<bool>,
},
List {},
}

#[cfg(test)]
mod tests {
use super::*;
impl CondaDenyCliConfig {
pub fn lockfile(&self) -> Option<Vec<PathBuf>> {
match self {
CondaDenyCliConfig::Check { lockfile, .. } => lockfile.clone(),
CondaDenyCliConfig::List { lockfile, .. } => lockfile.clone(),
}
}

#[test]
fn test_cli_check_include_safe() {
let cli = Cli::try_parse_from(vec!["conda-deny", "check", "--include-safe"]).unwrap();
match cli.command {
Commands::Check { include_safe, .. } => {
assert!(include_safe);
}
_ => panic!("Expected check subcommand with --include-safe"),
pub fn prefix(&self) -> Option<Vec<PathBuf>> {
match self {
CondaDenyCliConfig::Check { prefix, .. } => prefix.clone(),
CondaDenyCliConfig::List { prefix, .. } => prefix.clone(),
}
}

pub fn platform(&self) -> Option<Vec<Platform>> {
match self {
CondaDenyCliConfig::Check { platform, .. } => platform.clone(),
CondaDenyCliConfig::List { platform, .. } => platform.clone(),
}
}

pub fn environment(&self) -> Option<Vec<String>> {
match self {
CondaDenyCliConfig::Check { environment, .. } => environment.clone(),
CondaDenyCliConfig::List { environment, .. } => environment.clone(),
}
}

pub fn ignore_pypi(&self) -> Option<bool> {
match self {
CondaDenyCliConfig::Check { ignore_pypi, .. } => *ignore_pypi,
CondaDenyCliConfig::List { ignore_pypi, .. } => *ignore_pypi,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_cli_with_config() {
let cli =
Cli::try_parse_from(vec!["conda-deny", "list", "--config", "custom.toml"]).unwrap();
assert_eq!(cli.config.as_deref(), Some("custom.toml"));
assert_eq!(cli.config, Some("custom.toml".into()));
}

#[test]
fn test_cli_with_config_new_order() {
let cli =
Cli::try_parse_from(vec!["conda-deny", "check", "--config", "custom.toml"]).unwrap();
assert_eq!(cli.config.as_deref(), Some("custom.toml"));
assert_eq!(cli.config, Some("custom.toml".into()));
match cli.command {
Commands::Check { include_safe, .. } => {
assert!(!include_safe);
}
CondaDenyCliConfig::Check { .. } => {}
_ => panic!("Expected check subcommand with --config"),
}
}

#[test]
fn test_cli_with_check_arguments() {
let cli = Cli::try_parse_from(vec!["conda-deny", "check", "--include-safe"]).unwrap();
match cli.command {
Commands::Check { include_safe, .. } => {
assert!(include_safe);
}
_ => panic!("Expected check subcommand with --include-safe"),
}
}

#[test]
fn test_cli_with_check_osi() {
let cli = Cli::try_parse_from(vec!["conda-deny", "check", "--osi"]).unwrap();
match cli.command {
Commands::Check { osi, .. } => {
assert!(osi);
CondaDenyCliConfig::Check { osi, .. } => {
assert_eq!(osi, Some(true));
}
_ => panic!("Expected check subcommand with --osi"),
_ => panic!("Expected check subcommand with --include-safe"),
}
}
}
Loading
Loading