license_fetcher/
lib.rs

1// Copyright Adam McKellar 2024, 2025
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
7//! Fetch licenses of dependencies at build time and embed them into your program.
8//!
9//! `license-fetcher` is a crate for fetching actual license texts from the cargo source directory for
10//! crates that are compiled with your project. It does this in the build step
11//! in a build script. This means that the heavy dependencies of `license-fetcher`
12//! aren't your dependencies!
13//!
14//! ## Example
15//!
16//! Import `license-fetcher` as a normal AND as a build dependency:
17//! ```sh
18//! cargo add --build --features build license-fetcher
19//! cargo add license-fetcher
20//! ```
21//!
22//!
23//! `src/main.rs`
24//!
25//! ```no_run
26//! use license_fetcher::read_package_list_from_out_dir;
27//! fn main() {
28//!     let package_list = read_package_list_from_out_dir!().unwrap();
29//! }
30//! ```
31//!
32//!
33//! `build.rs`
34//!
35//! ```
36//! use license_fetcher::build::config::{ConfigBuilder, Config};
37//! use license_fetcher::build::package_list_with_licenses;
38//! use license_fetcher::PackageList;
39//!
40//! fn main() {
41//!     // Config with environment variables set by cargo, to fetch licenses at build time.
42//!     let config: Config = ConfigBuilder::from_build_env()
43//!         .build()
44//!         .expect("Failed to build configuration.");
45//!
46//!     let packages: PackageList = package_list_with_licenses(config)
47//!                                     .expect("Failed to fetch metadata or licenses.");
48//!
49//!     // Write packages to out dir to be embedded.
50//!     packages.write_package_list_to_out_dir().expect("Failed to write package list.");
51//!
52//!     // Rerun only if one of the following files changed:
53//!     println!("cargo::rerun-if-changed=build.rs");
54//!     println!("cargo::rerun-if-changed=Cargo.lock");
55//!     println!("cargo::rerun-if-changed=Cargo.toml");
56//! }
57//! ```
58//!
59//! For a more advanced example visit the [`build` module documentation](crate::build).
60//!
61//! ## Adding Packages that are not Crates
62//!
63//! Sometimes we have dependencies that are not crates. For these dependencies `license-fetcher` cannot
64//! automatically generate information. These dependencies can be added manually:
65//!
66//! ```
67//! use std::fs::read_to_string;
68//! use std::concat;
69//!
70//! use license_fetcher::build::config::{ConfigBuilder, Config};
71//! use license_fetcher::build::metadata::package_list;
72//! use license_fetcher::{PackageList, Package, package};
73//!
74//! fn main() {
75//!     // Config with environment variables set by cargo, to fetch licenses at build time.
76//!     let config: Config = ConfigBuilder::from_build_env()
77//!         .build()
78//!         .expect("Failed to build configuration.");
79//!
80//!     // `packages` does not hold any licenses!
81//!     let mut packages: PackageList = package_list(&config.metadata_config)
82//!                                                 .expect("Failed to fetch metadata.");
83//!
84//!     packages.push(package! {
85//!         name: "other dependency".to_owned(),
86//!         version: "0.1.0".to_owned(),
87//!         authors: vec!["Me".to_owned()],
88//!         description: Some("A dependency that is not a rust crate.".to_owned()),
89//!         homepage: None,
90//!         repository: None,
91//!         license_identifier: None,
92//!         license_text: Some(
93//!             read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/LICENSE"))
94//!             .expect("Failed reading license of other dependency")
95//!         )
96//!     });
97//!
98//!     // Write packages to out dir to be embedded.
99//!     packages.write_package_list_to_out_dir().expect("Failed to write package list.");
100//!
101//!     // Rerun only if one of the following files changed:
102//!     println!("cargo::rerun-if-changed=build.rs");
103//!     println!("cargo::rerun-if-changed=Cargo.lock");
104//!     println!("cargo::rerun-if-changed=Cargo.toml");
105//! }
106//! ```
107//!
108
109#![cfg_attr(docsrs, feature(doc_auto_cfg))]
110
111use std::cmp::Ordering;
112use std::default::Default;
113use std::fmt;
114use std::ops::{Deref, DerefMut};
115
116use bincode::{Decode, Encode};
117
118use miniz_oxide::inflate::decompress_to_vec;
119
120/// Wrapper around `bincode` and `miniz_oxide` errors during unpacking of a serialized and compressed [PackageList].
121pub mod error;
122use error::UnpackError;
123
124/// Functions for fetching metadata and licenses.
125#[cfg(feature = "build")]
126pub mod build;
127
128/// Information regarding a crate / package.
129///
130/// This struct holds information like package name, authors and of course license text.
131#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
132#[cfg_attr(feature = "build", derive(serde::Serialize))]
133pub struct Package {
134    pub name: String,
135    pub version: String,
136    pub authors: Vec<String>,
137    pub description: Option<String>,
138    pub homepage: Option<String>,
139    pub repository: Option<String>,
140    pub license_identifier: Option<String>,
141    pub license_text: Option<String>,
142    #[doc(hidden)]
143    pub restored_from_cache: bool,
144    #[doc(hidden)]
145    pub is_root_pkg: bool,
146    #[doc(hidden)]
147    pub name_version: String,
148}
149
150// TODO: Is there an alternative?
151/// Construct a [Package].
152#[macro_export]
153macro_rules! package {
154    (
155        name: $name:expr,
156        version: $version:expr,
157        authors: $authors:expr,
158        description: $description:expr,
159        homepage: $homepage:expr,
160        repository: $repository:expr,
161        license_identifier: $license_identifier:expr,
162        license_text: $license_text:expr $(,)?
163    ) => {
164        $crate::Package {
165            name: $name.clone(),
166            version: $version.clone(),
167            authors: $authors,
168            description: $description,
169            homepage: $homepage,
170            repository: $repository,
171            license_identifier: $license_identifier,
172            license_text: $license_text,
173            restored_from_cache: false,
174            is_root_pkg: false,
175            name_version: format!("{}-{}", $name, $version),
176        }
177    };
178}
179
180impl Package {
181    fn fmt_package(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        const SEPARATOR_WIDTH: usize = 80;
183        let separator: String = "=".repeat(SEPARATOR_WIDTH);
184        let separator_light: String = "-".repeat(SEPARATOR_WIDTH);
185
186        writeln!(f, "Package:     {} {}", self.name, self.version)?;
187        if let Some(description) = &self.description {
188            writeln!(f, "Description: {}", description)?;
189        }
190        if !self.authors.is_empty() {
191            writeln!(
192                f,
193                "Authors:     - {}",
194                self.authors.get(0).unwrap_or(&"".to_owned())
195            )?;
196            for author in self.authors.iter().skip(1) {
197                writeln!(f, "             - {}", author)?;
198            }
199        }
200        if let Some(homepage) = &self.homepage {
201            writeln!(f, "Homepage:    {}", homepage)?;
202        }
203        if let Some(repository) = &self.repository {
204            writeln!(f, "Repository:  {}", repository)?;
205        }
206        if let Some(license_identifier) = &self.license_identifier {
207            writeln!(f, "SPDX Ident:  {}", license_identifier)?;
208        }
209
210        if let Some(license_text) = &self.license_text {
211            writeln!(f, "\n{}\n{}", separator_light, license_text)?;
212        }
213
214        writeln!(f, "\n{}\n", separator)?;
215
216        Ok(())
217    }
218}
219
220impl Ord for Package {
221    fn cmp(&self, other: &Self) -> Ordering {
222        if self.name < other.name {
223            Ordering::Less
224        } else if self.name > other.name {
225            Ordering::Greater
226        } else {
227            if self.version < other.version {
228                Ordering::Less
229            } else if self.version > other.version {
230                Ordering::Greater
231            } else {
232                Ordering::Equal
233            }
234        }
235    }
236}
237
238impl PartialOrd for Package {
239    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
240        Some(self.cmp(other))
241    }
242}
243
244impl fmt::Display for Package {
245    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246        const SEPARATOR_WIDTH: usize = 80;
247        let separator: String = "=".repeat(SEPARATOR_WIDTH);
248
249        writeln!(f, "{}\n", separator)?;
250
251        self.fmt_package(f)
252    }
253}
254
255/// Holds information of all crates and licenses used for a release build.
256#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
257#[cfg_attr(feature = "build", derive(serde::Serialize))]
258pub struct PackageList(pub Vec<Package>);
259
260impl From<Vec<Package>> for PackageList {
261    fn from(value: Vec<Package>) -> Self {
262        Self(value)
263    }
264}
265
266impl Default for PackageList {
267    fn default() -> Self {
268        PackageList(vec![])
269    }
270}
271
272impl Deref for PackageList {
273    type Target = Vec<Package>;
274
275    fn deref(&self) -> &Self::Target {
276        &self.0
277    }
278}
279
280impl DerefMut for PackageList {
281    fn deref_mut(&mut self) -> &mut Self::Target {
282        &mut self.0
283    }
284}
285
286impl fmt::Display for PackageList {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        const SEPARATOR_WIDTH: usize = 80;
289        let separator: String = "=".repeat(SEPARATOR_WIDTH);
290
291        writeln!(f, "{}\n", separator)?;
292
293        for package in self.iter() {
294            package.fmt_package(f)?;
295        }
296
297        Ok(())
298    }
299}
300
301impl PackageList {
302    /// Decompresses and deserializes the crate and license information.
303    ///
304    /// ## Example
305    /// If you intend to embed license information:
306    /// ```no_run
307    /// use license_fetcher::PackageList;
308    /// fn main() {
309    ///     let package_list = PackageList::from_encoded(std::include_bytes!(std::concat!(
310    ///        env!("OUT_DIR"),
311    ///        "/LICENSE-3RD-PARTY.bincode.deflate"
312    ///     ))).unwrap();
313    /// }
314    /// ```
315    pub fn from_encoded(bytes: &[u8]) -> Result<PackageList, UnpackError> {
316        if bytes.is_empty() {
317            return Err(UnpackError::Empty);
318        }
319
320        let uncompressed_bytes = decompress_to_vec(bytes)?;
321
322        let (package_list, _) =
323            bincode::decode_from_slice(&uncompressed_bytes, bincode::config::standard())?;
324
325        Ok(package_list)
326    }
327}
328
329/// Embed and decode a [PackageList], which you expect to be in `$OUT_DIR/LICENSE-3RD-PARTY.bincode.deflate`, via [PackageList::from_encoded].
330///
331/// This macro is only meant to be used in conjunction with [PackageList::write_package_list_to_out_dir].
332///
333/// If you get an error that `OUT_DIR` is not set, then please compile your project once and restart rust analyzer.
334///
335/// ## Example
336/// ```no_run
337/// use license_fetcher::read_package_list_from_out_dir;
338/// fn main() {
339///     let package_list = read_package_list_from_out_dir!().expect("Failed to decode the embedded package list.");
340/// }
341/// ```
342#[macro_export]
343macro_rules! read_package_list_from_out_dir {
344    () => {
345        license_fetcher::PackageList::from_encoded(std::include_bytes!(std::concat!(
346            env!("OUT_DIR"),
347            "/LICENSE-3RD-PARTY.bincode.deflate"
348        )))
349    };
350}
351
352#[cfg(test)]
353mod test {
354    use std::fs::read_to_string;
355
356    use super::*;
357
358    #[test]
359    fn test_package_macro() {
360        let _pkg: Package = package! {
361            name: "dependency".to_owned(),
362            version: "0.1.0".to_owned(),
363            authors: vec!["Me".to_owned()],
364            description: Some("A dependency that is not a rust crate.".to_owned()),
365            homepage: None,
366            repository: None,
367            license_identifier: None,
368            license_text: Some(
369                read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/LICENSE"))
370                    .expect("Failed reading license of other dependency")
371            )
372        };
373    }
374}