nix_query/
nix.rs

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            // No URL? Play it safe and write the full name too.
77            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    /// I assume this is a Mistake.
241    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>, // url
269    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,    // gzip-1.10
283    pname: String,   // gzip
284    version: String, // 1.10
285    system: String,  // x86_64-linux
286    meta: NixMeta,
287    attr: Option<String>, // nixos.gzip
288}
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        // long_description is multiline so we indent it
343        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    /// Output was well-formed but empty. (This should not appear.)
378    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    // A few sub-packages don't show up by default. Is there a better way to
415    // include them...?
416    // TODO: Select 'nixpkgs' or 'nixos' automatically, somehow.
417    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        // Attribute names starting with _ are usually meant to be "private"
433        .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}