dotfiles_actions/brew/
action.rs

1// Copyright (c) 2021-2022 Miguel Barreto and others
2//
3// Permission is hereby granted, free of charge, to any person obtaining
4// a copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to
8// permit persons to whom the Software is furnished to do so, subject to
9// the following conditions:
10//
11// The above copyright notice and this permission notice shall be
12// included in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22//! This module contains the [BrewAction] that installs
23//! a brew formula using homebrew
24
25#![cfg(unix)]
26use crate::install_command::InstallCommand;
27use dotfiles_core::action::Action;
28use dotfiles_core::error::DotfilesError;
29use dotfiles_core_macros::ConditionalAction;
30use getset::Getters;
31#[cfg(target_os = "macos")]
32use log::info;
33#[cfg(target_os = "macos")]
34use std::fmt::Display;
35use std::marker::PhantomData;
36use subprocess::Exec;
37#[cfg(target_os = "macos")]
38#[derive(Getters, Eq, PartialEq, Debug, Clone)]
39/// An item to download from the app store
40pub struct MacAppStoreItem {
41  #[getset(get)]
42  /// Numeric ID from the app store
43  id: i64,
44  #[getset(get)]
45  /// Human readable name.
46  name: String,
47}
48
49#[cfg(target_os = "macos")]
50impl From<(i64, String)> for MacAppStoreItem {
51  fn from(value: (i64, String)) -> Self {
52    MacAppStoreItem {
53      id: value.0,
54      name: value.1,
55    }
56  }
57}
58
59#[cfg(target_os = "macos")]
60impl Display for MacAppStoreItem {
61  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62    write!(f, "id: {}, name: {}", self.id, self.name)
63  }
64}
65
66#[cfg(target_os = "macos")]
67#[derive(Eq, PartialEq, Debug, Clone)]
68/// Command to download something from the mac app store
69pub struct MacAppStoreCommand {
70  items: Vec<MacAppStoreItem>,
71  args: Vec<String>,
72}
73
74#[cfg(target_os = "macos")]
75impl From<Vec<MacAppStoreItem>> for MacAppStoreCommand {
76  fn from(items: Vec<MacAppStoreItem>) -> Self {
77    let mut args: Vec<String> = items.iter().map(|it| it.id().to_string()).collect();
78    args.insert(0, "install".into());
79    MacAppStoreCommand { items, args }
80  }
81}
82
83#[cfg(target_os = "macos")]
84impl InstallCommand<MacAppStoreItem> for MacAppStoreCommand {
85  fn base_command(&self) -> Exec {
86    Exec::cmd("mas")
87  }
88
89  fn args(&self) -> &Vec<String> {
90    &self.args
91  }
92
93  fn action_description(&self) -> &str {
94    "Installing from Mac App Store"
95  }
96
97  fn items(&self) -> &Vec<MacAppStoreItem> {
98    &self.items
99  }
100
101  fn action_name(&self) -> &str {
102    "mas"
103  }
104
105  fn execute(&self) -> Result<(), DotfilesError> {
106    let item_list: String = self
107      .items()
108      .iter()
109      .map(|it| format!("{}", it))
110      .collect::<Vec<String>>()
111      .join(", ");
112    info!("{} {}", self.action_description(), item_list);
113    let mut cmd = self.base_command();
114    for arg in self.args().iter() {
115      cmd = cmd.arg(arg);
116    }
117    dotfiles_core::exec_wrapper::execute_commands(
118      vec![cmd],
119      format!("Couldn't {} {}", self.action_name(), item_list).as_str(),
120      format!(
121        "Unexpected error while {} {}",
122        self.action_description(),
123        &item_list
124      )
125      .as_str(),
126    )
127  }
128}
129
130struct BrewCommand {
131  items: Vec<String>,
132  args: Vec<String>,
133  action_name: String,
134  action_description: String,
135}
136
137impl InstallCommand<String> for BrewCommand {
138  fn base_command(&self) -> Exec {
139    Exec::cmd("brew")
140  }
141
142  fn args(&self) -> &Vec<String> {
143    &self.args
144  }
145
146  fn action_description(&self) -> &str {
147    &self.action_description
148  }
149
150  fn items(&self) -> &Vec<String> {
151    &self.items
152  }
153
154  fn action_name(&self) -> &str {
155    &self.action_name
156  }
157}
158impl BrewCommand {
159  fn tap(tap: &str) -> BrewCommand {
160    BrewCommand {
161      items: vec![tap.into()],
162      args: vec!["tap".into(), tap.into()],
163      action_name: "tap".into(),
164      action_description: "tapping".into(),
165    }
166  }
167
168  fn install_formulae(items: &Vec<String>) -> BrewCommand {
169    let mut args: Vec<String> = items.clone();
170    args.insert(0, "install".into());
171    BrewCommand {
172      items: items.clone(),
173      args: args,
174      action_name: "install formula".into(),
175      action_description: "installing formula".into(),
176    }
177  }
178
179  fn install_casks(items: &Vec<String>, force: &bool, adopt: &bool) -> BrewCommand {
180    let mut args = vec!["install".into(), "--cask".into()];
181    if *force {
182      args.push("--force".into())
183    }
184    if *adopt {
185      args.push("--adopt".into())
186    }
187    {
188      let mut items = items.clone();
189      args.append(&mut items)
190    }
191    let args = args;
192    let items = items.clone();
193    BrewCommand {
194      items,
195      args,
196      action_name: "install cask".into(),
197      action_description: "installing cask".into(),
198    }
199  }
200}
201
202/// [BrewAction] Installs software using homebrew.
203#[derive(Eq, PartialEq, Debug, ConditionalAction, Getters)]
204pub struct BrewAction<'a> {
205  /// Skips this action if it is running in a CI environment.
206  #[get = "pub"]
207  skip_in_ci: bool,
208  /// Passes `--force` to `brew install --cask`.
209  #[get = "pub"]
210  force_casks: bool,
211  // Passes `--adopt` to `brew install --cask` to prevent the install failure
212  /// when the app is already installed before the cask install.
213  #[get = "pub"]
214  adopt_casks: bool,
215  /// List of repositories to tap into using `brew tap`.
216  #[get = "pub"]
217  taps: Vec<String>,
218  /// List of brew formulae to `brew install`, usually command line tools.
219  #[get = "pub"]
220  formulae: Vec<String>,
221
222  /// List of casks to install. Casks usually are macOS apps with some sort of UI or framework
223  /// dependencies.
224  #[get = "pub"]
225  casks: Vec<String>,
226
227  #[cfg(target_os = "macos")]
228  /// List of Mac OS apps to install from the App Store
229  #[get = "pub"]
230  mas_apps: Vec<MacAppStoreItem>,
231  phantom_data: PhantomData<&'a String>,
232}
233impl<'a> BrewAction<'a> {
234  /// Constructs a new [BrewAction]
235  pub fn new(
236    skip_in_ci: bool,
237    force_casks: bool,
238    adopt_casks: bool,
239    taps: Vec<String>,
240    formulae: Vec<String>,
241    casks: Vec<String>,
242    #[cfg(target_os = "macos")] mas_apps: Vec<MacAppStoreItem>,
243  ) -> Self {
244    let action = BrewAction {
245      skip_in_ci,
246      force_casks,
247      adopt_casks,
248      taps,
249      formulae,
250      casks,
251      #[cfg(target_os = "macos")]
252      mas_apps,
253      phantom_data: PhantomData,
254    };
255    log::trace!("Creating new {:?}", action);
256    action
257  }
258}
259
260impl Action<'_> for BrewAction<'_> {
261  fn execute(&self) -> Result<(), DotfilesError> {
262    for tap in &self.taps {
263      BrewCommand::tap(tap).execute()?;
264    }
265    if !self.formulae.is_empty() {
266      BrewCommand::install_formulae(&self.formulae).execute()?;
267    }
268    if !self.casks.is_empty() {
269      BrewCommand::install_casks(&self.casks, self.force_casks(), self.adopt_casks()).execute()?;
270    }
271    Ok(())
272  }
273}