use std::path::PathBuf;
use proc_macro2::TokenStream;
use quote::{quote, ToTokens, TokenStreamExt};
use spdx::{ExceptionId, LicenseId};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("cargo metadata invocation failed: {0}")]
CargoMetadata(#[from] cargo_metadata::Error),
#[error("parsing SPDX expression failed: {0}")]
SpdxParse(#[from] spdx::ParseError),
#[error("IO Error: {0}")]
Io(#[from] std::io::Error),
#[error("no license specified for crate {0}")]
NoLicense(String),
#[error("non-SPDX license identifier specified for crate {0}")]
NonSpdxLicense(String),
#[error("no website found for crate {0}")]
NoWebsite(String),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Licensing {
pub packages: Vec<Crate>,
pub licenses: Vec<LicenseId>,
pub exceptions: Vec<ExceptionId>,
}
impl Licensing {
#[doc(hidden)]
pub fn __macro_internal_new(
packages: &[Crate],
licenses: &[&str],
exceptions: &[&str],
) -> Self {
Self {
packages: packages.to_vec(),
licenses: licenses
.iter()
.map(|id| spdx::license_id(id))
.map(Option::unwrap)
.collect(),
exceptions: exceptions
.iter()
.map(|id| spdx::exception_id(id))
.map(Option::unwrap)
.collect(),
}
}
}
impl ToTokens for Licensing {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
packages,
licenses,
exceptions,
} = self;
let licenses = licenses.iter().map(|l| l.name.to_string());
let exceptions = exceptions.iter().map(|e| e.name.to_string());
tokens.append_all(quote! {
::embed_licensing::Licensing::__macro_internal_new(
&[#(#packages),*],
&[#(#licenses),*],
&[#(#exceptions),*],
)
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Crate {
pub name: String,
pub version: String,
pub authors: Vec<String>,
pub license: CrateLicense,
pub website: String,
}
impl PartialOrd for Crate {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Crate {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.name.cmp(&other.name)
}
}
impl Crate {
#[doc(hidden)]
pub fn __macro_internal_new(
name: &str,
version: &str,
authors: &[&str],
license: CrateLicense,
website: &str,
) -> Self {
Self {
name: name.to_string(),
version: version.to_string(),
authors: authors.iter().map(|s| s.to_string()).collect(),
license,
website: website.to_string(),
}
}
}
impl ToTokens for Crate {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
name,
version,
authors,
license,
website,
} = self;
tokens.append_all(quote! {
::embed_licensing::Crate::__macro_internal_new(#name, #version, &[#(#authors),*], #license, #website)
})
}
}
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)] pub enum CrateLicense {
SpdxExpression(spdx::Expression),
Other(String),
}
impl PartialEq for CrateLicense {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::SpdxExpression(a), Self::SpdxExpression(b)) => a == b,
(Self::Other(a), Self::Other(b)) => a == b,
_ => false,
}
}
}
impl Eq for CrateLicense {}
impl CrateLicense {
#[doc(hidden)]
pub fn __macro_internal_new_spdx_expression(expr: &str) -> Self {
Self::SpdxExpression(spdx::Expression::parse_mode(expr, spdx::ParseMode::LAX).unwrap())
}
}
impl ToTokens for CrateLicense {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.append_all(match self {
Self::SpdxExpression(expr) => {
let expr_string = expr.to_string();
quote!(
::embed_licensing::CrateLicense::__macro_internal_new_spdx_expression(#expr_string)
)
}
Self::Other(content) => {
quote!(::embed_licensing::CrateLicense::Other(#content.to_string()))
}
})
}
}
pub fn collect() -> Result<Licensing> {
collect_internal(None::<PathBuf>)
}
pub fn collect_from_manifest(manifest_path: impl Into<PathBuf>) -> Result<Licensing> {
collect_internal(Some(manifest_path))
}
fn collect_internal(manifest_path: Option<impl Into<PathBuf>>) -> Result<Licensing> {
let mut cmd = cargo_metadata::MetadataCommand::new();
if let Some(manifest_path) = manifest_path {
cmd.manifest_path(manifest_path);
}
let metadata = cmd.exec()?;
let mut licensing = Licensing {
packages: Vec::new(),
licenses: Vec::new(),
exceptions: Vec::new(),
};
for package in metadata.packages {
let license = if let Some(license_expr) = package.license {
let license = spdx::Expression::parse_mode(&license_expr, spdx::ParseMode::LAX)?;
for node in license.iter() {
if let spdx::expression::ExprNode::Req(req) = node {
licensing.licenses.push(
req.req
.license
.id()
.ok_or(Error::NonSpdxLicense(package.name.clone()))?,
);
if let Some(exception) = req.req.exception {
licensing.exceptions.push(exception);
}
}
}
CrateLicense::SpdxExpression(license)
} else if let Some(license_file) = package.license_file {
CrateLicense::Other(std::fs::read_to_string(
package
.manifest_path
.clone()
.parent()
.expect("the crate’s manifest path does not have a parent directory")
.join(license_file),
)?)
} else {
return Err(Error::NoLicense(package.name));
};
licensing.packages.push(Crate {
name: package.name.clone(),
version: package.version.to_string(),
authors: package.authors,
license,
website: package
.homepage
.or(package.repository)
.or(package.documentation)
.ok_or(Error::NoWebsite(package.name))?,
})
}
licensing.packages.sort_unstable();
licensing.licenses.sort_unstable();
licensing.licenses.dedup();
licensing.exceptions.sort_unstable();
licensing.exceptions.dedup();
Ok(licensing)
}