zng_tp_licenses/
lib.rs

1#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
3//!
4//! Third party license management and collection.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9
10use std::fmt;
11
12use serde::{Deserialize, Serialize};
13use zng_txt::Txt;
14
15/// Represents a license and dependencies that use it.
16#[derive(Serialize, Deserialize, Clone)]
17pub struct LicenseUsed {
18    /// License name and text.
19    pub license: License,
20    /// Project or packages that use this license.
21    pub used_by: Vec<User>,
22}
23impl fmt::Debug for LicenseUsed {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        f.debug_struct("License")
26            .field("license.id", &self.license.id)
27            .field("used_by", &self.used_by)
28            .finish_non_exhaustive()
29    }
30}
31impl LicenseUsed {
32    /// Invert data to be keyed by user.
33    pub fn user_licenses(&self) -> Vec<UserLicense> {
34        self.used_by
35            .iter()
36            .map(|u| UserLicense {
37                user: u.clone(),
38                license: self.license.clone(),
39            })
40            .collect()
41    }
42}
43
44/// Invert data to be keyed by user, also sorts by user name.
45pub fn user_licenses(licenses: &[LicenseUsed]) -> Vec<UserLicense> {
46    let mut r: Vec<_> = licenses.iter().flat_map(|l| l.user_licenses()).collect();
47    r.sort_by(|a, b| a.user.name.cmp(&b.user.name));
48    r
49}
50
51/// Represents a license user with license.
52#[derive(Clone, PartialEq, Eq)]
53pub struct UserLicense {
54    /// License user.
55    pub user: User,
56    /// License used.
57    pub license: License,
58}
59impl fmt::Debug for UserLicense {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.debug_struct("UserLicense")
62            .field("user", &self.user)
63            .field("license.id", &self.license.id)
64            .finish()
65    }
66}
67
68/// Represents a license id, name and text.
69#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, Hash)]
70pub struct License {
71    /// License [SPDX] id.
72    ///
73    /// [SPDX]: https://spdx.org/licenses/
74    pub id: Txt,
75    /// License name.
76    pub name: Txt,
77    /// License text.
78    pub text: Txt,
79}
80
81/// Represents a project or package that uses a license.
82#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug, Clone)]
83pub struct User {
84    /// Project or package name.
85    pub name: Txt,
86    /// Package version.
87    #[serde(default)]
88    pub version: Txt,
89    /// Project or package URL.
90    #[serde(default)]
91    pub url: Txt,
92}
93
94/// Merge `licenses` into `into`.
95///
96/// The licenses and users are not sorted, call [`sort_licenses`] after merging all licenses.
97pub fn merge_licenses(into: &mut Vec<LicenseUsed>, licenses: Vec<LicenseUsed>) {
98    for license in licenses {
99        if let Some(l) = into.iter_mut().find(|l| l.license == license.license) {
100            for user in license.used_by {
101                if !l.used_by.contains(&user) {
102                    l.used_by.push(user);
103                }
104            }
105        } else {
106            into.push(license);
107        }
108    }
109}
110
111/// Sort vec by license name, and users of each license by name.
112pub fn sort_licenses(l: &mut Vec<LicenseUsed>) {
113    l.sort_by(|a, b| a.license.name.cmp(&b.license.name));
114    for l in l {
115        l.used_by.sort_by(|a, b| a.name.cmp(&b.name));
116    }
117}
118
119/// Calls [`cargo about`] for the crate.
120///
121/// This method must be used in build scripts (`build.rs`).
122///
123/// Returns an empty vec if the [`DOCS_RS`] env var is set to any value or if `ZNG_TP_LICENSES=false` is set.
124///
125/// # Panics
126///
127/// Panics for any error, including `cargo about` errors and JSON deserialization errors.
128///
129/// [`cargo about`]: https://github.com/EmbarkStudios/cargo-about
130/// [`DOCS_RS`]: https://docs.rs/about/builds#detecting-docsrs
131#[cfg(feature = "build")]
132pub fn collect_cargo_about(about_cfg_path: &str) -> Vec<LicenseUsed> {
133    if std::env::var("DOCS_RS").is_ok() || std::env::var("ZNG_TP_LICENSES").unwrap_or_default() == "false" {
134        return vec![];
135    }
136
137    let mut cargo_about = std::process::Command::new("cargo");
138    cargo_about
139        .arg("about")
140        .arg("generate")
141        .arg("--format")
142        .arg("json")
143        .arg("--all-features");
144
145    // cargo about returns an error on stdout redirect in PowerShell
146    #[cfg(windows)]
147    let temp_file = tempfile::NamedTempFile::new().expect("cannot crate temp file for windows output");
148    #[cfg(windows)]
149    {
150        cargo_about.arg("--output-file").arg(temp_file.path());
151    }
152
153    if !about_cfg_path.is_empty() {
154        cargo_about.arg("-c").arg(about_cfg_path);
155    }
156
157    let output = cargo_about.output().expect("error calling `cargo about`");
158    let error = String::from_utf8(output.stderr).unwrap();
159    assert!(
160        output.status.success(),
161        "error code calling `cargo about`, {:?}\nstderr:\n{error}",
162        output.status
163    );
164
165    #[cfg(windows)]
166    let json = std::fs::read_to_string(temp_file.path()).expect("cannot read temp file with windows output");
167    #[cfg(not(windows))]
168    let json = String::from_utf8(output.stdout).unwrap();
169
170    parse_cargo_about(&json).expect("error parsing `cargo about` output")
171}
172
173/// Parse the output of [`cargo about`].
174///
175/// Example command:
176///
177/// ```console
178/// cargo about generate -c .cargo/about.toml --format json --workspace --all-features
179/// ```
180///
181/// See also [`collect_cargo_about`] that calls the command.
182///
183/// [`cargo about`]: https://github.com/EmbarkStudios/cargo-about
184#[cfg(feature = "build")]
185pub fn parse_cargo_about(json: &str) -> Result<Vec<LicenseUsed>, serde_json::Error> {
186    #[derive(Deserialize)]
187    struct Output {
188        licenses: Vec<LicenseJson>,
189    }
190    #[derive(Deserialize)]
191    struct LicenseJson {
192        id: Txt,
193        name: Txt,
194        text: Txt,
195        used_by: Vec<UsedBy>,
196    }
197    impl LicenseJson {
198        fn into(self) -> LicenseUsed {
199            LicenseUsed {
200                license: License {
201                    id: self.id,
202                    name: self.name,
203                    text: self.text,
204                },
205                used_by: self.used_by.into_iter().map(UsedBy::into).collect(),
206            }
207        }
208    }
209    #[derive(Deserialize)]
210    struct UsedBy {
211        #[serde(rename = "crate")]
212        crate_: Crate,
213    }
214    #[derive(Deserialize)]
215    struct Crate {
216        name: Txt,
217        version: Txt,
218        #[serde(default)]
219        repository: Option<Txt>,
220    }
221    impl UsedBy {
222        fn into(self) -> User {
223            let repo = self.crate_.repository.unwrap_or_default();
224            User {
225                version: self.crate_.version,
226                url: if repo.is_empty() {
227                    zng_txt::formatx!("https://crates.io/crates/{}", self.crate_.name)
228                } else {
229                    repo
230                },
231                name: self.crate_.name,
232            }
233        }
234    }
235
236    serde_json::from_str::<Output>(json).map(|o| o.licenses.into_iter().map(LicenseJson::into).collect())
237}
238
239/// Bincode serialize and deflate the licenses.
240///
241/// # Panics
242///
243/// Panics in case of any error.
244#[cfg(feature = "build")]
245pub fn encode_licenses(licenses: &[LicenseUsed]) -> Vec<u8> {
246    deflate::deflate_bytes(&bincode::serialize(licenses).expect("bincode error"))
247}
248
249/// Encode licenses and write to the output file that is included by [`include_bundle!`].
250///
251/// # Panics
252///
253/// Panics in case of any error.
254#[cfg(feature = "build")]
255pub fn write_bundle(licenses: &[LicenseUsed]) {
256    let bin = encode_licenses(licenses);
257    std::fs::write(format!("{}/zng-tp-licenses.bin", std::env::var("OUT_DIR").unwrap()), bin).expect("error writing file");
258}
259
260/// Includes the bundle file generated using [`write_bundle`].
261///
262/// This macro output is a `Vec<LicenseUsed>`.
263#[macro_export]
264#[cfg(feature = "bundle")]
265macro_rules! include_bundle {
266    () => {
267        $crate::include_bundle!(concat!(env!("OUT_DIR"), "/zng-tp-licenses.bin"))
268    };
269    ($custom_name:expr) => {{ $crate::decode_licenses(include_bytes!($custom_name)) }};
270}
271
272/// Decode licenses encoded with [`encode_licenses`]. Note that the encoded format is only guaranteed to work
273/// if both encoding and decoding is made with the same `Cargo.lock` dependencies.
274#[cfg(feature = "bundle")]
275pub fn decode_licenses(bin: &[u8]) -> Vec<LicenseUsed> {
276    let bin = inflate::inflate_bytes(bin).expect("invalid bundle deflate binary");
277    bincode::deserialize(&bin).expect("invalid bundle bincode binary")
278}