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}