1use std::collections::HashMap;
2use std::convert::TryFrom;
3use std::fmt;
4use std::fmt::{Display, Formatter};
5use std::process::Command;
6use std::str::FromStr;
7
8use colored::*;
9use serde::{Deserialize, Deserializer};
10use serde_json;
11
12use crate::proc;
13use crate::proc::CommandError;
14
15#[derive(Deserialize, Debug, PartialEq, Clone)]
16#[serde(rename_all = "camelCase")]
17pub struct FullLicense {
18 full_name: String,
19 short_name: String,
20 spdx_id: Option<String>,
21 url: Option<String>,
22 #[serde(default = "true_")]
23 free: bool,
24}
25
26impl FullLicense {
27 pub fn console_fmt(&self) -> ConsoleFormatFullLicense {
28 ConsoleFormatFullLicense(self)
29 }
30}
31
32pub struct ConsoleFormatFullLicense<'a>(&'a FullLicense);
33
34impl<'a> ConsoleFormatFullLicense<'a> {
35 fn fmt_unfree(&self, f: &mut Formatter<'_>) -> fmt::Result {
36 let license = self.0;
37 let has_full_name = license.full_name != "Unfree";
38 let has_short_name = license.short_name != "unfree";
39
40 let mut parenthetical = false;
41
42 if has_full_name {
43 write!(f, "{}", license.full_name)?;
44 parenthetical = true;
45 write!(f, " ({}", "unfree".bold().red())?;
46 } else {
47 write!(f, "{}", "unfree".bold().red())?;
48 }
49
50 if has_short_name {
51 if parenthetical {
52 write!(f, "; ")?;
53 } else {
54 write!(f, " (")?;
55 }
56 write!(f, "{})", license.short_name)?;
57 } else if parenthetical {
58 write!(f, ")")?;
59 }
60
61 if let Some(url_str) = &license.url {
62 write!(f, " {}", url(url_str.as_ref()))?;
63 }
64
65 Ok(())
66 }
67
68 fn fmt_free(&self, f: &mut Formatter<'_>) -> fmt::Result {
69 let license = self.0;
70
71 if let Some(spdx_id) = &license.spdx_id {
72 write!(f, "{}", spdx_id)?;
73 } else {
74 write!(f, "{}", license.short_name)?;
75
76 if license.url.is_none() {
78 write!(f, " ({})", license.full_name)?;
79 }
80 }
81
82 if let Some(url_str) = &license.url {
83 write!(f, " {}", url(url_str.as_ref()))?;
84 }
85
86 Ok(())
87 }
88}
89
90impl Display for ConsoleFormatFullLicense<'_> {
91 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
92 if self.0.free {
93 self.fmt_free(f)
94 } else {
95 self.fmt_unfree(f)
96 }
97 }
98}
99
100#[derive(Deserialize, Debug, PartialEq, Clone)]
101#[serde(rename_all = "camelCase")]
102pub struct NamedLicense {
103 full_name: String,
104}
105
106#[derive(Deserialize, Debug, PartialEq, Clone)]
107#[serde(rename_all = "camelCase")]
108pub struct UrlLicense {
109 url: String,
110}
111
112#[derive(Deserialize, Debug, PartialEq, Clone)]
113#[serde(untagged)]
114pub enum License {
115 Id(String),
116 Full(FullLicense),
117 FullVec(Vec<FullLicense>),
118 Named(NamedLicense),
119 Url(UrlLicense),
120}
121
122impl License {
123 pub fn console_fmt(&self) -> ConsoleFormatLicense {
124 ConsoleFormatLicense(self)
125 }
126}
127
128fn url<C: Colorize>(s: C) -> ColoredString {
129 s.underline().cyan()
130}
131
132fn write_licenses(licenses: &[FullLicense], f: &mut Formatter<'_>) -> fmt::Result {
133 if licenses.is_empty() {
134 Ok(())
135 } else if licenses.len() == 1 {
136 write!(f, "{}", licenses.get(0).unwrap().console_fmt())
137 } else {
138 for license in licenses
139 .iter()
140 .take(licenses.len() - 1)
141 .map(FullLicense::console_fmt)
142 {
143 write!(f, "{}\n ", license)?;
144 }
145 write!(f, "{}", licenses.last().unwrap().console_fmt())?;
146
147 Ok(())
148 }
149}
150
151pub struct ConsoleFormatLicense<'a>(&'a License);
152
153impl Display for ConsoleFormatLicense<'_> {
154 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
155 match self.0 {
156 License::Id(s) => write!(f, "{}", s),
157 License::Named(s) => write!(f, "{}", s.full_name),
158 License::Url(s) => write!(f, "{}", url(s.url.as_ref())),
159 License::Full(s) => write!(f, "{}", s.console_fmt()),
160 License::FullVec(s) => write_licenses(s, f),
161 }
162 }
163}
164
165fn true_() -> bool {
166 true
167}
168
169#[derive(Deserialize, Debug, PartialEq, Clone)]
170#[serde(rename_all = "camelCase", try_from = "String")]
171pub struct NixPath {
172 path: String,
173 line: usize,
174}
175
176#[derive(Debug, Clone)]
177pub enum NixPathParseErr {
178 BadSplit,
179}
180
181impl Display for NixPathParseErr {
182 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
183 match self {
184 Self::BadSplit => write!(f, "Nix path must contain a ':'"),
185 }
186 }
187}
188
189impl FromStr for NixPath {
190 type Err = NixPathParseErr;
191
192 fn from_str(s: &str) -> Result<Self, Self::Err> {
193 let mut split = s.split(':');
194 let path = split.next().ok_or(NixPathParseErr::BadSplit)?.to_string();
195 let line = split
196 .next()
197 .ok_or(NixPathParseErr::BadSplit)?
198 .parse()
199 .map_err(|_| NixPathParseErr::BadSplit)?;
200 Ok(NixPath { path, line })
201 }
202}
203
204impl TryFrom<String> for NixPath {
205 type Error = NixPathParseErr;
206
207 fn try_from(s: String) -> Result<Self, Self::Error> {
208 s.parse()
209 }
210}
211
212#[derive(Deserialize, Debug, PartialEq, Clone)]
213pub struct Key {
214 longkeyid: String,
215 fingerprint: String,
216}
217
218#[derive(Deserialize, Debug, PartialEq, Clone)]
219#[serde(rename_all = "camelCase")]
220pub struct MaintainerInfo {
221 name: Option<String>,
222 email: String,
223 github: Option<String>,
224 github_id: Option<usize>,
225 #[serde(default = "Vec::new")]
226 keys: Vec<Key>,
227}
228
229#[derive(Deserialize, Debug, PartialEq, Clone)]
230#[serde(untagged)]
231pub enum Maintainer {
232 Name(String),
233 Info(MaintainerInfo),
234}
235
236#[derive(Deserialize, Debug)]
237#[serde(untagged)]
238enum Platforms {
239 Normal(Vec<String>),
240 Weird(Vec<Vec<String>>),
242}
243
244impl From<Platforms> for Vec<String> {
245 fn from(p: Platforms) -> Self {
246 match p {
247 Platforms::Normal(v) => v,
248 Platforms::Weird(vs) => vs.iter().flatten().cloned().collect(),
249 }
250 }
251}
252
253fn deserialize_platforms<'de, D>(d: D) -> Result<Vec<String>, D::Error>
254where
255 D: Deserializer<'de>,
256{
257 Platforms::deserialize(d).map(Into::into)
258}
259
260#[derive(Deserialize, Debug, PartialEq, Clone, Default)]
261#[serde(rename_all = "camelCase", default)]
262pub struct NixMeta {
263 #[serde(default = "true_")]
264 available: bool,
265 broken: bool,
266 description: Option<String>,
267 long_description: Option<String>,
268 homepage: Option<String>, license: Option<License>,
270 name: Option<String>,
271 outputs_to_install: Vec<String>,
272 #[serde(deserialize_with = "deserialize_platforms")]
273 platforms: Vec<String>,
274 position: Option<NixPath>,
275 priority: Option<isize>,
276 maintainers: Vec<Maintainer>,
277}
278
279#[derive(Deserialize, Debug, PartialEq, Clone)]
280#[serde(rename_all = "camelCase")]
281pub struct NixInfo {
282 name: String, pname: String, version: String, system: String, meta: NixMeta,
287 attr: Option<String>, }
289
290impl NixInfo {
291 pub fn console_fmt(&self) -> ConsoleFormatInfo {
292 ConsoleFormatInfo(self)
293 }
294}
295
296#[derive(Deserialize, Debug, PartialEq, Clone)]
297#[serde(rename_all = "camelCase", transparent)]
298pub struct AllNixInfo {
299 pub attrs: HashMap<String, NixInfo>,
300}
301
302pub struct ConsoleFormatInfo<'a>(&'a NixInfo);
303
304impl Display for ConsoleFormatInfo<'_> {
305 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
306 macro_rules! write_val {
307 ($f:expr, $label:expr, $val:expr) => {
308 writeln!($f, "{} {}", format!("{}:", $label).bold(), $val)
309 };
310 }
311
312 macro_rules! write_val_opt {
313 ($f:expr, $label:expr, $val:expr) => {
314 if let Some(v) = $val {
315 write_val!($f, $label, v)
316 } else {
317 Ok(())
318 }
319 };
320 }
321
322 let info = self.0;
323 write_val_opt!(f, "attr", &info.attr.as_ref().map(|a| a.bold().green()))?;
324 write_val!(f, "name", info.name.bold().green())?;
325
326 let meta = &info.meta;
327 if meta.broken {
328 write_val!(f, "broken", "true".bold().red())?;
329 }
330 if !meta.available {
331 write_val!(f, "available", "false".bold().red())?;
332 }
333
334 write_val_opt!(f, "priority", &meta.priority)?;
335
336 if let Some(homepage) = &meta.homepage {
337 write_val!(f, "homepage", homepage.underline().cyan())?;
338 }
339
340 write_val_opt!(f, "description", &meta.description)?;
341
342 if let Some(long_desc) = &meta.long_description {
344 let mut lines = long_desc.lines();
345 let first_line_opt = lines.next();
346 if let Some(first_line) = first_line_opt {
347 write_val!(f, "long desc.", first_line)?;
348 for line in lines {
349 writeln!(f, " {}", line)?;
350 }
351 }
352 }
353
354 write_val_opt!(
355 f,
356 "license",
357 &meta.license.as_ref().map(License::console_fmt)
358 )?;
359
360 write_val_opt!(
361 f,
362 "defined in",
363 &meta.position.as_ref().map(|pos| format!(
364 "{} line {}",
365 pos.path.underline(),
366 pos.line,
367 ))
368 )?;
369
370 Ok(())
371 }
372}
373
374#[derive(Debug)]
375pub enum NixQueryError {
376 Command(CommandError),
377 Empty,
379}
380
381impl From<CommandError> for NixQueryError {
382 fn from(e: CommandError) -> Self {
383 Self::Command(e)
384 }
385}
386
387pub fn nix_query(attr: &str) -> Result<NixInfo, NixQueryError> {
388 serde_json::from_str::<AllNixInfo>(&proc::run_cmd_stdout(Command::new("nix-env").args(&[
389 "--query",
390 "--available",
391 "--json",
392 "--attr",
393 attr,
394 ]))?)
395 .map_err(CommandError::De)?
396 .attrs
397 .iter()
398 .next()
399 .ok_or(NixQueryError::Empty)
400 .map(|(attr, info)| NixInfo {
401 attr: Some(attr.clone()),
402 ..info.clone()
403 })
404}
405
406pub fn nix_query_all() -> Result<String, CommandError> {
407 let mut output = proc::run_cmd_stdout(Command::new("nix-env").args(&[
408 "--query",
409 "--available",
410 "--no-name",
411 "--attr-path",
412 ]))?;
413
414 let extra_attrs = &["nixpkgs.nodePackages", "nixpkgs.haskellPackages"];
418
419 for base_attr in extra_attrs {
420 output.push_str(&proc::run_cmd_stdout(Command::new("nix-env").args(&[
421 "--query",
422 "--available",
423 "--no-name",
424 "--attr-path",
425 "--attr",
426 base_attr,
427 ]))?);
428 }
429
430 Ok(output
431 .lines()
432 .filter(|attr| !attr.contains("._"))
434 .fold(String::with_capacity(output.len()), |mut acc, attr| {
435 acc.push_str(attr);
436 acc.push_str("\n");
437 acc
438 }))
439}
440
441#[cfg(test)]
442mod test {
443 use pretty_assertions::assert_eq;
444
445 use super::*;
446
447 #[test]
448 fn test_deserialize_tern() {
449 let tern = include_str!("../test_data/tern.json");
450 assert_eq!(
451 &NixInfo {
452 name: "node_tern-0.24.2".to_string(),
453 pname: "node_tern".to_string(),
454 version: "0.24.2".to_string(),
455 system: "x86_64-linux".to_string(),
456 meta: NixMeta {
457 available: true,
458 description: Some("A JavaScript code analyzer for deep, cross-editor language support".to_string()),
459 homepage: Some("https://github.com/ternjs/tern#readme".to_string()),
460 license: Some(License::Id("MIT".to_string())),
461 name: Some("node_tern-0.24.2".to_string()),
462 outputs_to_install: vec!["out".to_string()],
463 position: Some(NixPath {
464 path: "/nix/store/lybqxz1h84knafw4l9mh248lfiqrw35a-nixpkgs-20.03pre210712.d8cb4ed910c/nixpkgs/pkgs/development/node-packages/node-packages-v10.nix".to_string(),
465 line: 72689,
466 }),
467 broken: false,
468 long_description: None,
469 maintainers: vec![],
470 platforms: vec![],
471 priority: None,
472 },
473 attr: None,
474 },
475 serde_json::from_str::<AllNixInfo>(tern)
476 .unwrap()
477 .attrs
478 .get("nixpkgs.nodePackages.tern").unwrap()
479 );
480 }
481
482 #[test]
483 fn test_deserialize_ok() {
484 let check = |s: &str, label: &str| {
485 serde_json::from_str::<AllNixInfo>(s)
486 .expect(&format!("Can deserialize test data for {}.", label))
487 .attrs
488 .values()
489 .next()
490 .expect(&format!(
491 "String -> NixInfo map for {} has at least one value.",
492 label
493 ))
494 .clone()
495 };
496
497 let _ = check(include_str!("../test_data/gcc.json"), "test_data/gcc.json");
498 let _ = check(include_str!("../test_data/gzip.json"), "test_data/gcc.json");
499 let _ = check(
500 include_str!("../test_data/spotify.json"),
501 "test_data/spotify.json",
502 );
503 let _ = check(
504 include_str!("../test_data/tern.json"),
505 "test_data/tern.json",
506 );
507 let _ = check(
508 include_str!("../test_data/acpitool.json"),
509 "test_data/acpitool.json",
510 );
511 }
512}