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#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9
10use std::fmt;
11
12use serde::{Deserialize, Serialize};
13use zng_txt::Txt;
14
15#[derive(Serialize, Deserialize, Clone)]
17pub struct LicenseUsed {
18 pub license: License,
20 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 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
44pub 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#[derive(Clone, PartialEq, Eq)]
53pub struct UserLicense {
54 pub user: User,
56 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#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, Hash)]
70#[non_exhaustive]
71pub struct License {
72 pub id: Txt,
76 pub name: Txt,
78 pub text: Txt,
80}
81
82impl License {
83 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#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug, Clone)]
95#[non_exhaustive]
96pub struct User {
97 pub name: Txt,
99 #[serde(default)]
101 pub version: Txt,
102 #[serde(default)]
104 pub url: Txt,
105}
106impl User {
107 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
117pub 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
134pub 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#[cfg(feature = "build")]
155pub fn collect_cargo_about(about_cfg_path: &str) -> Vec<LicenseUsed> {
156 collect_cargo_about_for(
159 about_cfg_path,
160 "Cargo.toml",
161 &std::env::var("CARGO_CFG_FEATURE").unwrap_or_default(),
162 )
163}
164#[cfg(feature = "build")]
177pub fn collect_cargo_about_for(
178 about_cfg_path: impl AsRef<std::path::Path>,
179 manifest_path: impl AsRef<std::path::Path>,
180 features: &str,
181) -> Vec<LicenseUsed> {
182 collect_cargo_about_for_impl(about_cfg_path.as_ref(), manifest_path.as_ref(), features)
183}
184#[cfg(feature = "build")]
185fn collect_cargo_about_for_impl(about_cfg_path: &std::path::Path, manifest_path: &std::path::Path, features: &str) -> Vec<LicenseUsed> {
186 if std::env::var("DOCS_RS").is_ok() || std::env::var("ZNG_TP_LICENSES").unwrap_or_default() == "false" {
187 return vec![];
188 }
189
190 let mut cargo_about = std::process::Command::new("cargo");
191 cargo_about
192 .arg("about")
193 .arg("generate")
194 .arg("--manifest-path")
195 .arg(manifest_path)
196 .arg("--format")
197 .arg("json")
198 .arg("--no-default-features");
199
200 for feature in features.split(',') {
202 cargo_about.arg("--features");
203 cargo_about.arg(feature);
204 }
205
206 #[cfg(windows)]
208 let temp_file = tempfile::NamedTempFile::new().expect("cannot crate temp file for windows output");
209 #[cfg(windows)]
210 {
211 cargo_about.arg("--output-file").arg(temp_file.path());
212 }
213
214 if !about_cfg_path.as_os_str().is_empty() {
215 cargo_about.arg("--config").arg(about_cfg_path);
216 }
217
218 let output = cargo_about.output().expect("error calling `cargo about`");
219 let error = String::from_utf8(output.stderr).unwrap();
220 assert!(
221 output.status.success(),
222 "error code calling `cargo about`, {:?}\nstderr:\n{error}",
223 output.status
224 );
225
226 #[cfg(windows)]
227 let json = std::fs::read_to_string(temp_file.path()).expect("cannot read temp file with windows output");
228 #[cfg(not(windows))]
229 let json = String::from_utf8(output.stdout).unwrap();
230
231 let mut entries = parse_cargo_about(&json).expect("error parsing `cargo about` output");
232
233 entries.retain_mut(|e| {
235 e.used_by.retain(|u| !u.version.ends_with("-local"));
236 !e.used_by.is_empty()
237 });
238
239 entries
240}
241
242#[cfg(feature = "build")]
254pub fn parse_cargo_about(json: &str) -> Result<Vec<LicenseUsed>, serde_json::Error> {
255 #[derive(Deserialize)]
256 struct Output {
257 licenses: Vec<LicenseJson>,
258 }
259 #[derive(Deserialize)]
260 struct LicenseJson {
261 id: Txt,
262 name: Txt,
263 text: Txt,
264 used_by: Vec<UsedBy>,
265 }
266 impl LicenseJson {
267 fn into(self) -> LicenseUsed {
268 LicenseUsed {
269 license: License {
270 id: self.id,
271 name: self.name,
272 text: self.text,
273 },
274 used_by: self.used_by.into_iter().map(UsedBy::into).collect(),
275 }
276 }
277 }
278 #[derive(Deserialize)]
279 struct UsedBy {
280 #[serde(rename = "crate")]
281 crate_: Crate,
282 }
283 #[derive(Deserialize)]
284 struct Crate {
285 name: Txt,
286 version: Txt,
287 #[serde(default)]
288 repository: Option<Txt>,
289 }
290 impl UsedBy {
291 fn into(self) -> User {
292 let repo = self.crate_.repository.unwrap_or_default();
293 User {
294 version: self.crate_.version,
295 url: if repo.is_empty() {
296 zng_txt::formatx!("https://crates.io/crates/{}", self.crate_.name)
297 } else {
298 repo
299 },
300 name: self.crate_.name,
301 }
302 }
303 }
304
305 serde_json::from_str::<Output>(json).map(|o| o.licenses.into_iter().map(LicenseJson::into).collect())
306}
307
308#[cfg(feature = "build")]
314pub fn encode_licenses(licenses: &[LicenseUsed]) -> Vec<u8> {
315 deflate::deflate_bytes(&postcard::to_allocvec(licenses).expect("postard error"))
316}
317
318#[deprecated = "renamed to `write_embedding`"]
320#[cfg(feature = "build")]
321pub fn write_bundle(licenses: &[LicenseUsed]) {
322 write_embedding(licenses);
323}
324
325#[cfg(feature = "build")]
331pub fn write_embedding(licenses: &[LicenseUsed]) {
332 let bin = encode_licenses(licenses);
333 std::fs::write(format!("{}/zng-tp-licenses.bin", std::env::var("OUT_DIR").unwrap()), bin).expect("error writing file");
334}
335
336#[macro_export]
340#[cfg(feature = "embed")]
341macro_rules! decode_embedding {
342 () => {
343 $crate::decode_embedding!(concat!(env!("OUT_DIR"), "/zng-tp-licenses.bin"))
344 };
345 ($custom_name:expr) => {{ $crate::decode_licenses(include_bytes!($custom_name)) }};
346}
347
348#[deprecated = "renamed to decode_embedding"]
350#[macro_export]
351#[cfg(feature = "embed")]
352macro_rules! include_bundle {
353 ($($tt:tt)*) => {
354 $crate::decode_embedding!($($tt)*)
355 };
356}
357
358#[cfg(feature = "embed")]
361pub fn decode_licenses(bin: &[u8]) -> Vec<LicenseUsed> {
362 let bin = inflate::inflate_bytes(bin).expect("invalid deflate binary");
363 postcard::from_bytes(&bin).expect("invalid postard binary")
364}