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: impl AsRef<std::path::Path>) -> Vec<LicenseUsed> {
156 collect_cargo_about_for_impl(
158 about_cfg_path.as_ref(),
159 "Cargo.toml".as_ref(),
160 &std::env::var("CARGO_CFG_FEATURE").unwrap_or_default(),
161 )
162}
163#[cfg(feature = "build")]
176pub fn collect_cargo_about_for(
177 about_cfg_path: impl AsRef<std::path::Path>,
178 manifest_path: impl AsRef<std::path::Path>,
179 features: &str,
180) -> Vec<LicenseUsed> {
181 collect_cargo_about_for_impl(about_cfg_path.as_ref(), manifest_path.as_ref(), features)
182}
183#[cfg(feature = "build")]
184fn collect_cargo_about_for_impl(about_cfg_path: &std::path::Path, manifest_path: &std::path::Path, features: &str) -> Vec<LicenseUsed> {
185 if std::env::var("DOCS_RS").is_ok() || std::env::var("ZNG_TP_LICENSES").unwrap_or_default() == "false" {
186 return vec![];
187 }
188
189 let mut cargo_about = std::process::Command::new("cargo");
190 cargo_about
191 .arg("about")
192 .arg("generate")
193 .arg("--manifest-path")
194 .arg(manifest_path)
195 .arg("--format")
196 .arg("json")
197 .arg("--no-default-features");
198
199 for feature in features.split(',') {
201 cargo_about.arg("--features");
202 cargo_about.arg(feature);
203 }
204
205 #[cfg(windows)]
207 let temp_file = tempfile::NamedTempFile::new().expect("cannot crate temp file for windows output");
208 #[cfg(windows)]
209 {
210 cargo_about.arg("--output-file").arg(temp_file.path());
211 }
212
213 if !about_cfg_path.as_os_str().is_empty() {
214 cargo_about.arg("--config").arg(about_cfg_path);
215 }
216
217 let output = cargo_about.output().expect("error calling `cargo about`");
218 let error = String::from_utf8(output.stderr).unwrap();
219 assert!(
220 output.status.success(),
221 "error code calling `cargo about`, {:?}\nstderr:\n{error}",
222 output.status
223 );
224
225 #[cfg(windows)]
226 let json = std::fs::read_to_string(temp_file.path()).expect("cannot read temp file with windows output");
227 #[cfg(not(windows))]
228 let json = String::from_utf8(output.stdout).unwrap();
229
230 let mut entries = parse_cargo_about(&json).expect("error parsing `cargo about` output");
231
232 entries.retain_mut(|e| {
234 e.used_by.retain(|u| !u.version.ends_with("-local"));
235 !e.used_by.is_empty()
236 });
237
238 entries
239}
240
241#[cfg(feature = "build")]
253pub fn parse_cargo_about(json: &str) -> Result<Vec<LicenseUsed>, serde_json::Error> {
254 #[derive(Deserialize)]
255 struct Output {
256 licenses: Vec<LicenseJson>,
257 }
258 #[derive(Deserialize)]
259 struct LicenseJson {
260 id: Txt,
261 name: Txt,
262 text: Txt,
263 used_by: Vec<UsedBy>,
264 }
265 impl LicenseJson {
266 fn into(self) -> LicenseUsed {
267 LicenseUsed {
268 license: License {
269 id: self.id,
270 name: self.name,
271 text: self.text,
272 },
273 used_by: self.used_by.into_iter().map(UsedBy::into).collect(),
274 }
275 }
276 }
277 #[derive(Deserialize)]
278 struct UsedBy {
279 #[serde(rename = "crate")]
280 crate_: Crate,
281 }
282 #[derive(Deserialize)]
283 struct Crate {
284 name: Txt,
285 version: Txt,
286 #[serde(default)]
287 repository: Option<Txt>,
288 }
289 impl UsedBy {
290 fn into(self) -> User {
291 let repo = self.crate_.repository.unwrap_or_default();
292 User {
293 version: self.crate_.version,
294 url: if repo.is_empty() {
295 zng_txt::formatx!("https://crates.io/crates/{}", self.crate_.name)
296 } else {
297 repo
298 },
299 name: self.crate_.name,
300 }
301 }
302 }
303
304 serde_json::from_str::<Output>(json).map(|o| o.licenses.into_iter().map(LicenseJson::into).collect())
305}
306
307#[cfg(feature = "build")]
313pub fn encode_licenses(licenses: &[LicenseUsed]) -> Vec<u8> {
314 deflate::deflate_bytes(&postcard::to_allocvec(licenses).expect("postard error"))
315}
316
317#[cfg(feature = "build")]
323pub fn write_embedding(licenses: &[LicenseUsed]) {
324 let bin = encode_licenses(licenses);
325 std::fs::write(format!("{}/zng-tp-licenses.bin", std::env::var("OUT_DIR").unwrap()), bin).expect("error writing file");
326}
327
328#[macro_export]
332#[cfg(feature = "embed")]
333macro_rules! decode_embedding {
334 () => {
335 $crate::decode_embedding!(concat!(env!("OUT_DIR"), "/zng-tp-licenses.bin"))
336 };
337 ($custom_name:expr) => {{ $crate::decode_licenses(include_bytes!($custom_name)) }};
338}
339
340#[cfg(feature = "embed")]
343pub fn decode_licenses(bin: &[u8]) -> Vec<LicenseUsed> {
344 let bin = inflate::inflate_bytes(bin).expect("invalid deflate binary");
345 postcard::from_bytes(&bin).expect("invalid postard binary")
346}