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#[derive(Serialize, Deserialize, Clone)]
47#[serde(transparent)]
48pub struct Version {
49 original: String,
50}
51
52impl Version {
53 pub fn parse(&self) -> Option<version_rs::Version> {
55 version_rs::Version::from_str(&self.original).ok()
56 }
57
58 pub fn original(&self) -> &str {
60 &self.original
61 }
62}
63
64#[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 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 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 pub fn is_installed(&self) -> bool {
231 self.installed.len() != 0
232 }
233
234 pub fn install_options(&self) -> Option<&[String]> {
236 self.installed
237 .first()
238 .map(|i: &Installed| i.used_options.as_slice())
239 }
240
241 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 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 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 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
300pub 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
311pub fn all_installed() -> Result<HashMap<String, Package>> {
313 packages("--installed")
314}
315
316fn 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
334pub 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
405pub 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#[allow(dead_code)]
424fn install_homebrew() -> Result<()> {
425 install_homebrew_at("/usr/local")
426}
427
428#[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#[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 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 pub fn env_std(mut self) -> Self {
496 self.env = BuildEnv::Std;
497 self
498 }
499
500 pub fn env_super(mut self) -> Self {
502 self.env = BuildEnv::Super;
503 self
504 }
505
506 pub fn ignore_dependencies(mut self) -> Self {
508 self.ignore_dependencies = true;
509 self
510 }
511
512 pub fn build_from_source(mut self) -> Self {
514 self.build_from_source = true;
515 self
516 }
517
518 pub fn include_test(mut self) -> Self {
520 self.include_test = true;
521 self
522 }
523
524 pub fn force_bottle(mut self) -> Self {
526 self.force_bottle = true;
527 self
528 }
529
530 pub fn devel(mut self) -> Self {
532 self.devel = true;
533 self
534 }
535
536 pub fn head(mut self) -> Self {
538 self.head = true;
539 self
540 }
541
542 pub fn keep_tmp(mut self) -> Self {
544 self.keep_tmp = true;
545 self
546 }
547
548 pub fn build_bottle(mut self) -> Self {
550 self.build_bottle = true;
551 self
552 }
553
554 pub fn bottle_arch(mut self) -> Self {
556 self.bottle_arch = true;
557 self
558 }
559
560 pub fn force(mut self) -> Self {
562 self.force = true;
563 self
564 }
565
566 pub fn git(mut self) -> Self {
568 self.git = true;
569 self
570 }
571
572 pub fn option(mut self, opt: &str) -> Self {
574 self.package_options.push(opt.to_string());
575 self
576 }
577
578 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}