embed_licensing_core/
lib.rs

1// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5use 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/// Error type.
18#[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    /// The crate does not specify either `license` or `license-file` in its manifest.
28    #[error("no license specified for crate {0}")]
29    NoLicense(String),
30    /// The crate’s `license` includes license identifiers which are not standard SPDX identifiers.
31    #[error("non-SPDX license identifier specified for crate {0}")]
32    NonSpdxLicense(String),
33    /// The crate specifies no website.
34    ///
35    /// This means it does not set any of the following manifest keys:
36    ///
37    /// - [`homepage`](https://doc.rust-lang.org/cargo/reference/manifest.html#the-homepage-field)
38    /// - [`repository`](https://doc.rust-lang.org/cargo/reference/manifest.html#the-repository-field)
39    /// - [`documentation`](https://doc.rust-lang.org/cargo/reference/manifest.html#the-documentation-field)
40    #[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/// Licensing information returned by [`collect`].
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct Licensing {
54    /// All dependencies of the crate (including transitive dependencies and the crate itself).
55    pub packages: Vec<Crate>,
56    /// All SPDX licenses used by the crate and its dependencies.
57    ///
58    /// It does not include non-SPDX-licenses.
59    /// Where such custom licenses are used,
60    /// their text is only included as part of the corresponding [`Crate`].
61    pub licenses: Vec<LicenseId>,
62    /// All license exceptions used by the crate and its dependencies.
63    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/// Information about a crate.
113///
114/// The crate can be either the crate from which [`collect`] is called,
115/// or one of its dependencies.
116#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct Crate {
118    /// The name of the crate.
119    pub name: String,
120    /// The version of the crate.
121    pub version: String,
122    /// The authors of the crate.
123    pub authors: Vec<String>,
124    /// The licenses of the crate.
125    ///
126    /// If the
127    /// [`license`](https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields)
128    /// attribute of the manifest is set,
129    /// its content is be passed as an [`spdx::Expression`].
130    /// Otherwise, if the
131    /// [`license-file`](https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields)
132    /// is specified,
133    /// its content is be included as a String.
134    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/// Represents the license of a [`Crate`].
224#[derive(Clone, Debug)]
225#[allow(clippy::large_enum_variant)] // SpdxExpression is much more common than Other
226pub enum CrateLicense {
227    /// The [`Crate`]’s license is specified by a [`spdx::Expression`].
228    SpdxExpression(spdx::Expression),
229    /// The [`Crate`] has a custom license whose contents are included in the argument.
230    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    /// Collect dependencies regardless of their target and cfg restrictions.
273    #[default]
274    Any,
275    /// Only collect dependencies for a specific static platform.
276    Static {
277        /// All dependencies that require a specific target (like `x86_64-pc-windows-gnu`)
278        /// will only be collected when their target matches the given one.
279        target: String,
280        /// Only dependencies which are satisfied by the given cfg options will be collected.
281        cfg: Vec<Cfg>,
282    },
283    /// Only collect dependencies compatible with the current platform.
284    #[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/// Configuration options for the collection process.
390#[derive(Debug, Default, PartialEq)]
391pub struct CollectConfig {
392    /// If set to `true`,
393    /// [`dev-dependencies`](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#development-dependencies)
394    /// are collected
395    ///
396    /// Because dependents never inherit development dependencies,
397    /// transitive development dependencies are never collected,
398    /// even with this option set.
399    pub dev: bool,
400    /// If set to `true`,
401    /// [`build-dependencies`](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#build-dependencies)
402    /// are collected.
403    ///
404    /// Unlike with development dependencies,
405    /// this also applies to transitive build dependencies.
406    pub build: bool,
407    /// Which platform to collect for.
408    ///
409    /// Defaults to [`CollectPlatform::Any`].
410    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
468/// Collect licensing information at runtime.
469///
470/// It uses the defaults of `cargo metadata`,
471/// which is to search for `Cargo.toml` in the current directory and its parent directories.
472///
473/// To specify a manifest path, please use [`collect_from_manifest`].
474pub fn collect(config: CollectConfig) -> Result<Licensing> {
475    collect_internal(None::<PathBuf>, config)
476}
477
478/// Collect licensing information from given manifest path.
479pub 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        // Cargo does not resolve development dependencies for transitive dependencies
598        // (as they are not needed to build the crate).
599        // It is possible to create the following dependency loop:
600        //
601        // ┌──────┐   dependency    ┌──────┐
602        // │      ├────────────────►│      │
603        // │ pkg1 │                 │ pkg2 │
604        // │      │◄────────────────┤      │
605        // └──────┘  dev-dependency └──────┘
606        // (also possible with dev-dependency in both directions)
607        //
608        // It successfully resolves with cargo,
609        // as it will no longer find dependencies once it reaches pkg2 from pkg1.
610        //
611        // The following check ensures that this implementation also stops the recursion in this
612        // case.
613        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}