1#![cfg(unix)]
26use crate::install_command::InstallCommand;
27use dotfiles_core::error::{DotfilesError, ErrorType};
28use dotfiles_core::settings::{initialize_settings_object, Setting, Settings};
29use dotfiles_core::yaml_util::{
30 fold_hash_until_first_err, get_boolean_setting_from_yaml_or_context,
31 get_optional_string_array_from_yaml_hash, process_value_from_yaml_hash,
32};
33use dotfiles_core::{action::SKIP_IN_CI_SETTING, Action};
34use getset::Getters;
35#[cfg(target_os = "macos")]
36use log::info;
37#[cfg(target_os = "macos")]
38use std::fmt::Display;
39use std::path::Path;
40use strict_yaml_rust::StrictYaml;
41use subprocess::Exec;
42#[cfg(target_os = "macos")]
43#[derive(Getters, Eq, PartialEq, Debug, Clone)]
44pub struct MacAppStoreItem {
46 #[getset(get)]
47 id: i64,
49 #[getset(get)]
50 name: String,
52}
53
54#[cfg(target_os = "macos")]
55impl From<(i64, String)> for MacAppStoreItem {
56 fn from(value: (i64, String)) -> Self {
57 MacAppStoreItem {
58 id: value.0,
59 name: value.1,
60 }
61 }
62}
63
64#[cfg(target_os = "macos")]
65impl Display for MacAppStoreItem {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(f, "id: {}, name: {}", self.id, self.name)
68 }
69}
70
71#[cfg(target_os = "macos")]
72#[derive(Eq, PartialEq, Debug, Clone)]
73pub struct MacAppStoreCommand {
75 items: Vec<MacAppStoreItem>,
76 args: Vec<String>,
77}
78
79#[cfg(target_os = "macos")]
80impl From<Vec<MacAppStoreItem>> for MacAppStoreCommand {
81 fn from(items: Vec<MacAppStoreItem>) -> Self {
82 let mut args: Vec<String> = items.iter().map(|it| it.id().to_string()).collect();
83 args.insert(0, "install".into());
84 MacAppStoreCommand { items, args }
85 }
86}
87
88#[cfg(target_os = "macos")]
89impl InstallCommand<MacAppStoreItem> for MacAppStoreCommand {
90 fn base_command(&self) -> Exec {
91 Exec::cmd("mas")
92 }
93
94 fn args(&self) -> &Vec<String> {
95 &self.args
96 }
97
98 fn action_description(&self) -> &str {
99 "Installing from Mac App Store"
100 }
101
102 fn items(&self) -> &Vec<MacAppStoreItem> {
103 &self.items
104 }
105
106 fn action_name(&self) -> &str {
107 "mas"
108 }
109
110 fn execute(&self) -> Result<(), DotfilesError> {
111 let item_list: String = self
112 .items()
113 .iter()
114 .map(|it| format!("{}", it))
115 .collect::<Vec<String>>()
116 .join(", ");
117 info!("{} {}", self.action_description(), item_list);
118 let mut cmd = self.base_command();
119 for arg in self.args().iter() {
120 cmd = cmd.arg(arg);
121 }
122 dotfiles_core::exec_wrapper::execute_commands(
123 vec![cmd],
124 format!("Couldn't {} {}", self.action_name(), item_list).as_str(),
125 format!(
126 "Unexpected error while {} {}",
127 self.action_description(),
128 item_list
129 )
130 .as_str(),
131 )
132 }
133}
134
135struct BrewCommand {
136 items: Vec<String>,
137 args: Vec<String>,
138 action_name: String,
139 action_description: String,
140}
141
142impl InstallCommand<String> for BrewCommand {
143 fn base_command(&self) -> Exec {
144 Exec::cmd("brew")
145 }
146
147 fn args(&self) -> &Vec<String> {
148 &self.args
149 }
150
151 fn action_description(&self) -> &str {
152 &self.action_description
153 }
154
155 fn items(&self) -> &Vec<String> {
156 &self.items
157 }
158
159 fn action_name(&self) -> &str {
160 &self.action_name
161 }
162}
163impl BrewCommand {
164 fn tap(tap: &str) -> BrewCommand {
165 BrewCommand {
166 items: vec![tap.into()],
167 args: vec!["tap".into(), tap.into()],
168 action_name: "tap".into(),
169 action_description: "tapping".into(),
170 }
171 }
172
173 fn trust(tap: &str) -> BrewCommand {
174 BrewCommand {
175 items: vec![tap.into()],
176 args: vec!["trust".into(), tap.into()],
177 action_name: "trust".into(),
178 action_description: "trusting".into(),
179 }
180 }
181
182 fn install_formulae(items: &[String]) -> BrewCommand {
183 let mut args: Vec<String> = items.to_vec();
184 args.insert(0, "install".into());
185 BrewCommand {
186 items: items.to_vec(),
187 args,
188 action_name: "install formula".into(),
189 action_description: "installing formula".into(),
190 }
191 }
192
193 fn install_casks(items: &[String], force: &bool, adopt: &bool) -> BrewCommand {
194 let mut args = vec!["install".into(), "--cask".into()];
195 if *force {
196 args.push("--force".into())
197 }
198 if *adopt {
199 args.push("--adopt".into())
200 }
201 {
202 let mut items = items.to_vec();
203 args.append(&mut items)
204 }
205 let args = args;
206 let items = items.to_vec();
207 BrewCommand {
208 items,
209 args,
210 action_name: "install cask".into(),
211 action_description: "installing cask".into(),
212 }
213 }
214}
215
216pub const FORCE_CASKS_SETTING: &str = "force_casks";
218pub const ADOPT_CASKS_SETTING: &str = "adopt_casks";
220pub const AUTO_TRUST_TAPS_SETTING: &str = "auto_trust_taps";
222
223pub const TAP_SETTING: &str = "tap";
225pub const FORMULA_SETTING: &str = "formula";
227pub const CASK_SETTING: &str = "cask";
229
230pub fn default_settings() -> Settings {
232 initialize_settings_object(&[
233 (FORCE_CASKS_SETTING.to_owned(), Setting::Boolean(false)),
234 (ADOPT_CASKS_SETTING.to_owned(), Setting::Boolean(false)),
235 (AUTO_TRUST_TAPS_SETTING.to_owned(), Setting::Boolean(false)),
236 (SKIP_IN_CI_SETTING.to_owned(), Setting::Boolean(false)),
237 ])
238}
239
240#[derive(Eq, PartialEq, Debug, Getters)]
242pub struct BrewAction {
243 #[get = "pub"]
245 skip_in_ci: bool,
246 #[get = "pub"]
248 force_casks: bool,
249 #[get = "pub"]
252 adopt_casks: bool,
253 #[get = "pub"]
255 auto_trust_taps: bool,
256 #[get = "pub"]
258 taps: Vec<String>,
259 #[get = "pub"]
261 formulae: Vec<String>,
262
263 #[get = "pub"]
266 casks: Vec<String>,
267
268 #[cfg(target_os = "macos")]
269 #[get = "pub"]
271 mas_apps: Vec<MacAppStoreItem>,
272}
273impl BrewAction {
274 #[allow(clippy::too_many_arguments)]
276 pub fn new(
277 skip_in_ci: bool,
278 force_casks: bool,
279 adopt_casks: bool,
280 auto_trust_taps: bool,
281 taps: Vec<String>,
282 formulae: Vec<String>,
283 casks: Vec<String>,
284 #[cfg(target_os = "macos")] mas_apps: Vec<MacAppStoreItem>,
285 ) -> Self {
286 let action = BrewAction {
287 skip_in_ci,
288 force_casks,
289 adopt_casks,
290 auto_trust_taps,
291 taps,
292 formulae,
293 casks,
294 #[cfg(target_os = "macos")]
295 mas_apps,
296 };
297 log::trace!("Creating new {:?}", action);
298 action
299 }
300}
301
302impl Action for BrewAction {
303 fn execute(&self) -> Result<(), DotfilesError> {
304 for tap in &self.taps {
305 if self.auto_trust_taps {
306 BrewCommand::trust(tap).execute()?;
307 }
308 BrewCommand::tap(tap).execute()?;
309 }
310 if !self.formulae.is_empty() {
311 BrewCommand::install_formulae(&self.formulae).execute()?;
312 }
313 if !self.casks.is_empty() {
314 BrewCommand::install_casks(&self.casks, self.force_casks(), self.adopt_casks()).execute()?;
315 }
316 #[cfg(target_os = "macos")]
317 if !self.mas_apps.is_empty() {
318 MacAppStoreCommand::from(self.mas_apps.clone()).execute()?;
319 }
320 Ok(())
321 }
322
323 fn skip_in_ci(&self) -> bool {
324 self.skip_in_ci
325 }
326}
327
328pub fn parse_action_list(
330 context_settings: &Settings,
331 yaml: &StrictYaml,
332 _current_dir: &Path,
333) -> Result<Vec<BrewAction>, DotfilesError> {
334 let defaults = default_settings();
335 let force_casks = get_boolean_setting_from_yaml_or_context(
336 FORCE_CASKS_SETTING,
337 yaml,
338 context_settings,
339 &defaults,
340 )?;
341 let adopt_casks = get_boolean_setting_from_yaml_or_context(
342 ADOPT_CASKS_SETTING,
343 yaml,
344 context_settings,
345 &defaults,
346 )?;
347 let auto_trust_taps = get_boolean_setting_from_yaml_or_context(
348 AUTO_TRUST_TAPS_SETTING,
349 yaml,
350 context_settings,
351 &defaults,
352 )?;
353 let skip_in_ci = get_boolean_setting_from_yaml_or_context(
354 SKIP_IN_CI_SETTING,
355 yaml,
356 context_settings,
357 &defaults,
358 )?;
359 let taps = get_optional_string_array_from_yaml_hash(TAP_SETTING, yaml)?;
360 let formulae = get_optional_string_array_from_yaml_hash(FORMULA_SETTING, yaml)?;
361 let casks = get_optional_string_array_from_yaml_hash(CASK_SETTING, yaml)?;
362 #[cfg(target_os = "macos")]
363 let mas_apps = process_value_from_yaml_hash("mas", yaml, |mas_yaml| {
364 fold_hash_until_first_err(
365 mas_yaml,
366 Ok(Vec::<MacAppStoreItem>::new()),
367 |key, val| {
368 Ok((
369 val
370 .to_owned()
371 .into_string()
372 .ok_or(DotfilesError::from_wrong_yaml(
373 "Mac App Store app ID is not a string as expected".into(),
374 val.to_owned(),
375 StrictYaml::String("".into()),
376 ))
377 .and_then(|id| {
378 id.parse::<i64>().map_err(|_| {
379 DotfilesError::from(
380 format!("{id} is not a valid Mac App Store app id"),
381 ErrorType::InconsistentConfigurationError,
382 )
383 })
384 })?,
385 key,
386 ))
387 },
388 |mut list, item| {
389 list.push(MacAppStoreItem::from(item));
390 Ok(list)
391 },
392 )
393 })
394 .map_or_else(
395 |err| {
396 if err.is_missing_config("mas") {
397 Ok(Vec::new())
398 } else {
399 Err(err)
400 }
401 },
402 Ok,
403 )?;
404
405 Ok(vec![BrewAction::new(
406 skip_in_ci,
407 force_casks,
408 adopt_casks,
409 auto_trust_taps,
410 taps,
411 formulae,
412 casks,
413 #[cfg(target_os = "macos")]
414 mas_apps,
415 )])
416}