diff --git a/Cargo.toml b/Cargo.toml index 83d2a5f..4f6aaa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,5 @@ semver = "1.0" clap = { version = "3.1.17", features = ["derive"] } atty = "0.2" anyhow = "1" +spdx = "0.10.0" +itertools = "0.10.5" diff --git a/README.md b/README.md index dffcbc8..2656095 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ OPTIONS: --filter-platform Only include resolve dependencies matching the given target-triple -h, --help Print help information + -g, --gitlab Gitlab license scanner output -j, --json Detailed output as JSON --manifest-path Path to Cargo.toml --no-default-features Deactivate default features diff --git a/src/lib.rs b/src/lib.rs index 4f765eb..07ac325 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,8 @@ use anyhow::Result; use cargo_metadata::{ DepKindInfo, DependencyKind, Metadata, MetadataCommand, Node, NodeDep, Package, PackageId, }; +use itertools::Itertools; +use semver::Version; use serde_derive::Serialize; use std::collections::{HashMap, HashSet}; use std::io; @@ -78,6 +80,75 @@ impl DependencyDetails { } } +#[derive(Debug, Serialize, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] +struct GitlabDependency { + name: String, + version: Version, + package_manager: &'static str, + path: String, + licenses: Vec<&'static str>, +} + +#[derive(Debug, Serialize, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] +struct GitlabLicense { + id: &'static str, + name: &'static str, + url: String, +} + +impl GitlabLicense { + fn parse_licenses(dependency: &DependencyDetails) -> Result> { + let Some(license) = &dependency.license else {return Ok(HashSet::new())}; + let expression = spdx::Expression::parse_mode(license, spdx::ParseMode::LAX)?; + Ok(expression + .requirements() + .flat_map(|req| { + req.req.license.id().map(|license| Self { + id: license.name, + name: license.full_name, + url: Default::default(), + }) + }) + .collect()) + } +} + +#[derive(Debug, Serialize, Clone)] +struct GitlabLicenseScanningReport { + version: &'static str, + licenses: HashSet, + dependencies: Vec, +} + +impl TryFrom<&[DependencyDetails]> for GitlabLicenseScanningReport { + type Error = anyhow::Error; + fn try_from(dependencies: &[DependencyDetails]) -> Result { + let mut licenses = HashSet::new(); + let dependencies = dependencies + .iter() + .cloned() + .map(|dependency| { + let dep_licenses = GitlabLicense::parse_licenses(&dependency)?; + let license_ids = dep_licenses.iter().map(|license| license.id).collect(); + licenses.extend(dep_licenses); + Ok::<_, Self::Error>(GitlabDependency { + name: dependency.name, + version: dependency.version, + package_manager: "cargo", + path: Default::default(), + licenses: license_ids, + }) + }) + .try_collect()?; + + Ok(GitlabLicenseScanningReport { + version: "2.1", + dependencies, + licenses, + }) + } +} + #[derive(Default)] pub struct GetDependenciesOpt { pub avoid_dev_deps: bool, @@ -171,6 +242,13 @@ pub fn write_json(dependencies: &[DependencyDetails]) -> Result<()> { Ok(()) } +pub fn write_gitlab(dependencies: &[DependencyDetails]) -> Result<()> { + let dependencies = GitlabLicenseScanningReport::try_from(dependencies)?; + println!("{}", serde_json::to_string_pretty(&dependencies)?); + + Ok(()) +} + #[cfg(test)] mod test { use super::*; diff --git a/src/main.rs b/src/main.rs index ed3ae8b..6105d46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,8 @@ use ansi_term::Colour::Green; use ansi_term::Style; use anyhow::Result; use cargo_license::{ - get_dependencies_from_cargo_lock, write_json, write_tsv, DependencyDetails, GetDependenciesOpt, + get_dependencies_from_cargo_lock, write_gitlab, write_json, write_tsv, DependencyDetails, + GetDependenciesOpt, }; use cargo_metadata::{CargoOpt, MetadataCommand}; use clap::Parser; @@ -104,7 +105,7 @@ fn one_license_per_line( } } -fn colored<'a, 'b>(s: &'a str, style: &'b Style, enable_color: bool) -> Cow<'a, str> { +fn colored<'a>(s: &'a str, style: &Style, enable_color: bool) -> Cow<'a, str> { if enable_color { Cow::Owned(format!("{}", style.paint(s))) } else { @@ -143,6 +144,10 @@ struct Opt { /// Detailed output as JSON. json: bool, + #[clap(short, long)] + /// Gitlab license scanner output + gitlab: bool, + #[clap(long)] /// Exclude development dependencies avoid_dev_deps: bool, @@ -244,6 +249,8 @@ fn run() -> Result<()> { write_tsv(&dependencies)?; } else if opt.json { write_json(&dependencies)?; + } else if opt.gitlab { + write_gitlab(&dependencies)?; } else if opt.do_not_bundle { one_license_per_line(dependencies, opt.authors, enable_color); } else { @@ -257,7 +264,7 @@ fn main() { Ok(_) => 0, Err(e) => { for cause in e.chain() { - eprintln!("{}", cause); + eprintln!("{cause}"); } 1 }