brew/
lib.rs

1#[cfg(test)]
2mod tests {
3    use crate::*;
4
5    #[test]
6    fn test_brew_install_test() {
7        assert!(matches!(test_brew_installed(), Ok(())));
8    }
9
10    #[test]
11    fn get_info() {
12        let exa = Package::new("exa").unwrap();
13        assert_eq!(exa.name, "exa");
14        assert_eq!(exa.desc.unwrap(), "Modern replacement for 'ls'");
15        assert!(
16            exa.versions.stable.parse().unwrap() >= version_rs::Version::from((0 as u32, 9 as u32))
17        );
18    }
19
20    #[test]
21    fn look_at_everything() {
22        all_installed().unwrap();
23        all_packages().unwrap();
24    }
25}
26
27use command_builder::{Command, Single};
28use serde::{Deserialize, Serialize};
29use serde_json;
30use std::collections::HashMap;
31use std::str::FromStr;
32use version_rs;
33
34fn brew_return(command: command_builder::Output, name: &str) -> Result<Package> {
35    if command.success() {
36        Ok(Package::new(name)?)
37    } else {
38        test_brew_installed()?;
39        Err(Error::UnknownError(command.stderr().to_owned()))
40    }
41}
42
43/// Represents a string which might be a version number for homebrew.
44/// Homebrew has requirments for version strings, so it is not possable
45/// to definitivly parse it.
46#[derive(Serialize, Deserialize, Clone)]
47#[serde(transparent)]
48pub struct Version {
49    original: String,
50}
51
52impl Version {
53    /// Attempts to return a version of the form "N.N.N".
54    pub fn parse(&self) -> Option<version_rs::Version> {
55        version_rs::Version::from_str(&self.original).ok()
56    }
57
58    /// Returns the original version string.
59    pub fn original(&self) -> &str {
60        &self.original
61    }
62}
63
64/// Represents a homebrew package, which may or may not be installed.
65#[derive(Deserialize, Serialize, Clone)]
66pub struct Package {
67    pub name: String,
68    pub full_name: String,
69    pub aliases: Vec<String>,
70    pub oldname: Option<String>,
71    pub desc: Option<String>,
72    pub homepage: Option<String>,
73    pub versions: Versions,
74    pub urls: HashMap<String, Url>,
75    pub revision: usize,
76    pub version_scheme: usize,
77    pub bottle: HashMap<String, Bottle>,
78    pub keg_only: bool,
79    pub bottle_disabled: bool,
80    pub options: Vec<BrewOption>,
81    pub build_dependencies: Vec<String>,
82    pub dependencies: Vec<String>,
83    pub recommended_dependencies: Vec<String>,
84    pub optional_dependencies: Vec<String>,
85    pub uses_from_macos: Vec<MapOrString>,
86    pub requirements: Vec<Requirment>,
87    pub conflicts_with: Vec<String>,
88    pub caveats: Option<String>,
89    pub installed: Vec<Installed>,
90    pub linked_keg: Option<String>,
91    pub pinned: bool,
92    pub outdated: bool,
93    pub analytics: Option<Analytics>,
94}
95
96#[derive(Deserialize, Serialize, Clone)]
97#[serde(untagged)]
98pub enum MapOrString {
99    MapStringString(HashMap<String, String>),
100    String(String),
101    MapStringVecString(HashMap<String, Vec<String>>),
102}
103
104#[derive(Deserialize, Serialize, Clone)]
105#[serde(untagged)]
106pub enum NumOrString {
107    Num(u32),
108    String(String),
109}
110
111#[derive(Deserialize, Serialize, Clone)]
112pub struct Requirment {
113    name: String,
114    cask: Option<String>,
115    download: Option<String>,
116    version: Option<VersionResult>,
117    contexts: Vec<String>,
118}
119
120#[derive(Deserialize, Serialize, Clone)]
121pub struct BrewOption {
122    option: String,
123    description: String,
124}
125
126pub type Result<T> = std::result::Result<T, Error>;
127
128#[derive(Debug)]
129pub enum Error {
130    NotInstalled,
131    PackageNotFound,
132    IOError(std::io::Error),
133    ParseError(serde_json::Error),
134    InstallFailed(String),
135    UnknownError(String),
136}
137
138impl From<std::io::Error> for Error {
139    fn from(e: std::io::Error) -> Self {
140        Error::IOError(e)
141    }
142}
143
144impl From<serde_json::Error> for Error {
145    fn from(e: serde_json::Error) -> Self {
146        Error::ParseError(e)
147    }
148}
149
150fn contains<I, J, E>(iter1: I, iter2: J) -> bool
151where
152    I: IntoIterator<Item = E>,
153    J: IntoIterator<Item = E>,
154    E: std::cmp::Eq + std::hash::Hash,
155{
156    let hash: std::collections::HashSet<E> = iter1.into_iter().collect();
157    for item in iter2.into_iter() {
158        if !hash.contains(&item) {
159            return false;
160        }
161    }
162    return true;
163}
164
165impl Package {
166    /// Creates package, filling out struct from the command line toole.
167    pub fn new(name: &str) -> Result<Package> {
168        let output = Single::new("/usr/local/bin/brew")
169            .arg("info")
170            .arg(name)
171            .arg("--json=v1")
172            .arg("--analytics")
173            .env("HOMEBREW_NO_AUTO_UPDATE", "1")
174            .run()?;
175        if output.success() {
176            let packages: Vec<Package> = serde_json::from_str(output.stdout())?;
177            packages
178                .into_iter()
179                .next()
180                .map(|p| Ok(p))
181                .unwrap_or(Err(Error::PackageNotFound))
182        } else {
183            test_brew_installed()?;
184            Err(Error::PackageNotFound)
185        }
186    }
187
188    /// Attempts to install a package, reinstalling a package if it is already installed.
189    pub fn install(&self, options: &Options) -> Result<Package> {
190        let command = Single::new("brew")
191            .arg(if self.is_installed() && options.force {
192                "reinstall"
193            } else if self.is_installed() {
194                let opts = self.install_options().unwrap();
195                if contains(opts, options.package_options()) {
196                    return Self::new(&self.name);
197                } else {
198                    "reinstall"
199                }
200            } else {
201                "install"
202            })
203            .args(options.brew_options().as_slice())
204            .arg(&self.name)
205            .args(
206                &options
207                    .package_options()
208                    .into_iter()
209                    .map(|f| f.as_str())
210                    .collect::<Vec<_>>(),
211            )
212            .env("HOMEBREW_NO_AUTO_UPDATE", "1")
213            .run()?;
214        if command.success() {
215            let new = Self::new(&self.name)?;
216            if new.is_installed() {
217                Ok(new)
218            } else {
219                Err(Error::InstallFailed(
220                    "Could not detect new install".to_owned(),
221                ))
222            }
223        } else {
224            test_brew_installed()?;
225            Err(Error::InstallFailed(command.stderr().to_owned()))
226        }
227    }
228
229    /// Check if a package is installed.
230    pub fn is_installed(&self) -> bool {
231        self.installed.len() != 0
232    }
233
234    /// The package options that the package was installed with.
235    pub fn install_options(&self) -> Option<&[String]> {
236        self.installed
237            .first()
238            .map(|i: &Installed| i.used_options.as_slice())
239    }
240
241    /// Uninstalls the package.
242    pub fn uninstall(&self, force: bool, ignore_dependencies: bool) -> Result<Package> {
243        let mut args = vec!["uninstall", &self.name];
244        if force {
245            args.push("--force");
246        }
247        if ignore_dependencies {
248            args.push("--ignore-dependencies");
249        }
250        let command = Single::new("brew")
251            .args(args)
252            .env("HOMEBREW_NO_AUTO_UPDATE", "1")
253            .run()?;
254        brew_return(command, &self.name)
255    }
256
257    /// Pin forumla to prevent automatic updates/upgrades.
258    pub fn pin(&self) -> Result<Package> {
259        if !self.pinned {
260            let command = Single::new("brew")
261                .arg("pin")
262                .arg(&self.name)
263                .env("HOMEBREW_NO_AUTO_UPDATE", "1")
264                .run()?;
265            brew_return(command, &self.name)
266        } else {
267            Ok(self.clone())
268        }
269    }
270
271    /// Unpin formula to allow automatic updates/upgrades.
272    pub fn unpin(&self) -> Result<Package> {
273        if self.pinned {
274            let command = Single::new("brew")
275                .arg("unpin")
276                .arg(&self.name)
277                .env("HOMEBREW_NO_AUTO_UPDATE", "1")
278                .run()?;
279            brew_return(command, &self.name)
280        } else {
281            Ok(self.clone())
282        }
283    }
284
285    /// Upgrade formula.
286    pub fn upgrade(&self) -> Result<Package> {
287        if self.is_installed() {
288            let command = Single::new("brew")
289                .arg("upgrade")
290                .arg(&self.name)
291                .env("HOMEBREW_NO_AUTO_UPDATE", "1")
292                .run()?;
293            brew_return(command, &self.name)
294        } else {
295            Err(Error::NotInstalled)
296        }
297    }
298}
299
300/// Update homebrew, synchronizing the homebrew-core and package list.
301pub fn update() -> Result<()> {
302    let command = Single::new("brew").arg("update").run()?;
303    if command.success() {
304        Ok(())
305    } else {
306        test_brew_installed()?;
307        Err(Error::UnknownError(command.stderr().to_owned()))
308    }
309}
310
311/// Return a map of all installed packages.
312pub fn all_installed() -> Result<HashMap<String, Package>> {
313    packages("--installed")
314}
315
316/// For internal use, wrapper to get package info.
317fn packages(arg: &str) -> Result<HashMap<String, Package>> {
318    let output = Single::new("brew")
319        .arg("info")
320        .arg("--json=v1")
321        .arg(arg)
322        .arg("--analytics")
323        .env("HOMEBREW_NO_AUTO_UPDATE", "1")
324        .run()?;
325    if output.success() {
326        let v: Vec<Package> = serde_json::from_str(output.stdout())?;
327        Ok(v.into_iter().map(|p| (p.name.clone(), p)).collect())
328    } else {
329        test_brew_installed()?;
330        Err(Error::UnknownError(output.stdout().to_string()))
331    }
332}
333
334/// Returns a map of all packages in the downloaded homebrew repository.
335pub fn all_packages() -> Result<HashMap<String, Package>> {
336    packages("--all")
337}
338
339#[derive(Deserialize, Serialize, Clone)]
340pub struct Analytics {
341    pub install: Analytic,
342    pub install_on_request: Analytic,
343    pub build_error: Analytic,
344}
345
346#[derive(Deserialize, Serialize, Clone)]
347pub struct Analytic {
348    #[serde(rename = "30d")]
349    d30: Option<HashMap<String, usize>>,
350    #[serde(rename = "90d")]
351    d90: Option<HashMap<String, usize>>,
352    #[serde(rename = "d365")]
353    d365: Option<HashMap<String, usize>>,
354}
355
356#[derive(Deserialize, Serialize, Clone)]
357pub struct Versions {
358    pub stable: VersionResult,
359    pub devel: Option<VersionResult>,
360    pub head: Option<String>,
361    pub bottle: bool,
362}
363
364#[derive(Deserialize, Serialize, Clone)]
365pub struct Bottle {
366    pub rebuild: usize,
367    pub cellar: String,
368    pub prefix: String,
369    pub root_url: String,
370    pub files: HashMap<String, File>,
371}
372
373#[derive(Deserialize, Serialize, Clone)]
374pub struct File {
375    pub url: String,
376    pub sha256: String,
377}
378
379#[derive(Deserialize, Serialize, Clone)]
380pub struct Url {
381    pub url: String,
382    pub tag: Option<String>,
383    pub revision: Option<NumOrString>,
384}
385
386#[derive(Deserialize, Serialize, Clone)]
387pub struct Installed {
388    pub version: VersionResult,
389    pub used_options: Vec<String>,
390    pub built_as_bottle: bool,
391    pub poured_from_bottle: bool,
392    pub runtime_dependencies: Vec<Dependency>,
393    pub installed_as_dependency: bool,
394    pub installed_on_request: bool,
395}
396
397#[derive(Deserialize, Serialize, Clone)]
398pub struct Dependency {
399    pub full_name: String,
400    pub version: VersionResult,
401}
402
403type VersionResult = Version;
404
405/// Tests weither homebrew is installed by seeing if "brew --version" returns
406/// successfully.
407pub fn test_brew_installed() -> Result<()> {
408    if Single::new("brew")
409        .arg("--version")
410        .env("HOMEBREW_NO_AUTO_UPDATE", "1")
411        .run()
412        .map(|o| o.success())
413        .unwrap_or(false)
414    {
415        Ok(())
416    } else {
417        Err(Error::NotInstalled)
418    }
419}
420
421/// WARNING: untested
422/// installs the homebrew cli in "usr/local" which is it's recomended install location.
423#[allow(dead_code)]
424fn install_homebrew() -> Result<()> {
425    install_homebrew_at("/usr/local")
426}
427
428/// WARNING: untested
429/// TODO: Test this function
430/// installs the homebrew cli in `dir`.
431#[allow(dead_code)]
432fn install_homebrew_at(dir: &str) -> Result<()> {
433    Single::new("mkdir")
434        .arg("homebrew")
435        .and(
436            Single::new("curl")
437                .arg("-L")
438                .arg("https://github.com/Homebrew/brew/tarball/master"),
439        )
440        .pipe(
441            Single::new("tar")
442                .arg("xz")
443                .arg("--strip")
444                .arg("1")
445                .arg("-C")
446                .arg("homebrew"),
447        )
448        .with_dir(dir)
449        .run()?;
450    test_brew_installed()?;
451    Ok(())
452}
453
454/// Represents command line options with which to install a package.
455#[derive(Clone)]
456pub struct Options {
457    env: BuildEnv,
458    ignore_dependencies: bool,
459    only_dependencies: bool,
460    build_from_source: bool,
461    include_test: bool,
462    force_bottle: bool,
463    devel: bool,
464    head: bool,
465    keep_tmp: bool,
466    build_bottle: bool,
467    bottle_arch: bool,
468    force: bool,
469    git: bool,
470    package_options: Vec<String>,
471}
472
473impl Options {
474    /// Represents no options added.
475    pub fn new() -> Self {
476        Self {
477            env: BuildEnv::None,
478            ignore_dependencies: false,
479            only_dependencies: false,
480            build_from_source: false,
481            include_test: false,
482            force_bottle: false,
483            devel: false,
484            head: false,
485            keep_tmp: false,
486            build_bottle: false,
487            bottle_arch: false,
488            force: false,
489            git: false,
490            package_options: Vec::new(),
491        }
492    }
493
494    /// Adds the `--env=std` option.
495    pub fn env_std(mut self) -> Self {
496        self.env = BuildEnv::Std;
497        self
498    }
499
500    /// Adds the `env=super` option.
501    pub fn env_super(mut self) -> Self {
502        self.env = BuildEnv::Super;
503        self
504    }
505
506    /// Adds the `--ignore-dependencies` flag.
507    pub fn ignore_dependencies(mut self) -> Self {
508        self.ignore_dependencies = true;
509        self
510    }
511
512    /// Adds the `--build-from-source` flag.
513    pub fn build_from_source(mut self) -> Self {
514        self.build_from_source = true;
515        self
516    }
517
518    /// Adds the `--include-test` flag.
519    pub fn include_test(mut self) -> Self {
520        self.include_test = true;
521        self
522    }
523
524    /// Adds the `--force-bottle` flag.
525    pub fn force_bottle(mut self) -> Self {
526        self.force_bottle = true;
527        self
528    }
529
530    /// Adds the `--devel` flag.
531    pub fn devel(mut self) -> Self {
532        self.devel = true;
533        self
534    }
535
536    /// Adds the `--HEAD` flag.
537    pub fn head(mut self) -> Self {
538        self.head = true;
539        self
540    }
541
542    /// Adds the `--keep-tmp` flag.
543    pub fn keep_tmp(mut self) -> Self {
544        self.keep_tmp = true;
545        self
546    }
547
548    /// Adds the `--build-bottle` flag.
549    pub fn build_bottle(mut self) -> Self {
550        self.build_bottle = true;
551        self
552    }
553
554    /// Adds the `--bottle-arch` flag.
555    pub fn bottle_arch(mut self) -> Self {
556        self.bottle_arch = true;
557        self
558    }
559
560    /// Adds the `--force` flag.
561    pub fn force(mut self) -> Self {
562        self.force = true;
563        self
564    }
565
566    /// Adds the `--git` flag.
567    pub fn git(mut self) -> Self {
568        self.git = true;
569        self
570    }
571
572    /// Adds a flag for the package to use directly.
573    pub fn option(mut self, opt: &str) -> Self {
574        self.package_options.push(opt.to_string());
575        self
576    }
577
578    /// Adds an multiple flags for the package to use directly.
579    pub fn options<I, S>(mut self, opts: I) -> Self
580    where
581        I: IntoIterator<Item = S>,
582        S: AsRef<str>,
583    {
584        self.package_options
585            .extend(opts.into_iter().map(|s| s.as_ref().to_string()));
586        self
587    }
588
589    fn package_options(&self) -> &Vec<String> {
590        &self.package_options
591    }
592
593    fn brew_options(&self) -> Vec<&str> {
594        let mut out = Vec::new();
595        match self.env {
596            BuildEnv::Std => out.push("--env=std"),
597            BuildEnv::Super => out.push("--env=super"),
598            BuildEnv::None => {}
599        }
600        if self.ignore_dependencies {
601            out.push("--ignore-dependencies")
602        }
603        if self.build_from_source {
604            out.push("--build-from-source")
605        }
606        if self.include_test {
607            out.push("--include-test")
608        }
609        if self.force_bottle {
610            out.push("--force-bottle")
611        }
612        if self.devel {
613            out.push("--devel")
614        }
615        if self.head {
616            out.push("--HEAD")
617        }
618        if self.keep_tmp {
619            out.push("--keep-tmp")
620        }
621        if self.build_bottle {
622            out.push("--build-bottle")
623        }
624        if self.bottle_arch {
625            out.push("--bottle-arch")
626        }
627        if self.force {
628            out.push("--force")
629        }
630        if self.git {
631            out.push("--git")
632        }
633        out
634    }
635}
636
637#[derive(Clone, Copy)]
638pub enum BuildEnv {
639    Std,
640    Super,
641    None,
642}