1use std::borrow::Cow;
6use std::collections::{BTreeSet, HashMap};
7use std::path::PathBuf;
8
9use cargo_metadata::DependencyKind;
10use cargo_platform::{Cfg, Platform};
11#[cfg(feature = "macros")]
12use proc_macro2::TokenStream;
13#[cfg(feature = "macros")]
14use quote::{quote, ToTokens, TokenStreamExt};
15use spdx::{ExceptionId, LicenseId};
16
17#[derive(Debug, thiserror::Error)]
19pub enum Error {
20 #[error("cargo metadata invocation failed: {0}")]
21 CargoMetadata(#[from] cargo_metadata::Error),
22 #[error("parsing SPDX expression failed: {0}")]
23 SpdxParse(#[from] spdx::ParseError),
24 #[error("IO Error: {0}")]
25 Io(#[from] std::io::Error),
26
27 #[error("no license specified for crate {0}")]
29 NoLicense(String),
30 #[error("non-SPDX license identifier specified for crate {0}")]
32 NonSpdxLicense(String),
33 #[error("no website found for crate {0}")]
41 NoWebsite(String),
42
43 #[error("no dependency graph found")]
44 NoDependencyGraph,
45 #[error("no root node found in dependency graph")]
46 NoRootNode,
47}
48
49type Result<T> = std::result::Result<T, Error>;
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct Licensing {
54 pub packages: Vec<Crate>,
56 pub licenses: Vec<LicenseId>,
62 pub exceptions: Vec<ExceptionId>,
64}
65
66impl Licensing {
67 #[cfg(feature = "macros")]
68 #[doc(hidden)]
69 pub fn __macro_internal_new(
70 packages: &[Crate],
71 licenses: &[&str],
72 exceptions: &[&str],
73 ) -> Self {
74 Self {
75 packages: packages.to_vec(),
76 licenses: licenses
77 .iter()
78 .map(|id| spdx::license_id(id))
79 .map(Option::unwrap)
80 .collect(),
81 exceptions: exceptions
82 .iter()
83 .map(|id| spdx::exception_id(id))
84 .map(Option::unwrap)
85 .collect(),
86 }
87 }
88}
89
90#[cfg(feature = "macros")]
91impl ToTokens for Licensing {
92 fn to_tokens(&self, tokens: &mut TokenStream) {
93 let Self {
94 packages,
95 licenses,
96 exceptions,
97 } = self;
98
99 let licenses = licenses.iter().map(|l| l.name.to_string());
100 let exceptions = exceptions.iter().map(|e| e.name.to_string());
101
102 tokens.append_all(quote! {
103 ::embed_licensing::Licensing::__macro_internal_new(
104 &[#(#packages),*],
105 &[#(#licenses),*],
106 &[#(#exceptions),*],
107 )
108 })
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct Crate {
118 pub name: String,
120 pub version: String,
122 pub authors: Vec<String>,
124 pub license: CrateLicense,
135 pub website: String,
136}
137
138impl PartialOrd for Crate {
139 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
140 Some(self.cmp(other))
141 }
142}
143
144impl Ord for Crate {
145 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
146 self.name.cmp(&other.name)
147 }
148}
149
150impl Crate {
151 #[cfg(feature = "macros")]
152 #[doc(hidden)]
153 pub fn __macro_internal_new(
154 name: &str,
155 version: &str,
156 authors: &[&str],
157 license: CrateLicense,
158 website: &str,
159 ) -> Self {
160 Self {
161 name: name.to_string(),
162 version: version.to_string(),
163 authors: authors.iter().map(|s| s.to_string()).collect(),
164 license,
165 website: website.to_string(),
166 }
167 }
168}
169
170#[cfg(feature = "macros")]
171impl ToTokens for Crate {
172 fn to_tokens(&self, tokens: &mut TokenStream) {
173 let Self {
174 name,
175 version,
176 authors,
177 license,
178 website,
179 } = self;
180
181 tokens.append_all(quote! {
182 ::embed_licensing::Crate::__macro_internal_new(#name, #version, &[#(#authors),*], #license, #website)
183 })
184 }
185}
186
187impl TryFrom<&cargo_metadata::Package> for Crate {
188 type Error = Error;
189
190 fn try_from(package: &cargo_metadata::Package) -> Result<Self> {
191 Ok(Crate {
192 name: package.name.clone(),
193 version: package.version.to_string(),
194 authors: package.authors.clone(),
195 license: if let Some(license_expr) = &package.license {
196 CrateLicense::SpdxExpression(spdx::Expression::parse_mode(
197 license_expr,
198 spdx::ParseMode::LAX,
199 )?)
200 } else if let Some(license_file) = &package.license_file {
201 CrateLicense::Other(std::fs::read_to_string(
202 package
203 .manifest_path
204 .clone()
205 .parent()
206 .expect("the crate’s manifest path does not have a parent directory")
207 .join(license_file),
208 )?)
209 } else {
210 return Err(Error::NoLicense(package.name.clone()));
211 },
212 website: package
213 .homepage
214 .as_ref()
215 .or(package.repository.as_ref())
216 .or(package.documentation.as_ref())
217 .ok_or(Error::NoWebsite(package.name.clone()))
218 .map(String::from)?,
219 })
220 }
221}
222
223#[derive(Clone, Debug)]
225#[allow(clippy::large_enum_variant)] pub enum CrateLicense {
227 SpdxExpression(spdx::Expression),
229 Other(String),
231}
232
233impl PartialEq for CrateLicense {
234 fn eq(&self, other: &Self) -> bool {
235 match (self, other) {
236 (Self::SpdxExpression(a), Self::SpdxExpression(b)) => a == b,
237 (Self::Other(a), Self::Other(b)) => a == b,
238 _ => false,
239 }
240 }
241}
242
243impl Eq for CrateLicense {}
244
245impl CrateLicense {
246 #[cfg(feature = "macros")]
247 #[doc(hidden)]
248 pub fn __macro_internal_new_spdx_expression(expr: &str) -> Self {
249 Self::SpdxExpression(spdx::Expression::parse_mode(expr, spdx::ParseMode::LAX).unwrap())
250 }
251}
252
253#[cfg(feature = "macros")]
254impl ToTokens for CrateLicense {
255 fn to_tokens(&self, tokens: &mut TokenStream) {
256 tokens.append_all(match self {
257 Self::SpdxExpression(expr) => {
258 let expr_string = expr.to_string();
259 quote!(
260 ::embed_licensing::CrateLicense::__macro_internal_new_spdx_expression(#expr_string)
261 )
262 }
263 Self::Other(content) => {
264 quote!(::embed_licensing::CrateLicense::Other(#content.to_string()))
265 }
266 })
267 }
268}
269
270#[derive(Debug, Default, PartialEq)]
271pub enum CollectPlatform {
272 #[default]
274 Any,
275 Static {
277 target: String,
280 cfg: Vec<Cfg>,
282 },
283 #[cfg(feature = "current_platform")]
285 Current,
286}
287
288impl CollectPlatform {
289 fn target(&self) -> Option<&str> {
290 match self {
291 CollectPlatform::Any => None,
292 CollectPlatform::Static { target, .. } => Some(target),
293 #[cfg(feature = "current_platform")]
294 CollectPlatform::Current => Some(current_platform::CURRENT_PLATFORM),
295 }
296 }
297
298 fn cfg(&self) -> Option<Cow<[Cfg]>> {
299 match self {
300 CollectPlatform::Any => None,
301 CollectPlatform::Static { cfg, .. } => Some(Cow::Borrowed(cfg)),
302 #[cfg(feature = "current_platform")]
303 CollectPlatform::Current => Some(
304 serde_json::from_str::<HashMap<String, String>>(env!("CFG_OPTIONS_JSON"))
305 .expect("could not parse CFG_OPTIONS_JSON content")
306 .into_iter()
307 .map(|(opt, val)| {
308 if val.is_empty() {
309 Cfg::Name(opt)
310 } else {
311 Cfg::KeyPair(opt, val)
312 }
313 })
314 .collect(),
315 ),
316 }
317 }
318}
319
320#[cfg(feature = "macros")]
321impl syn::parse::Parse for CollectPlatform {
322 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
323 use syn::{ext::IdentExt, parenthesized, Ident, LitStr, Token};
324
325 let inner;
326 parenthesized!(inner in input);
327
328 Ok(match inner.call(Ident::parse_any)?.to_string().as_str() {
329 "any" => CollectPlatform::Any,
330 "static" => {
331 let mut target = None;
332 let mut cfg = None;
333
334 let inner_static;
335 parenthesized!(inner_static in inner);
336 while !inner_static.is_empty() {
337 match inner_static.call(Ident::parse_any)?.to_string().as_str() {
338 "target" => {
339 inner_static.parse::<syn::Token![=]>()?;
340 target = Some(inner_static.parse::<LitStr>()?.value());
341 }
342 "cfg" => {
343 cfg = Some(Vec::new());
344
345 let inner_cfg;
346 parenthesized!(inner_cfg in inner_static);
347 while !inner_cfg.is_empty() {
348 let key = inner_cfg.call(Ident::parse_any)?;
349 if inner_cfg.parse::<syn::Token![=]>().is_ok() {
350 let value = inner_cfg.parse::<LitStr>()?.value();
351 cfg.as_mut()
352 .unwrap()
353 .push(Cfg::KeyPair(key.to_string(), value))
354 } else {
355 cfg.as_mut().unwrap().push(Cfg::Name(key.to_string()))
356 }
357
358 if inner_cfg.parse::<Token![,]>().is_err() {
359 break;
360 }
361 }
362 }
363 _ => return Err(inner_static.error("unknown static platform argument")),
364 }
365
366 if inner_static.parse::<Token![,]>().is_err() {
367 break;
368 }
369 }
370
371 if target.is_none() || cfg.is_none() {
372 return Err(inner_static.error("static platform must specify target and cfg"));
373 }
374
375 CollectPlatform::Static {
376 target: target.unwrap(),
377 cfg: cfg.unwrap(),
378 }
379 }
380 #[cfg(feature = "current_platform")]
381 "current" => CollectPlatform::Current,
382 #[cfg(not(feature = "current_platform"))]
383 "current" => return Err(inner.error("current_platform feature is not enabled")),
384 _ => return Err(inner.error("unknown platform collection variant")),
385 })
386 }
387}
388
389#[derive(Debug, Default, PartialEq)]
391pub struct CollectConfig {
392 pub dev: bool,
400 pub build: bool,
407 pub platform: CollectPlatform,
411}
412
413impl CollectConfig {
414 fn kinds(&self) -> Vec<DependencyKind> {
415 let mut kinds = vec![DependencyKind::Normal];
416 if self.dev {
417 kinds.push(DependencyKind::Development);
418 }
419 if self.build {
420 kinds.push(DependencyKind::Build);
421 }
422 kinds
423 }
424}
425
426#[cfg(feature = "macros")]
427impl syn::parse::Parse for CollectConfig {
428 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
429 use syn::{ext::IdentExt, Error, Ident, LitBool, Token};
430
431 let mut config = Self::default();
432
433 while !input.is_empty() {
434 let key = input.call(Ident::parse_any)?;
435
436 match key.to_string().as_str() {
437 "dev" => {
438 if input.parse::<Token![=]>().is_ok() {
439 config.dev = input.parse::<LitBool>()?.value();
440 } else {
441 config.dev = true;
442 }
443 }
444 "build" => {
445 if input.parse::<Token![=]>().is_ok() {
446 config.build = input.parse::<LitBool>()?.value();
447 } else {
448 config.build = true;
449 }
450 }
451 "platform" => {
452 config.platform = input.parse()?;
453 }
454 _ => {
455 return Err(Error::new(key.span(), "unrecognized argument"));
456 }
457 }
458
459 if input.parse::<Token![,]>().is_err() {
460 break;
461 }
462 }
463
464 Ok(config)
465 }
466}
467
468pub fn collect(config: CollectConfig) -> Result<Licensing> {
475 collect_internal(None::<PathBuf>, config)
476}
477
478pub fn collect_from_manifest(
480 manifest_path: impl Into<PathBuf>,
481 config: CollectConfig,
482) -> Result<Licensing> {
483 collect_internal(Some(manifest_path), config)
484}
485
486fn collect_internal(
487 manifest_path: Option<impl Into<PathBuf>>,
488 config: CollectConfig,
489) -> Result<Licensing> {
490 let mut cmd = cargo_metadata::MetadataCommand::new();
491 if let Some(manifest_path) = manifest_path {
492 cmd.manifest_path(manifest_path);
493 }
494 let metadata = cmd.exec()?;
495
496 let mut resolve_map = HashMap::new();
497 let resolve = metadata.resolve.as_ref().ok_or(Error::NoDependencyGraph)?;
498 for node in &resolve.nodes {
499 resolve_map.insert(node.id.clone(), node);
500 }
501
502 let mut licensing = Licensing {
503 packages: collect_tree(
504 &metadata,
505 &resolve_map,
506 resolve_map
507 .get(resolve.root.as_ref().ok_or(Error::NoRootNode)?)
508 .unwrap(),
509 &config,
510 true,
511 )?
512 .into_iter()
513 .collect(),
514 licenses: Vec::new(),
515 exceptions: Vec::new(),
516 };
517
518 for package in &licensing.packages {
519 if let CrateLicense::SpdxExpression(expr) = &package.license {
520 for node in expr.iter() {
521 if let spdx::expression::ExprNode::Req(req) = node {
522 licensing.licenses.push(
523 req.req
524 .license
525 .id()
526 .ok_or(Error::NonSpdxLicense(package.name.clone()))?,
527 );
528
529 if let Some(exception) = req.req.exception {
530 licensing.exceptions.push(exception);
531 }
532 }
533 }
534 };
535 }
536
537 licensing.packages.sort_unstable();
538
539 licensing.licenses.sort_unstable();
540 licensing.licenses.dedup();
541
542 licensing.exceptions.sort_unstable();
543 licensing.exceptions.dedup();
544
545 Ok(licensing)
546}
547
548fn collect_tree(
549 metadata: &cargo_metadata::Metadata,
550 resolve: &HashMap<cargo_metadata::PackageId, &cargo_metadata::Node>,
551 node: &cargo_metadata::Node,
552 config: &CollectConfig,
553 root: bool,
554) -> Result<BTreeSet<Crate>> {
555 let mut crates = BTreeSet::new();
556
557 let package = &metadata[&node.id];
558
559 crates.insert(package.try_into()?);
560
561 let kinds = config.kinds();
562
563 for dep in &node.deps {
564 let mut selected_kinds = Vec::new();
565 let mut mismatching_targets = Vec::new();
566 let mut cfg_mismatch = false;
567
568 for kind in &dep.dep_kinds {
569 let target = kind.target.as_ref();
570 let kind = kind.kind;
571
572 if kinds.contains(&kind) {
573 selected_kinds.push(kind);
574 }
575
576 if let Some(target) = target {
577 match (config.platform.target(), config.platform.cfg(), target) {
578 (Some(wanted_target), _, Platform::Name(actual_target)) => {
579 if wanted_target != actual_target {
580 mismatching_targets.push(actual_target);
581 }
582 }
583 (_, Some(wanted_cfgs), Platform::Cfg(actual_cfgs)) => {
584 if !actual_cfgs.matches(&wanted_cfgs) {
585 cfg_mismatch = true;
586 }
587 }
588 (None, _, Platform::Name(_)) | (_, None, Platform::Cfg(_)) => (),
589 }
590 }
591 }
592
593 if selected_kinds.is_empty() || !mismatching_targets.is_empty() || cfg_mismatch {
594 continue;
595 }
596
597 if !root
614 && selected_kinds.len() == 1
615 && *selected_kinds.first().unwrap() == DependencyKind::Development
616 {
617 continue;
618 }
619
620 crates.append(&mut collect_tree(
621 metadata,
622 resolve,
623 resolve.get(&dep.pkg).unwrap(),
624 config,
625 false,
626 )?)
627 }
628
629 Ok(crates)
630}
631
632#[cfg(all(test, feature = "macros"))]
633mod tests {
634 use cargo_platform::Cfg;
635 use syn::parse_quote;
636
637 use super::{CollectConfig, CollectPlatform};
638
639 macro_rules! should_panic_multiple {
640 ($($name:ident => $body:block),* $(,)?) => {
641 $(
642 #[test]
643 #[should_panic]
644 fn $name() $body
645 )*
646 }
647 }
648
649 #[test]
650 fn collect_config_parse_empty() {
651 let config: CollectConfig = parse_quote!();
652 assert_eq!(config, CollectConfig::default());
653 }
654
655 #[test]
656 fn collect_config_parse_full() {
657 let config: CollectConfig = parse_quote!(build, dev);
658 assert_eq!(
659 config,
660 CollectConfig {
661 build: true,
662 dev: true,
663 ..Default::default()
664 }
665 );
666 }
667
668 #[test]
669 fn collect_config_parse_args() {
670 let config: CollectConfig = parse_quote!(build = true, dev = true);
671 assert_eq!(
672 config,
673 CollectConfig {
674 build: true,
675 dev: true,
676 ..Default::default()
677 }
678 );
679
680 let config: CollectConfig = parse_quote!(build = false, dev = false);
681 assert_eq!(
682 config,
683 CollectConfig {
684 build: false,
685 dev: false,
686 ..Default::default()
687 }
688 );
689 }
690
691 #[test]
692 fn collect_config_parse_single_keyword() {
693 let config: CollectConfig = parse_quote!(build);
694 assert_eq!(
695 config,
696 CollectConfig {
697 build: true,
698 ..Default::default()
699 }
700 );
701
702 let config: CollectConfig = parse_quote!(dev);
703 assert_eq!(
704 config,
705 CollectConfig {
706 dev: true,
707 ..Default::default()
708 }
709 );
710 }
711
712 #[test]
713 fn collect_config_parse_platform_any() {
714 let config: CollectConfig = parse_quote!(platform(any));
715 assert_eq!(
716 config,
717 CollectConfig {
718 platform: CollectPlatform::Any,
719 ..Default::default()
720 }
721 );
722 }
723
724 #[test]
725 fn collect_config_parse_platform_static_target() {
726 let config: CollectConfig =
727 parse_quote!(platform(static(target = "x86_64-linux-gnu", cfg())));
728 assert_eq!(
729 config,
730 CollectConfig {
731 platform: CollectPlatform::Static {
732 target: "x86_64-linux-gnu".to_string(),
733 cfg: vec![]
734 },
735 ..Default::default()
736 }
737 );
738 }
739
740 #[test]
741 fn collect_config_parse_platform_static_cfg_name() {
742 let config: CollectConfig =
743 parse_quote!(platform(static(target = "aarch64-apple-darwin", cfg(foo))));
744 assert_eq!(
745 config,
746 CollectConfig {
747 platform: CollectPlatform::Static {
748 target: "aarch64-apple-darwin".to_string(),
749 cfg: vec![Cfg::Name("foo".to_string())],
750 },
751 ..Default::default()
752 }
753 );
754 }
755
756 #[test]
757 fn collect_config_parse_platform_static_cfg_keypair() {
758 let config: CollectConfig =
759 parse_quote!(platform(static(target = "x86_64-pc-windows-gnu", cfg(foo = "bar"))));
760 assert_eq!(
761 config,
762 CollectConfig {
763 platform: CollectPlatform::Static {
764 target: "x86_64-pc-windows-gnu".to_string(),
765 cfg: vec![Cfg::KeyPair("foo".to_string(), "bar".to_string())],
766 },
767 ..Default::default()
768 }
769 );
770 }
771
772 #[test]
773 fn collect_config_parse_platform_static_cfg_multiple() {
774 let config: CollectConfig = parse_quote!(platform(static(target = "wasm32-unknown-unknown", cfg(foo = "bar", baz))));
775 assert_eq!(
776 config,
777 CollectConfig {
778 platform: CollectPlatform::Static {
779 target: "wasm32-unknown-unknown".to_string(),
780 cfg: vec![
781 Cfg::KeyPair("foo".to_string(), "bar".to_string()),
782 Cfg::Name("baz".to_string())
783 ],
784 },
785 ..Default::default()
786 }
787 );
788 }
789
790 should_panic_multiple! {
791 collect_config_macro_parse_invalid1 => { let _: CollectConfig = parse_quote!(foo); },
792
793 collect_config_macro_parse_invalid2 => { let _: CollectConfig = parse_quote!(dev = 1); },
794 collect_config_macro_parse_invalid3 => { let _: CollectConfig = parse_quote!(build = "foo"); },
795
796 collect_config_macro_parse_invalid4 => { let _: CollectConfig = parse_quote!(platform()); },
797 collect_config_macro_parse_invalid5 => { let _: CollectConfig = parse_quote!(platform(xy)); },
798
799 collect_config_macro_parse_invalid6 => { let _: CollectConfig = parse_quote!(platform(static())); },
800 collect_config_macro_parse_invalid7 => { let _: CollectConfig = parse_quote!(platform(static(target = "x86_64-unknown-linux-gnu"))); },
801 collect_config_macro_parse_invalid8 => { let _: CollectConfig = parse_quote!(platform(static(cfg(unix)))); },
802 }
803}