zng_tp_licenses/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/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)]
70#[non_exhaustive]
71pub struct License {
72    /// License [SPDX] id.
73    ///
74    /// [SPDX]: https://spdx.org/licenses/
75    pub id: Txt,
76    /// License name.
77    pub name: Txt,
78    /// License text.
79    pub text: Txt,
80}
81
82impl License {
83    /// New License.
84    pub fn new(id: impl Into<Txt>, name: impl Into<Txt>, text: impl Into<Txt>) -> Self {
85        Self {
86            id: id.into(),
87            name: name.into(),
88            text: text.into(),
89        }
90    }
91}
92
93/// Represents a project or package that uses a license.
94#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug, Clone)]
95#[non_exhaustive]
96pub struct User {
97    /// Project or package name.
98    pub name: Txt,
99    /// Package version.
100    #[serde(default)]
101    pub version: Txt,
102    /// Project or package URL.
103    #[serde(default)]
104    pub url: Txt,
105}
106impl User {
107    /// New license user.
108    pub fn new(name: impl Into<Txt>, version: impl Into<Txt>, url: impl Into<Txt>) -> Self {
109        Self {
110            name: name.into(),
111            version: version.into(),
112            url: url.into(),
113        }
114    }
115}
116
117/// Merge `licenses` into `into`.
118///
119/// The licenses and users are not sorted, call [`sort_licenses`] after merging all licenses.
120pub fn merge_licenses(into: &mut Vec<LicenseUsed>, licenses: Vec<LicenseUsed>) {
121    for license in licenses {
122        if let Some(l) = into.iter_mut().find(|l| l.license == license.license) {
123            for user in license.used_by {
124                if !l.used_by.contains(&user) {
125                    l.used_by.push(user);
126                }
127            }
128        } else {
129            into.push(license);
130        }
131    }
132}
133
134/// Sort vec by license name, and users of each license by name.
135pub fn sort_licenses(l: &mut Vec<LicenseUsed>) {
136    l.sort_by(|a, b| a.license.name.cmp(&b.license.name));
137    for l in l {
138        l.used_by.sort_by(|a, b| a.name.cmp(&b.name));
139    }
140}
141
142/// Calls [`cargo about`] for the crate.
143///
144/// This method must be used in build scripts (`build.rs`).
145///
146/// Returns an empty vec if the [`DOCS_RS`] env var is set to any value or if `"ZNG_TP_LICENSES=false"` is set.
147///
148/// # Panics
149///
150/// Panics for any error, including `cargo about` errors and JSON deserialization errors.
151///
152/// [`cargo about`]: https://github.com/EmbarkStudios/cargo-about
153/// [`DOCS_RS`]: https://docs.rs/about/builds#detecting-docsrs
154#[cfg(feature = "build")]
155pub fn collect_cargo_about(about_cfg_path: &str) -> Vec<LicenseUsed> {
156    if std::env::var("DOCS_RS").is_ok() || std::env::var("ZNG_TP_LICENSES").unwrap_or_default() == "false" {
157        return vec![];
158    }
159
160    let mut cargo_about = std::process::Command::new("cargo");
161    cargo_about
162        .arg("about")
163        .arg("generate")
164        .arg("--format")
165        .arg("json")
166        .arg("--all-features");
167
168    // cargo about returns an error on stdout redirect in PowerShell
169    #[cfg(windows)]
170    let temp_file = tempfile::NamedTempFile::new().expect("cannot crate temp file for windows output");
171    #[cfg(windows)]
172    {
173        cargo_about.arg("--output-file").arg(temp_file.path());
174    }
175
176    if !about_cfg_path.is_empty() {
177        cargo_about.arg("-c").arg(about_cfg_path);
178    }
179
180    let output = cargo_about.output().expect("error calling `cargo about`");
181    let error = String::from_utf8(output.stderr).unwrap();
182    assert!(
183        output.status.success(),
184        "error code calling `cargo about`, {:?}\nstderr:\n{error}",
185        output.status
186    );
187
188    #[cfg(windows)]
189    let json = std::fs::read_to_string(temp_file.path()).expect("cannot read temp file with windows output");
190    #[cfg(not(windows))]
191    let json = String::from_utf8(output.stdout).unwrap();
192
193    parse_cargo_about(&json).expect("error parsing `cargo about` output")
194}
195
196/// Parse the output of [`cargo about`].
197///
198/// Example command:
199///
200/// ```console
201/// cargo about generate -c .cargo/about.toml --format json --workspace --all-features
202/// ```
203///
204/// See also [`collect_cargo_about`] that calls the command.
205///
206/// [`cargo about`]: https://github.com/EmbarkStudios/cargo-about
207#[cfg(feature = "build")]
208pub fn parse_cargo_about(json: &str) -> Result<Vec<LicenseUsed>, serde_json::Error> {
209    #[derive(Deserialize)]
210    struct Output {
211        licenses: Vec<LicenseJson>,
212    }
213    #[derive(Deserialize)]
214    struct LicenseJson {
215        id: Txt,
216        name: Txt,
217        text: Txt,
218        used_by: Vec<UsedBy>,
219    }
220    impl LicenseJson {
221        fn into(self) -> LicenseUsed {
222            LicenseUsed {
223                license: License {
224                    id: self.id,
225                    name: self.name,
226                    text: self.text,
227                },
228                used_by: self.used_by.into_iter().map(UsedBy::into).collect(),
229            }
230        }
231    }
232    #[derive(Deserialize)]
233    struct UsedBy {
234        #[serde(rename = "crate")]
235        crate_: Crate,
236    }
237    #[derive(Deserialize)]
238    struct Crate {
239        name: Txt,
240        version: Txt,
241        #[serde(default)]
242        repository: Option<Txt>,
243    }
244    impl UsedBy {
245        fn into(self) -> User {
246            let repo = self.crate_.repository.unwrap_or_default();
247            User {
248                version: self.crate_.version,
249                url: if repo.is_empty() {
250                    zng_txt::formatx!("https://crates.io/crates/{}", self.crate_.name)
251                } else {
252                    repo
253                },
254                name: self.crate_.name,
255            }
256        }
257    }
258
259    serde_json::from_str::<Output>(json).map(|o| o.licenses.into_iter().map(LicenseJson::into).collect())
260}
261
262/// Bincode serialize and deflate the licenses.
263///
264/// # Panics
265///
266/// Panics in case of any error.
267#[cfg(feature = "build")]
268pub fn encode_licenses(licenses: &[LicenseUsed]) -> Vec<u8> {
269    deflate::deflate_bytes(&bincode::serde::encode_to_vec(licenses, bincode::config::standard()).expect("bincode error"))
270}
271
272/// Encode licenses and write to the output file that is included by [`include_bundle!`].
273///
274/// # Panics
275///
276/// Panics in case of any error.
277#[cfg(feature = "build")]
278pub fn write_bundle(licenses: &[LicenseUsed]) {
279    let bin = encode_licenses(licenses);
280    std::fs::write(format!("{}/zng-tp-licenses.bin", std::env::var("OUT_DIR").unwrap()), bin).expect("error writing file");
281}
282
283/// Includes the bundle file generated using [`write_bundle`].
284///
285/// This macro output is a `Vec<LicenseUsed>`.
286#[macro_export]
287#[cfg(feature = "bundle")]
288macro_rules! include_bundle {
289    () => {
290        $crate::include_bundle!(concat!(env!("OUT_DIR"), "/zng-tp-licenses.bin"))
291    };
292    ($custom_name:expr) => {{ $crate::decode_licenses(include_bytes!($custom_name)) }};
293}
294
295/// Decode licenses encoded with [`encode_licenses`]. Note that the encoded format is only guaranteed to work
296/// if both encoding and decoding is made with the same `Cargo.lock` dependencies.
297#[cfg(feature = "bundle")]
298pub fn decode_licenses(bin: &[u8]) -> Vec<LicenseUsed> {
299    let bin = inflate::inflate_bytes(bin).expect("invalid bundle deflate binary");
300    bincode::serde::decode_from_slice(&bin, bincode::config::standard())
301        .expect("invalid bundle bincode binary")
302        .0
303}