use std::borrow::Cow;
use std::collections::{BTreeSet, HashMap};
use std::path::PathBuf;
use cargo_metadata::DependencyKind;
use cargo_platform::{Cfg, Platform};
#[cfg(feature = "macros")]
use proc_macro2::TokenStream;
#[cfg(feature = "macros")]
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),
#[error("no dependency graph found")]
NoDependencyGraph,
#[error("no root node found in dependency graph")]
NoRootNode,
}
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 {
#[cfg(feature = "macros")]
#[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(),
}
}
}
#[cfg(feature = "macros")]
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 {
#[cfg(feature = "macros")]
#[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(),
}
}
}
#[cfg(feature = "macros")]
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)
})
}
}
impl TryFrom<&cargo_metadata::Package> for Crate {
type Error = Error;
fn try_from(package: &cargo_metadata::Package) -> Result<Self> {
Ok(Crate {
name: package.name.clone(),
version: package.version.to_string(),
authors: package.authors.clone(),
license: if let Some(license_expr) = &package.license {
CrateLicense::SpdxExpression(spdx::Expression::parse_mode(
license_expr,
spdx::ParseMode::LAX,
)?)
} 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.clone()));
},
website: package
.homepage
.as_ref()
.or(package.repository.as_ref())
.or(package.documentation.as_ref())
.ok_or(Error::NoWebsite(package.name.clone()))
.map(String::from)?,
})
}
}
#[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 {
#[cfg(feature = "macros")]
#[doc(hidden)]
pub fn __macro_internal_new_spdx_expression(expr: &str) -> Self {
Self::SpdxExpression(spdx::Expression::parse_mode(expr, spdx::ParseMode::LAX).unwrap())
}
}
#[cfg(feature = "macros")]
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()))
}
})
}
}
#[derive(Debug, Default, PartialEq)]
pub enum CollectPlatform {
#[default]
Any,
Static {
target: String,
cfg: Vec<Cfg>,
},
#[cfg(feature = "current_platform")]
Current,
}
impl CollectPlatform {
fn target(&self) -> Option<&str> {
match self {
CollectPlatform::Any => None,
CollectPlatform::Static { target, .. } => Some(target),
#[cfg(feature = "current_platform")]
CollectPlatform::Current => Some(current_platform::CURRENT_PLATFORM),
}
}
fn cfg(&self) -> Option<Cow<[Cfg]>> {
match self {
CollectPlatform::Any => None,
CollectPlatform::Static { cfg, .. } => Some(Cow::Borrowed(cfg)),
#[cfg(feature = "current_platform")]
CollectPlatform::Current => Some(
serde_json::from_str::<HashMap<String, String>>(env!("CFG_OPTIONS_JSON"))
.expect("could not parse CFG_OPTIONS_JSON content")
.into_iter()
.map(|(opt, val)| {
if val.is_empty() {
Cfg::Name(opt)
} else {
Cfg::KeyPair(opt, val)
}
})
.collect(),
),
}
}
}
#[cfg(feature = "macros")]
impl syn::parse::Parse for CollectPlatform {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
use syn::{ext::IdentExt, parenthesized, Ident, LitStr, Token};
let inner;
parenthesized!(inner in input);
Ok(match inner.call(Ident::parse_any)?.to_string().as_str() {
"any" => CollectPlatform::Any,
"static" => {
let mut target = None;
let mut cfg = None;
let inner_static;
parenthesized!(inner_static in inner);
while !inner_static.is_empty() {
match inner_static.call(Ident::parse_any)?.to_string().as_str() {
"target" => {
inner_static.parse::<syn::Token![=]>()?;
target = Some(inner_static.parse::<LitStr>()?.value());
}
"cfg" => {
cfg = Some(Vec::new());
let inner_cfg;
parenthesized!(inner_cfg in inner_static);
while !inner_cfg.is_empty() {
let key = inner_cfg.call(Ident::parse_any)?;
if inner_cfg.parse::<syn::Token![=]>().is_ok() {
let value = inner_cfg.parse::<LitStr>()?.value();
cfg.as_mut()
.unwrap()
.push(Cfg::KeyPair(key.to_string(), value))
} else {
cfg.as_mut().unwrap().push(Cfg::Name(key.to_string()))
}
if inner_cfg.parse::<Token![,]>().is_err() {
break;
}
}
}
_ => return Err(inner_static.error("unknown static platform argument")),
}
if inner_static.parse::<Token![,]>().is_err() {
break;
}
}
if target.is_none() || cfg.is_none() {
return Err(inner_static.error("static platform must specify target and cfg"));
}
CollectPlatform::Static {
target: target.unwrap(),
cfg: cfg.unwrap(),
}
}
#[cfg(feature = "current_platform")]
"current" => CollectPlatform::Current,
#[cfg(not(feature = "current_platform"))]
"current" => return Err(inner.error("current_platform feature is not enabled")),
_ => return Err(inner.error("unknown platform collection variant")),
})
}
}
#[derive(Debug, Default, PartialEq)]
pub struct CollectConfig {
pub dev: bool,
pub build: bool,
pub platform: CollectPlatform,
}
impl CollectConfig {
fn kinds(&self) -> Vec<DependencyKind> {
let mut kinds = vec![DependencyKind::Normal];
if self.dev {
kinds.push(DependencyKind::Development);
}
if self.build {
kinds.push(DependencyKind::Build);
}
kinds
}
}
#[cfg(feature = "macros")]
impl syn::parse::Parse for CollectConfig {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
use syn::{ext::IdentExt, Error, Ident, LitBool, Token};
let mut config = Self::default();
while !input.is_empty() {
let key = input.call(Ident::parse_any)?;
match key.to_string().as_str() {
"dev" => {
if input.parse::<Token![=]>().is_ok() {
config.dev = input.parse::<LitBool>()?.value();
} else {
config.dev = true;
}
}
"build" => {
if input.parse::<Token![=]>().is_ok() {
config.build = input.parse::<LitBool>()?.value();
} else {
config.build = true;
}
}
"platform" => {
config.platform = input.parse()?;
}
_ => {
return Err(Error::new(key.span(), "unrecognized argument"));
}
}
if input.parse::<Token![,]>().is_err() {
break;
}
}
Ok(config)
}
}
pub fn collect(config: CollectConfig) -> Result<Licensing> {
collect_internal(None::<PathBuf>, config)
}
pub fn collect_from_manifest(
manifest_path: impl Into<PathBuf>,
config: CollectConfig,
) -> Result<Licensing> {
collect_internal(Some(manifest_path), config)
}
fn collect_internal(
manifest_path: Option<impl Into<PathBuf>>,
config: CollectConfig,
) -> 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 resolve_map = HashMap::new();
let resolve = metadata.resolve.as_ref().ok_or(Error::NoDependencyGraph)?;
for node in &resolve.nodes {
resolve_map.insert(node.id.clone(), node);
}
let mut licensing = Licensing {
packages: collect_tree(
&metadata,
&resolve_map,
resolve_map
.get(resolve.root.as_ref().ok_or(Error::NoRootNode)?)
.unwrap(),
&config,
true,
)?
.into_iter()
.collect(),
licenses: Vec::new(),
exceptions: Vec::new(),
};
for package in &licensing.packages {
if let CrateLicense::SpdxExpression(expr) = &package.license {
for node in expr.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);
}
}
}
};
}
licensing.packages.sort_unstable();
licensing.licenses.sort_unstable();
licensing.licenses.dedup();
licensing.exceptions.sort_unstable();
licensing.exceptions.dedup();
Ok(licensing)
}
fn collect_tree(
metadata: &cargo_metadata::Metadata,
resolve: &HashMap<cargo_metadata::PackageId, &cargo_metadata::Node>,
node: &cargo_metadata::Node,
config: &CollectConfig,
root: bool,
) -> Result<BTreeSet<Crate>> {
let mut crates = BTreeSet::new();
let package = &metadata[&node.id];
crates.insert(package.try_into()?);
let kinds = config.kinds();
for dep in &node.deps {
let mut selected_kinds = Vec::new();
let mut mismatching_targets = Vec::new();
let mut cfg_mismatch = false;
for kind in &dep.dep_kinds {
let target = kind.target.as_ref();
let kind = kind.kind;
if kinds.contains(&kind) {
selected_kinds.push(kind);
}
if let Some(target) = target {
match (config.platform.target(), config.platform.cfg(), target) {
(Some(wanted_target), _, Platform::Name(actual_target)) => {
if wanted_target != actual_target {
mismatching_targets.push(actual_target);
}
}
(_, Some(wanted_cfgs), Platform::Cfg(actual_cfgs)) => {
if !actual_cfgs.matches(&wanted_cfgs) {
cfg_mismatch = true;
}
}
(None, _, Platform::Name(_)) | (_, None, Platform::Cfg(_)) => (),
}
}
}
if selected_kinds.is_empty() || !mismatching_targets.is_empty() || cfg_mismatch {
continue;
}
if !root
&& selected_kinds.len() == 1
&& *selected_kinds.first().unwrap() == DependencyKind::Development
{
continue;
}
crates.append(&mut collect_tree(
metadata,
resolve,
resolve.get(&dep.pkg).unwrap(),
config,
false,
)?)
}
Ok(crates)
}
#[cfg(all(test, feature = "macros"))]
mod tests {
use cargo_platform::Cfg;
use syn::parse_quote;
use super::{CollectConfig, CollectPlatform};
macro_rules! should_panic_multiple {
($($name:ident => $body:block),* $(,)?) => {
$(
#[test]
#[should_panic]
fn $name() $body
)*
}
}
#[test]
fn collect_config_parse_empty() {
let config: CollectConfig = parse_quote!();
assert_eq!(config, CollectConfig::default());
}
#[test]
fn collect_config_parse_full() {
let config: CollectConfig = parse_quote!(build, dev);
assert_eq!(
config,
CollectConfig {
build: true,
dev: true,
..Default::default()
}
);
}
#[test]
fn collect_config_parse_args() {
let config: CollectConfig = parse_quote!(build = true, dev = true);
assert_eq!(
config,
CollectConfig {
build: true,
dev: true,
..Default::default()
}
);
let config: CollectConfig = parse_quote!(build = false, dev = false);
assert_eq!(
config,
CollectConfig {
build: false,
dev: false,
..Default::default()
}
);
}
#[test]
fn collect_config_parse_single_keyword() {
let config: CollectConfig = parse_quote!(build);
assert_eq!(
config,
CollectConfig {
build: true,
..Default::default()
}
);
let config: CollectConfig = parse_quote!(dev);
assert_eq!(
config,
CollectConfig {
dev: true,
..Default::default()
}
);
}
#[test]
fn collect_config_parse_platform_any() {
let config: CollectConfig = parse_quote!(platform(any));
assert_eq!(
config,
CollectConfig {
platform: CollectPlatform::Any,
..Default::default()
}
);
}
#[test]
fn collect_config_parse_platform_static_target() {
let config: CollectConfig =
parse_quote!(platform(static(target = "x86_64-linux-gnu", cfg())));
assert_eq!(
config,
CollectConfig {
platform: CollectPlatform::Static {
target: "x86_64-linux-gnu".to_string(),
cfg: vec![]
},
..Default::default()
}
);
}
#[test]
fn collect_config_parse_platform_static_cfg_name() {
let config: CollectConfig =
parse_quote!(platform(static(target = "aarch64-apple-darwin", cfg(foo))));
assert_eq!(
config,
CollectConfig {
platform: CollectPlatform::Static {
target: "aarch64-apple-darwin".to_string(),
cfg: vec![Cfg::Name("foo".to_string())],
},
..Default::default()
}
);
}
#[test]
fn collect_config_parse_platform_static_cfg_keypair() {
let config: CollectConfig =
parse_quote!(platform(static(target = "x86_64-pc-windows-gnu", cfg(foo = "bar"))));
assert_eq!(
config,
CollectConfig {
platform: CollectPlatform::Static {
target: "x86_64-pc-windows-gnu".to_string(),
cfg: vec![Cfg::KeyPair("foo".to_string(), "bar".to_string())],
},
..Default::default()
}
);
}
#[test]
fn collect_config_parse_platform_static_cfg_multiple() {
let config: CollectConfig = parse_quote!(platform(static(target = "wasm32-unknown-unknown", cfg(foo = "bar", baz))));
assert_eq!(
config,
CollectConfig {
platform: CollectPlatform::Static {
target: "wasm32-unknown-unknown".to_string(),
cfg: vec![
Cfg::KeyPair("foo".to_string(), "bar".to_string()),
Cfg::Name("baz".to_string())
],
},
..Default::default()
}
);
}
should_panic_multiple! {
collect_config_macro_parse_invalid1 => { let _: CollectConfig = parse_quote!(foo); },
collect_config_macro_parse_invalid2 => { let _: CollectConfig = parse_quote!(dev = 1); },
collect_config_macro_parse_invalid3 => { let _: CollectConfig = parse_quote!(build = "foo"); },
collect_config_macro_parse_invalid4 => { let _: CollectConfig = parse_quote!(platform()); },
collect_config_macro_parse_invalid5 => { let _: CollectConfig = parse_quote!(platform(xy)); },
collect_config_macro_parse_invalid6 => { let _: CollectConfig = parse_quote!(platform(static())); },
collect_config_macro_parse_invalid7 => { let _: CollectConfig = parse_quote!(platform(static(target = "x86_64-unknown-linux-gnu"))); },
collect_config_macro_parse_invalid8 => { let _: CollectConfig = parse_quote!(platform(static(cfg(unix)))); },
}
}