diff --git a/Cargo.toml b/Cargo.toml index 1adf3b5..7f2b07d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "license-fetcher" description = "Fetch licenses of dependencies at build time and embed them into your program." authors = ["Adam McKellar "] -version = "0.5.0" +version = "0.6.0" edition = "2021" readme = "README.md" repository = "https://github.com/WyvernIXTL/license-fetcher" @@ -14,7 +14,7 @@ keywords = ["license", "embed", "fetch", "about", "find"] bincode = "=2.0.0-rc.3" directories = {version = "5.0.1", optional = true} log = { version = "0.4.22", optional = true } -miniz_oxide = { version = "0.8.0", optional = true } +miniz_oxide = { version = "0.8.0", optional = true, features = ["std"]} once_cell = { version = "1.19.0", optional = true } regex = { version = "1.10.6", optional = true } serde = { version = "1.0.210", features = ["derive"], optional = true } diff --git a/README.md b/README.md index 484b351..09a54e0 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Add following content to your `main.rs`: use license_fetcher::get_package_list_macro; fn main() { - let packages = get_package_list_macro!(); + let packages = get_package_list_macro!().unwrap(); println!("{}", packages); } ``` @@ -72,7 +72,7 @@ fn main() { + Also retrieves licenses in the build step and loads them into the program. #### Cons -- Does not fetch licenses from loacal source files. +- Does not fetch licenses from local source files. - Very slow. - Does not compress licenses. @@ -84,7 +84,7 @@ fn main() { #### Cons - Is not a library to access said data but rather a command line tool. -- Does not fetch licenses from loacal source files. +- Does not fetch licenses from local source files. ## Screenshots diff --git a/src/build_script/mod.rs b/src/build_script/mod.rs index d30ee9d..0394c24 100644 --- a/src/build_script/mod.rs +++ b/src/build_script/mod.rs @@ -1,31 +1,35 @@ -// Copyright Adam McKellar 2024 +// Copyright Adam McKellar 2024, 2025 // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE or copy at // https://www.boost.org/LICENSE_1_0.txt) -use std::env::{var_os, var}; +use std::collections::BTreeSet; +use std::env::{var, var_os}; +use std::ffi::OsString; use std::fs::write; +use std::path::PathBuf; use std::process::Command; -use std::collections::BTreeSet; use std::time::Instant; -use std::path::PathBuf; #[cfg(feature = "compress")] use miniz_oxide::deflate::compress_to_vec; -use serde_json::from_slice; use log::info; -use simplelog::{TermLogger, LevelFilter, Config, TerminalMode, ColorChoice}; +use serde_json::from_slice; +use simplelog::{ColorChoice, Config, LevelFilter, TermLogger, TerminalMode}; -mod metadata; mod cargo_source; +mod metadata; use crate::*; use build_script::metadata::*; -use cargo_source::{licenses_text_from_cargo_src_folder, license_text_from_folder}; +use cargo_source::{license_text_from_folder, licenses_text_from_cargo_src_folder}; - -fn walk_dependencies<'a>(used_dependencies: &mut BTreeSet<&'a String>, dependencies: &'a Vec, root: &String) { +fn walk_dependencies<'a>( + used_dependencies: &mut BTreeSet<&'a String>, + dependencies: &'a Vec, + root: &String, +) { let package = match dependencies.iter().find(|&dep| dep.id == *root) { Some(pack) => pack, None => return, @@ -38,27 +42,36 @@ fn walk_dependencies<'a>(used_dependencies: &mut BTreeSet<&'a String>, dependenc } } -fn generate_package_list() -> PackageList { - let cargo_path = var_os("CARGO").unwrap(); - let manifest_path = var_os("CARGO_MANIFEST_DIR").unwrap(); +fn generate_package_list(cargo_path: Option, manifest_dir_path: OsString) -> PackageList { + let cargo_path = cargo_path.unwrap_or_else(|| OsString::from("cargo")); let mut metadata_output = Command::new(&cargo_path) - .current_dir(&manifest_path) - .args(["metadata", "--format-version", "1", "--frozen", "--color", "never"]) - .output() - .unwrap(); + .current_dir(&manifest_dir_path) + .args([ + "metadata", + "--format-version", + "1", + "--frozen", + "--color", + "never", + ]) + .output() + .unwrap(); #[cfg(not(feature = "frozen"))] if !metadata_output.status.success() { metadata_output = Command::new(&cargo_path) - .current_dir(&manifest_path) - .args(["metadata", "--format-version", "1", "--color", "never"]) - .output() - .unwrap(); + .current_dir(&manifest_dir_path) + .args(["metadata", "--format-version", "1", "--color", "never"]) + .output() + .unwrap(); } if !metadata_output.status.success() { - panic!("Failed executing cargo metadata with:\n{}", String::from_utf8_lossy(&metadata_output.stderr)); + panic!( + "Failed executing cargo metadata with:\n{}", + String::from_utf8_lossy(&metadata_output.stderr) + ); } let metadata_parsed: Metadata = from_slice(&metadata_output.stdout).unwrap(); @@ -71,7 +84,6 @@ fn generate_package_list() -> PackageList { walk_dependencies(&mut used_packages, &dependencies, &package_id); - // Add dependencies: let mut package_list = vec![]; @@ -89,24 +101,56 @@ fn generate_package_list() -> PackageList { repository: package.repository, }); } - } PackageList(package_list) } +/// Generates a package list with package name, authors and license text. Uses supplied parameters for cargo path and manifest path. +/// +/// Thist function is not as usefull as [generate_package_list_with_licenses()] for build scripts. +/// [generate_package_list_with_licenses()] fetches `cargo_path` and `manifest_dir_path` automatically. +/// This function does not. +/// The main use is for other rust programs to fetch the metadata outside of a build script. +/// +/// ### Arguments +/// +/// * **cargo_path - Absolute path to cargo executable. If omited tries to fetch the path from `PATH`. +/// * **manifest_dir_path** - Relative or absolut path to manifest dir. +/// * **this_package_name** - Name of the package. `cargo metadata` does not disclode the name, but it is needed for parsing the used licenses. +pub fn generate_package_list_with_licenses_without_env_calls( + cargo_path: Option, + manifest_dir_path: OsString, + this_package_name: String, +) -> PackageList { + let mut package_list = generate_package_list(cargo_path, manifest_dir_path.clone()); + + licenses_text_from_cargo_src_folder(&mut package_list); + + info!("Fetching license for: {}", &this_package_name); + let this_package_index = package_list + .iter() + .enumerate() + .filter(|(_, p)| p.name == this_package_name) + .map(|(i, _)| i) + .next() + .unwrap(); + package_list[this_package_index].license_text = + license_text_from_folder(&PathBuf::from(manifest_dir_path)); + package_list.swap(this_package_index, 0); -/// Generates a package list with package name, authors and license text. -/// + package_list +} + +/// Generates a package list with package name, authors and license text. Uses env variables supplied by cargo during build. +/// /// This function: /// 1. Calls `cargo tree -e normal --frozen`. *(After error tries again online if not `frozen` feature is set.)* /// 2. Calls `cargo metadata --frozen`. *(After error tries again online if not `frozen` feature is set.)* /// 3. Takes the packages gotten from `cargo tree` with the metadata of `cargo metadata`. -/// 4. Fetches the licenses from github with the `repository` link if it includes `github` in name. -/// 5. Serializes, copmresses and writes said package list to `OUT_DIR/LICENSE-3RD-PARTY.bincode` file. -/// +/// /// Needs the feature `build` and is only meant to be used in build scripts. -/// +/// /// # Example /// In `build.rs`: /// ```no_run @@ -120,43 +164,50 @@ fn generate_package_list() -> PackageList { /// } /// ``` pub fn generate_package_list_with_licenses() -> PackageList { - TermLogger::init(LevelFilter::Trace, Config::default(), TerminalMode::Stderr, ColorChoice::Auto).unwrap(); - - let mut package_list = generate_package_list(); - - licenses_text_from_cargo_src_folder(&mut package_list); + TermLogger::init( + LevelFilter::Trace, + Config::default(), + TerminalMode::Stderr, + ColorChoice::Auto, + ) + .unwrap(); + let cargo_path = var_os("CARGO").unwrap(); + let manifest_dir_path = var_os("CARGO_MANIFEST_DIR").unwrap(); let this_package_name = var("CARGO_PKG_NAME").unwrap(); - info!("Fetching license for: {}", &this_package_name); - let this_package_path = var("CARGO_MANIFEST_DIR").unwrap(); - let this_package_index = package_list.iter().enumerate().filter(|(_, p)| p.name == this_package_name).map(|(i, _)| i).next().unwrap(); - package_list[this_package_index].license_text = license_text_from_folder(&PathBuf::from(this_package_path)); - package_list.swap(this_package_index, 0); - package_list + generate_package_list_with_licenses_without_env_calls( + Some(cargo_path), + manifest_dir_path, + this_package_name, + ) } impl PackageList { /// Writes the [PackageList] to the file and folder where they can be embedded into the program at compile time. - /// + /// /// Copmresses and writes the PackageList into the `OUT_DIR` with file name `LICENSE-3RD-PARTY.bincode`. pub fn write(self) { let mut path = var_os("OUT_DIR").unwrap(); path.push("/LICENSE-3RD-PARTY.bincode"); - + let data = bincode::encode_to_vec(self, config::standard()).unwrap(); - + info!("License data size: {} Bytes", data.len()); let instant_before_compression = Instant::now(); - + #[cfg(feature = "compress")] let compressed_data = compress_to_vec(&data, 10); - + #[cfg(not(feature = "compress"))] let compressed_data = data; - - info!("Compressed data size: {} Bytes in {}ms", compressed_data.len(), instant_before_compression.elapsed().as_millis()); - + + info!( + "Compressed data size: {} Bytes in {}ms", + compressed_data.len(), + instant_before_compression.elapsed().as_millis() + ); + info!("Writing to file: {:?}", &path); write(path, compressed_data).unwrap(); } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..b7f30cc --- /dev/null +++ b/src/error.rs @@ -0,0 +1,48 @@ +// Copyright Adam McKellar 2025 +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +use std::error::Error; +use std::fmt; + +/// Error union representing errors that might occur during unpacking of license data. +#[derive(Debug)] +pub enum UnpackError { + #[cfg(feature = "compress")] + DecompressError(miniz_oxide::inflate::DecompressError), + DecodeError(bincode::error::DecodeError), +} + +#[cfg(feature = "compress")] +impl From for UnpackError { + fn from(value: miniz_oxide::inflate::DecompressError) -> Self { + Self::DecompressError(value) + } +} + +impl From for UnpackError { + fn from(value: bincode::error::DecodeError) -> Self { + Self::DecodeError(value) + } +} + +impl fmt::Display for UnpackError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + #[cfg(feature = "compress")] + Self::DecompressError(e) => writeln!(f, "{}", e), + Self::DecodeError(e) => writeln!(f, "{}", e), + } + } +} + +impl Error for UnpackError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(match self { + #[cfg(feature = "compress")] + Self::DecompressError(e) => e, + Self::DecodeError(e) => e, + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index 97e73c1..10c59f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,31 +1,30 @@ -// Copyright Adam McKellar 2024 +// Copyright Adam McKellar 2024, 2025 // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE or copy at // https://www.boost.org/LICENSE_1_0.txt) - //! Fetch licenses of dependencies at build time and embed them into your program. -//! +//! //! `license-fetcher` is a crate for fetching actual license texts from the cargo source directory for //! crates that are compiled with your project. It does this in the build step //! in a build script. This means that the heavy dependencies of `license-fetcher` //! aren't your dependencies! -//! +//! //! ## Example //! Don't forget to import `license-fetcher` as a normal AND as a build dependency! //! ```sh //! cargo add --build --features build license-fetcher //! cargo add license-fetcher //! ``` -//! +//! //! ### `src/main.rs` -//! +//! //! ```ignore //! use license_fetcher::get_package_list_macro; //! fn main() { -//! let package_list = get_package_list_macro!(); +//! let package_list = get_package_list_macro!().unwrap(); //! } -//! +//! //! ``` //! ### `build.rs` //! ```ignore @@ -38,23 +37,23 @@ //! println!("cargo::rerun-if-changed=Cargo.toml"); //! } //! ``` -//! +//! //! ## Adding Packages that are not Crates -//! +//! //! Sometimes we have dependencies that are not crates. For these dependencies `license-fetcher` cannot //! automatically generate information. These dependencies can be added manually: //! ```ignore //! use std::fs::read_to_string; //! use std::concat; -//! +//! //! use license_fetcher::{ //! Package, //! build_script::generate_package_list_with_licenses //! }; -//! +//! //! fn main() { //! let mut packages = generate_package_list_with_licenses(); -//! +//! //! packages.push(Package { //! name: "other dependency".to_owned(), //! version: "0.1.0".to_owned(), @@ -68,44 +67,44 @@ //! .expect("Failed reading license of other dependency") //! ) //! }); -//! +//! //! packages.write(); -//! +//! //! println!("cargo::rerun-if-changed=build.rs"); //! println!("cargo::rerun-if-changed=Cargo.lock"); //! println!("cargo::rerun-if-changed=Cargo.toml"); //! //! } //! ``` -//! +//! //! ## Feature Flags //! | Feature | Description | //! | ---------- | ----------------------------------------------------------------------- | //! | `compress` | *(default)* Enables compression. | //! | `build` | Used for build script component. | //! | `frozen` | Panics if `Cargo.lock` needs to be updated for `cargo metadata` to run. | -//! - - +//! +use std::default::Default; use std::fmt; -use std::error::Error; use std::ops::{Deref, DerefMut}; -use std::default::Default; use bincode::{config, Decode, Encode}; #[cfg(feature = "compress")] use miniz_oxide::inflate::decompress_to_vec; +pub mod error; +use error::UnpackError; + #[cfg(feature = "build")] pub mod build_script; - /// Information regarding a crate. -/// +/// /// This struct holds information like package name, authors and of course license text. #[derive(Encode, Decode, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "build", derive(serde::Serialize))] pub struct Package { pub name: String, pub version: String, @@ -128,9 +127,13 @@ impl Package { writeln!(f, "Description: {}", description)?; } if !self.authors.is_empty() { - writeln!(f, "Authors: - {}", self.authors.get(0).unwrap_or(&"".to_owned()))?; + writeln!( + f, + "Authors: - {}", + self.authors.get(0).unwrap_or(&"".to_owned()) + )?; for author in self.authors.iter().skip(1) { - writeln!(f, " - {}", author)?; + writeln!(f, " - {}", author)?; } } if let Some(homepage) = &self.homepage { @@ -142,7 +145,7 @@ impl Package { if let Some(license_identifier) = &self.license_identifier { writeln!(f, "SPDX Ident: {}", license_identifier)?; } - + if let Some(license_text) = &self.license_text { writeln!(f, "\n{}\n{}", separator_light, license_text)?; } @@ -164,12 +167,11 @@ impl fmt::Display for Package { } } - /// Holds information of all crates and licenses used for release build. #[derive(Encode, Decode, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "build", derive(serde::Serialize))] pub struct PackageList(pub Vec); - impl Deref for PackageList { type Target = Vec; @@ -200,11 +202,11 @@ impl fmt::Display for PackageList { } /// Decopresses and deserializes the crate and license information. -/// +/// /// Thise function decompresses the input, if `compress` feature was not disabled and /// then deserializes the input. The input should be the embeded license information from /// the build step. -/// +/// /// # Example /// Called from within main program: /// ```no_run @@ -217,19 +219,20 @@ impl fmt::Display for PackageList { /// ).unwrap(); /// } /// ``` -pub fn get_package_list(bytes: &[u8]) -> Result> { +pub fn get_package_list(bytes: &[u8]) -> Result { #[cfg(feature = "compress")] - let uncompressed_bytes = decompress_to_vec(bytes) - .expect("Failed decompressing license data."); + let uncompressed_bytes = decompress_to_vec(bytes).expect("Failed decompressing license data."); #[cfg(not(feature = "compress"))] let uncompressed_bytes = bytes; - let (package_list, _) = bincode::decode_from_slice(&uncompressed_bytes[..], config::standard())?; + let (package_list, _) = + bincode::decode_from_slice(&uncompressed_bytes[..], config::standard())?; + Ok(package_list) } /// Calls [get_package_list] with parameters expected from a call from `main.rs`. -/// +/// /// # Example /// ```no_run /// use license_fetcher::get_package_list_macro; @@ -240,7 +243,9 @@ pub fn get_package_list(bytes: &[u8]) -> Result { - license_fetcher::get_package_list(std::include_bytes!(std::concat!(env!("OUT_DIR"), "/LICENSE-3RD-PARTY.bincode"))).unwrap() + license_fetcher::get_package_list(std::include_bytes!(std::concat!( + env!("OUT_DIR"), + "/LICENSE-3RD-PARTY.bincode" + ))) }; } -