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#![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)]
70pub struct License {
71 pub id: Txt,
75 pub name: Txt,
77 pub text: Txt,
79}
80
81#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug, Clone)]
83pub struct User {
84 pub name: Txt,
86 #[serde(default)]
88 pub version: Txt,
89 #[serde(default)]
91 pub url: Txt,
92}
93
94pub 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
111pub 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#[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 #[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#[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#[cfg(feature = "build")]
245pub fn encode_licenses(licenses: &[LicenseUsed]) -> Vec<u8> {
246 deflate::deflate_bytes(&bincode::serialize(licenses).expect("bincode error"))
247}
248
249#[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#[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#[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}