cargo_about/
generate.rs

1use std::collections::BTreeMap;
2
3use crate::licenses::{self, LicenseInfo};
4use codespan_reporting::term;
5use krates::Utf8PathBuf as PathBuf;
6use krates::cm::Package;
7use serde::{Serialize, Serializer};
8
9#[derive(Clone, Serialize)]
10pub struct UsedBy<'a> {
11    #[serde(rename = "crate")]
12    pub krate: &'a krates::cm::Package,
13    pub path: Option<PathBuf>,
14}
15
16#[derive(Clone, Serialize)]
17pub struct License<'a> {
18    /// The full name of the license
19    pub name: String,
20    /// The SPDX short identifier for the license
21    pub id: String,
22    /// True if this is the first license of its kind in the flat array
23    pub first_of_kind: bool,
24    /// The full license text
25    pub text: String,
26    /// The path where the license text was sourced from
27    pub source_path: Option<PathBuf>,
28    /// The list of crates this license was applied to
29    pub used_by: Vec<UsedBy<'a>>,
30}
31
32#[derive(Serialize)]
33pub struct LicenseSet {
34    /// Number of packages that use this license.
35    pub count: usize,
36    /// This license's human-readable name (e.g. "Apache License 2.0").
37    pub name: String,
38    /// This license's SPDX identifier (e.g. "Apache-2.0").
39    pub id: String,
40    /// Indices (in [`Input::crates`]) of the crates that use this license.
41    pub indices: Vec<usize>,
42    /// This license's text. Currently taken from the first crate that uses the license.
43    pub text: String,
44}
45
46fn serialize_as_string<T: std::fmt::Display, S: Serializer>(
47    value: &T,
48    serializer: S,
49) -> Result<S::Ok, S::Error> {
50    serializer.collect_str(value)
51}
52
53#[derive(Serialize)]
54pub struct PackageLicense<'a> {
55    /// The package itself.
56    pub package: &'a Package,
57    /// The package's license: either a SPDX license identifier, "Unknown", or "Ignore".
58    #[serde(serialize_with = "serialize_as_string")]
59    pub license: &'a LicenseInfo,
60}
61
62#[derive(Serialize)]
63pub struct LicenseList<'a> {
64    /// All license types (e.g. Apache, MIT) and the indices (in [`Input::crates`]) of the crates that use them.
65    pub overview: Vec<LicenseSet>,
66    /// All unique license *texts* (which may differ by e.g. copyright string, even among licenses of the same type),
67    /// and the crates that use them.
68    pub licenses: Vec<License<'a>>,
69    /// All input packages/crates.
70    pub crates: Vec<PackageLicense<'a>>,
71}
72
73/// Generate a list of all licenses from a list of crates gathered from [`licenses::Gatherer`] and a list of resolved
74/// licenses and files from [`licenses::resolution::resolve`].
75pub fn generate<'kl>(
76    nfos: &'kl [licenses::KrateLicense<'kl>],
77    resolved: &[Option<licenses::Resolved>],
78    files: &licenses::resolution::Files,
79    stream: Option<term::termcolor::StandardStream>,
80) -> anyhow::Result<LicenseList<'kl>> {
81    use licenses::resolution::Severity;
82
83    let mut num_errors = 0;
84
85    let term_and_diag = stream.map(|s| (s, term::Config::default()));
86
87    let mut licenses = {
88        let mut licenses = BTreeMap::new();
89        for (krate_license, resolved) in nfos
90            .iter()
91            .zip(resolved.iter())
92            .filter_map(|(kl, res)| res.as_ref().map(|res| (kl, res)))
93        {
94            match &term_and_diag {
95                Some((stream, diag_cfg)) if !resolved.diagnostics.is_empty() => {
96                    let mut streaml = stream.lock();
97
98                    for diag in &resolved.diagnostics {
99                        if diag.severity >= Severity::Error {
100                            num_errors += 1;
101                        }
102
103                        term::emit_to_io_write(&mut streaml, diag_cfg, files, diag)?;
104                    }
105                }
106                _ => {}
107            }
108
109            let license_iter = resolved.licenses.iter().flat_map(|license| {
110                let mut license_texts = Vec::new();
111                match license.license {
112                    spdx::LicenseItem::Spdx { id, .. } => {
113                        // Attempt to retrieve the actual license file from the crate, note that in some cases
114                        // _sigh_ there are actually multiple license texts for the same license with different
115                        // copyright holders/authors/attribution so we can't just return 1
116                        license_texts.extend(krate_license
117                            .license_files
118                            .iter()
119                            .filter_map(|lf| {
120                                // Check if this is the actual license file we want
121                                if !lf
122                                    .license_expr
123                                    .evaluate(|ereq| ereq.license.id() == Some(id))
124                                {
125                                    return None;
126                                }
127
128                                match &lf.kind {
129                                    licenses::LicenseFileKind::Text(text)
130                                    | licenses::LicenseFileKind::AddendumText(text, _) => {
131                                        let license = License {
132                                            name: id.full_name.to_owned(),
133                                            id: id.name.to_owned(),
134                                            text: text.clone(),
135                                            source_path: Some(lf.path.clone()),
136                                            used_by: Vec::new(),
137                                            first_of_kind: false,
138                                        };
139                                        Some(license)
140                                    }
141                                    licenses::LicenseFileKind::Header => None,
142                                }
143                            }));
144
145                        if license_texts.is_empty() {
146                            log::debug!(
147                                "unable to find text for license '{license}' for crate '{}', falling back to canonical text",
148                                krate_license.krate
149                            );
150
151                            // If the crate doesn't have the actual license file,
152                            // fallback to the canonical license text and emit a warning
153                            license_texts.push(License {
154                                name: id.full_name.to_owned(),
155                                id: id.name.to_owned(),
156                                text: id.text().to_owned(),
157                                source_path: None,
158                                used_by: Vec::new(),
159                                first_of_kind: false,
160                            });
161                        }
162                    }
163                    spdx::LicenseItem::Other { .. } => {
164                        log::warn!(
165                            "{license} has no license file for crate '{}'",
166                            krate_license.krate
167                        );
168                    }
169                }
170
171                license_texts
172            });
173
174            for license in license_iter {
175                let entry = licenses
176                    .entry(license.name.clone())
177                    .or_insert_with(BTreeMap::new);
178
179                let lic = entry.entry(license.text.clone()).or_insert_with(|| license);
180                lic.used_by.push(UsedBy {
181                    krate: krate_license.krate,
182                    path: None,
183                });
184            }
185        }
186
187        let mut licenses: Vec<_> = licenses
188            .into_iter()
189            .flat_map(|(_, v)| v.into_values())
190            .collect();
191
192        // Sort the krates that use a license lexicographically
193        for lic in &mut licenses {
194            lic.used_by.sort_by(|a, b| a.krate.id.cmp(&b.krate.id));
195        }
196
197        licenses.sort_by(|a, b| a.id.cmp(&b.id));
198        licenses
199    };
200
201    if num_errors > 0 {
202        anyhow::bail!(
203            "encountered {num_errors} errors resolving licenses, unable to generate output"
204        );
205    }
206
207    let mut overview: BTreeMap<&str, LicenseSet> = BTreeMap::new();
208
209    for (ndx, lic) in licenses.iter_mut().enumerate() {
210        let ls = overview.entry(&lic.id).or_insert_with(|| {
211            lic.first_of_kind = true;
212            LicenseSet {
213                count: 0,
214                name: lic.name.clone(),
215                id: lic.id.clone(),
216                indices: Vec::with_capacity(10),
217                text: lic.text.clone(),
218            }
219        });
220        ls.indices.push(ndx);
221        ls.count += lic.used_by.len();
222    }
223
224    let mut overview = overview.into_values().collect::<Vec<_>>();
225    // Show the most used licenses first
226    overview.sort_by(|a, b| b.count.cmp(&a.count));
227
228    let crates = nfos
229        .iter()
230        .filter(|nfo| !matches!(nfo.lic_info, LicenseInfo::Ignore))
231        .map(|nfo| PackageLicense {
232            package: &nfo.krate.0,
233            license: &nfo.lic_info,
234        })
235        .collect();
236    Ok(LicenseList {
237        overview,
238        licenses,
239        crates,
240    })
241}