hm/
mod.rs

1//! `hm` is a commandline program to help with dotfile (and more) management.
2//!
3//! It can handle putting configuration files where they should go, but its real
4//! strength lies in the solutions it'll execute - shell scripts, usually - to
5//! pull a git repository, compile something from source, etc.
6//!
7//! `hm` exists because I bring along a few utilities to all Linux boxes I regularly
8//! use, and those are often built from source. So rather than manually install
9//! all dependency libraries, then build each dependent piece, then finally the
10//! top-level dependent program, I built hm.
11//! It also provides functionality to thread off heavier operations
12//! into task threads, which regularly report back their status with
13//! Worker objects over a `std::sync::mpsc`.
14//!
15//! `hm` can also perform dependency resolution, thanks to the solvent crate. You can
16//! provide a big ol' list of tasks to complete, each with their own dependencies, and
17//! as long as you have a solveable dependency graph, you can get workable batches from
18//! get_task_batches(). They will be in *some* order that will resolve your dependencies,
19//! but that order is non-deterministic - if there's multiple ways to solve a graph,
20//! you'll probably get all those different ways back if you run it multiple times.
21//!
22//! The crate provides this library, which is in turn used by the bin `hm` (in `src/bin/main.rs`).
23//! `hm` is a commandline program to help with dotfile (and more) management.
24//!
25//! [gfycat of it in action](https://gfycat.com/skinnywarmheartedafricanpiedkingfisher)
26//!
27//!
28//!  1. create a config.toml file either anywhere or in ~/.config/homemaker/.
29//!  2. enter things to do things to in the file.
30//!  example:
31//!  ``` text
32//!  ## config.toml
33//!
34//!  [[obj]]
35//!  file = 'tmux.conf'
36//!  source = '~/dotfiles/.tmux.conf'
37//!  destination = '~/.tmux.conf'
38//!  method = 'symlink'
39//!
40//!  [[obj]]
41//!  task = 'zt'
42//!  solution = 'cd ~/dotfiles/zt && git pull'
43//!  dependencies = 'maim, slop'
44//!
45//!  [[obj]]
46//!  task = 'slop'
47//!  source = '~/dotfiles/zt/slop'
48//!  solution = 'cd ~/dotfiles/zt/slop; make clean; cmake -DCMAKE_INSTALL_PREFIX="/usr" ./ && make && sudo make install'
49//!  method = 'execute'
50//!  platform = "linux::debian"
51//!  ```
52//!  3. `hm ~/path/to/your/config.toml`
53//!
54//!  [![built with spacemacs](https://cdn.rawgit.com/syl20bnr/spacemacs/442d025779da2f62fc86c2082703697714db6514/assets/spacemacs-badge.svg)](http://spacemacs.org) and neovim
55//!
56//!  thanks to actual good code:
57//!  serde
58//!  toml
59//!  symlink
60//!  solvent
61//!  indicatif
62//!  console
63#![allow(clippy::many_single_char_names)]
64#![allow(dead_code)]
65#![allow(unused_macros)]
66extern crate console;
67extern crate indicatif;
68extern crate log;
69extern crate shellexpand;
70extern crate simplelog;
71extern crate solvent;
72extern crate symlink;
73extern crate sys_info;
74
75pub mod config;
76mod hm_macro;
77pub mod hmerror;
78
79use config::{ManagedObject, Worker};
80use hmerror::{ErrorKind as hmek, HMError};
81
82use console::{pad_str, style, Alignment};
83use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
84use log::{info, warn};
85use solvent::DepGraph;
86use std::{
87  collections::{HashMap, HashSet},
88  fmt,
89  fs::{copy, create_dir_all, metadata, remove_dir_all, remove_file},
90  io::{BufRead, BufReader, Error, ErrorKind},
91  path::Path,
92  process::{exit, Command, Stdio},
93  sync::mpsc::{self, Sender},
94  {thread, time},
95};
96use symlink::{symlink_dir as sd, symlink_file as sf};
97
98/// I just wanna borrow one to look at it for a minute.
99/// use me with std::mem::transmute()
100/// absolutely not pub struct. don't look at me.
101#[derive(Debug, Clone)]
102struct SneakyDepGraphImposter<String> {
103  nodes: Vec<String>,
104  dependencies: HashMap<usize, HashSet<usize>>,
105  satisfied: HashSet<usize>,
106}
107
108impl fmt::Display for SneakyDepGraphImposter<String> {
109  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
110    let mut i: usize = 0;
111    for n in self.nodes.clone() {
112      if self.dependencies.get(&i).is_some() {
113        let _ = write!(f, "[ {} -> ", n);
114        #[allow(clippy::for_loops_over_fallibles)]
115        for d in self.dependencies.get(&i) {
116          if d.is_empty() {
117            write!(f, "<no deps> ")?;
118          }
119          let mut j: usize = 1;
120          for m in d {
121            if j == d.len() {
122              write!(f, "{} ", self.nodes[*m])?;
123            } else {
124              write!(f, "{}, ", self.nodes[*m])?;
125            }
126            j += 1;
127          }
128        }
129      }
130      i += 1;
131      if i != self.nodes.len() {
132        write!(f, "], ")?;
133      } else {
134        write!(f, "]")?;
135      }
136    }
137    Ok(())
138  }
139}
140
141///
142/// Copy our {file|directory} to the destination. Generally
143/// we'll be doing this in a tilde'd home subdirectory, so
144/// we need to be careful to get our Path right.
145///
146pub fn copy_item(source: String, target: String, force: bool) -> Result<(), HMError> {
147  let _lsource: String = shellexpand::tilde(&source).to_string();
148  let _ltarget: String = shellexpand::tilde(&target).to_string();
149  let md = match metadata(_lsource.clone()) {
150    Ok(a) => a,
151    Err(e) => return Err(HMError::Io(e)),
152  };
153  if force && Path::new(_ltarget.as_str()).exists() {
154    if md.is_dir() {
155      remove_dir_all(_ltarget.clone())?;
156    } else {
157      remove_file(_ltarget.clone())?;
158    }
159  }
160  copy(Path::new(_lsource.as_str()), Path::new(_ltarget.as_str()))?;
161  Ok(())
162}
163
164///
165/// Either create a symlink to a file or directory. Generally
166/// we'll be doing this in a tilde'd home subdirectory, so
167/// we need to be careful to get our Path right.
168///
169pub fn symlink_item(source: String, target: String, force: bool) -> Result<(), HMError> {
170  let _lsource: String = shellexpand::tilde(&source).to_string();
171  let _ltarget: String = shellexpand::tilde(&target).to_string();
172  let md = match metadata(_lsource.clone()) {
173    Ok(a) => a,
174    Err(e) => return Err(HMError::Io(e)),
175  };
176  let lmd = metadata(_ltarget.clone());
177  if force && Path::new(_ltarget.as_str()).exists() {
178    // this is reasonably safe because if the target exists our lmd is a Result not an Err
179    if lmd.unwrap().is_dir() {
180      remove_dir_all(_ltarget.clone())?;
181    } else {
182      remove_file(_ltarget.clone())?;
183    }
184  }
185  // create all the parent directories required for the target file/directory
186  create_dir_all(Path::new(_ltarget.as_str()).parent().unwrap())?;
187  if md.is_dir() {
188    sd(Path::new(_lsource.as_str()), Path::new(_ltarget.as_str()))?;
189  } else if md.is_file() {
190    sf(Path::new(_lsource.as_str()), Path::new(_ltarget.as_str()))?;
191  }
192  Ok(())
193}
194
195///
196/// Take a ManagedObject task, an mpsc tx, and a Progressbar. Execute task and regularly inform the rx
197/// (all the way over back in `main()`)about our status using config::Worker.
198///
199/// -TODO-: allow the `verbose` bool to show the output of the tasks as they go.
200/// Hey, it's done! Writes out to the logs/ directory.
201///
202/// Return () or io::Error (something went wrong in our task).
203///
204pub fn send_tasks_off_to_college(
205  mo: &ManagedObject,
206  tx: &Sender<Worker>,
207  p: ProgressBar,
208) -> Result<(), Error> {
209  let s: String = mo.solution.clone();
210  let s1: String = mo.solution.clone();
211  let n: String = mo.name.clone();
212  let tx1: Sender<Worker> = Sender::clone(tx);
213  let _: thread::JoinHandle<Result<(), HMError>> = thread::spawn(move || {
214    let mut c = Command::new("bash")
215      .arg("-c")
216      .arg(s)
217      .stdout(Stdio::piped())
218      .stderr(Stdio::piped())
219      .spawn()
220      .unwrap();
221    let output: std::process::ChildStdout = c.stdout.take().unwrap();
222    let reader: BufReader<std::process::ChildStdout> = BufReader::new(output);
223    // run this in another thread or the little block of tasks draws line-by-line
224    // instead of all at once then updating as the task info gets Worker'd back
225    thread::spawn(|| {
226      reader
227        .lines()
228        .filter_map(|line| line.ok())
229        .for_each(|line| info!("{}", line));
230    });
231    p.set_style(
232      ProgressStyle::default_spinner()
233        .template("[{elapsed:4}] {prefix:.bold.dim} {spinner} {wide_msg}"),
234    );
235    p.enable_steady_tick(200);
236    let x = pad_str(format!("task {}", n).as_str(), 30, Alignment::Left, None).into_owned();
237    p.set_prefix(x);
238    loop {
239      let mut w: Worker = Worker {
240        name: n.clone(),
241        status: None,
242        completed: false,
243      };
244      match c.try_wait() {
245        // we check each child status...
246        Ok(Some(status)) => {
247          if status.success() {
248            // if we're done, send back :thumbsup:
249            p.finish_with_message(console::style("✓").green().to_string());
250            w.status = status.code();
251            w.completed = true;
252            tx1.send(w).unwrap();
253            info!("Successfully completed {}.", n);
254            return Ok(());
255          } else {
256            // or :sadface:
257            drop(tx1);
258            warn!("Error within `{}`", s1);
259            p.abandon_with_message(console::style("✗").red().to_string());
260            return Err(HMError::Regular(hmek::SolutionError { solution: s1 }));
261          }
262        } // it's sent back nothing, not error, but not done
263        Ok(None) => {
264          tx1.send(w).unwrap();
265          thread::sleep(time::Duration::from_millis(200));
266        }
267        Err(_e) => {
268          // ahh send back err!
269          drop(tx1);
270          p.abandon_with_message(console::style("✗").red().to_string());
271          return Err(HMError::Regular(hmek::SolutionError { solution: s1 }));
272        }
273      }
274    }
275  });
276  Ok(())
277}
278
279/*
280*/
281///
282/// Create a non-cyclical dependency graph and give it back as a Vec&lt;Vec&lt;ManagedObject&gt;&gt;.
283/// Will return a CyclicalDependencyError if the graph is unsolveable.
284/// Intended to be used with either mgmt::execute_solution or mgmt::send_tasks_off_to_college.
285///
286///
287/// Example:
288///
289/// ```
290/// extern crate indicatif;
291/// use std::sync::mpsc;
292/// use std::collections::HashMap;
293/// use indicatif::{MultiProgress, ProgressBar};
294/// use hm::config::ManagedObject;
295/// use hm::{get_task_batches, send_tasks_off_to_college};
296/// let nodes: HashMap<String, ManagedObject> = HashMap::new();
297/// let (tx, rx) = mpsc::channel();
298/// let mp: MultiProgress = MultiProgress::new();
299/// let v: Vec<Vec<ManagedObject>> = get_task_batches(nodes, None).unwrap();
300/// for a in v {
301///   for b in a {
302///     let _p: ProgressBar = mp.add(ProgressBar::new_spinner());
303///     send_tasks_off_to_college(&b, &tx, _p);
304///   }
305/// }
306/// ```
307///
308pub fn get_task_batches(
309  mut nodes: HashMap<String, ManagedObject>,
310  target_task: Option<String>,
311) -> Result<Vec<Vec<ManagedObject>>, HMError> {
312  let our_os = config::determine_os();
313  let mut depgraph: DepGraph<String> = DepGraph::new();
314  let mut nodes_to_remove: Vec<String> = Vec::new();
315  let mut wrong_platforms: HashMap<String, config::OS> = HashMap::new();
316  for (name, node) in &nodes {
317    if node.os.is_none() || node.os.clone().unwrap() == our_os {
318      depgraph.register_dependencies(name.to_owned(), node.dependencies.clone());
319    } else {
320      nodes_to_remove.push(name.to_string());
321      wrong_platforms.insert(name.clone(), node.os.clone().unwrap());
322    }
323  }
324  for n in nodes_to_remove {
325    nodes.remove(&n);
326  }
327  let mut tasks: Vec<Vec<ManagedObject>> = Vec::new();
328  let mut _dedup: HashSet<String> = HashSet::new();
329  /*
330    ok. we've got a target task. we can get the depgraph for ONLY that task,
331    and don't need to go through our entire config to solve for each.
332    just the subtree involved in our target.
333  */
334  if let Some(tt_name) = target_task {
335    // TODO: break out getting our tasklist into its own function
336    // de-dup me!
337    let mut qtdg: Vec<ManagedObject> = Vec::new();
338    let tdg: solvent::DepGraphIterator<String> = match depgraph.dependencies_of(&tt_name) {
339      Ok(i) => i,
340      Err(_) => {
341        return Err(HMError::Regular(hmek::DependencyUndefinedError {
342          dependency: tt_name,
343        }));
344      }
345    };
346    for n in tdg {
347      match n {
348        Ok(r) => {
349          let mut a = match nodes.get(r) {
350            Some(a) => a,
351            None => {
352              /*
353              if we have a dependency, but it can't be solved because it's for the incorrect platform,
354              let's complain about it.
355              doing it this way is necessary because we DO still want our dependency graph to get run.
356              */
357              if wrong_platforms.contains_key(r) {
358                return Err(HMError::Regular(hmek::IncorrectPlatformError {
359                  dependency: String::from(r),
360                  platform: our_os,
361                  target_platform: wrong_platforms.get(r).cloned().unwrap(),
362                }));
363              } else {
364                return Err(HMError::Regular(hmek::DependencyUndefinedError {
365                  dependency: String::from(r),
366                }));
367              }
368            }
369          }
370          .to_owned();
371          a.set_satisfied();
372          qtdg.push(a);
373        }
374        Err(_e) => unsafe {
375          // we'll just borrow this for a second
376          // just to look
377          // i'm not gonna touch it i promise
378          let my_sneaky_depgraph: SneakyDepGraphImposter<String> = std::mem::transmute(depgraph);
379          return Err(HMError::Regular(hmek::CyclicalDependencyError {
380            // we can do this because we've implemented fmt::Display for SneakyDepGraphImposter
381            dependency_graph: my_sneaky_depgraph.to_string(),
382          }));
383        },
384      }
385    }
386    tasks.push(qtdg);
387  } else {
388    // not doing target task
389    // we're doing the entire config
390    for name in nodes.keys() {
391      let mut q: Vec<ManagedObject> = Vec::new();
392      let dg: solvent::DepGraphIterator<String> = depgraph.dependencies_of(name).unwrap();
393      for n in dg {
394        match n {
395          Ok(r) => {
396            let c = String::from(r.as_str());
397            // returns true if the set DID NOT have c in it already
398            if _dedup.insert(c) {
399              let mut a = match nodes.get(r) {
400                Some(a) => a,
401                None => {
402                  /*
403                  if we have a dependency, but it can't be solved because it's for the incorrect platform,
404                  let's complain about it.
405                  doing it this way is necessary because we DO still want our dependency graph to get run.
406                  */
407                  if wrong_platforms.contains_key(r) {
408                    return Err(HMError::Regular(hmek::IncorrectPlatformError {
409                      dependency: String::from(r),
410                      platform: our_os,
411                      target_platform: wrong_platforms.get(r).cloned().unwrap(),
412                    }));
413                  } else {
414                    return Err(HMError::Regular(hmek::DependencyUndefinedError {
415                      dependency: String::from(r),
416                    }));
417                  }
418                }
419              }
420              .to_owned();
421              a.set_satisfied();
422              q.push(a);
423            }
424          }
425          Err(_e) => unsafe {
426            // we'll just borrow this for a second
427            // just to look
428            // i'm not gonna touch it i promise
429            let my_sneaky_depgraph: SneakyDepGraphImposter<String> = std::mem::transmute(depgraph);
430            return Err(HMError::Regular(hmek::CyclicalDependencyError {
431              // we can do this because we've implemented fmt::Display for SneakyDepGraphImposter
432              dependency_graph: my_sneaky_depgraph.to_string(),
433            }));
434          },
435        }
436      }
437      tasks.push(q);
438    }
439  }
440  Ok(tasks)
441}
442
443///
444/// Execute the shell commands specified in the MO's solution in a thread, so as not to block.
445///
446fn execute_solution(solution: String) -> Result<(), HMError> {
447  // marginally adapted but mostly stolen from
448  // https://rust-lang-nursery.github.io/rust-cookbook/os/external.html
449
450  let child: thread::JoinHandle<Result<(), HMError>> = thread::spawn(|| {
451    let output = Command::new("bash")
452      .arg("-c")
453      .arg(solution)
454      .stdout(Stdio::piped())
455      .spawn()?
456      .stdout
457      .ok_or_else(|| Error::new(ErrorKind::Other, "Couldn't capture stdout"))?;
458    if cfg!(debug_assertions) {
459      let reader = BufReader::new(output);
460      reader
461        .lines()
462        .filter_map(|line| line.ok())
463        .for_each(|line| println!("{}", line));
464    }
465    Ok(())
466  });
467  child.join().unwrap()
468}
469
470///
471/// Pretty simple.
472/// Hand off to the actual function that does the work.
473///
474pub fn perform_operation_on(mo: ManagedObject) -> Result<(), HMError> {
475  let _s = mo.method.as_str();
476  match _s {
477    "symlink" => {
478      let source: String = mo.source;
479      let destination: String = mo.destination;
480      symlink_item(source, destination, mo.force)
481    }
482    "copy" => {
483      let source: String = mo.source;
484      let destination: String = mo.destination;
485      copy_item(source, destination, mo.force)
486    }
487    _ => {
488      println!("{}", style(_s.to_string()).red());
489      Ok(())
490    }
491  }
492}
493
494///
495/// Take our list of ManagedObjects to do stuff to, and determine
496/// if they're simple or complex (simple is symlink or copy, complex
497/// maybe compilation or pulling a git repo). We just do the simple ones, as they
498/// won't be computationally expensive.
499///
500/// For complex ones we get a list of list of MOs that we can do in some order that
501/// satisfies their dependencies, then we hand them off to send_tasks_off_to_college().
502///
503pub fn do_tasks(
504  a: HashMap<String, config::ManagedObject>,
505  target_task: Option<String>,
506) -> Result<(), HMError> {
507  let mut complex_operations = a.clone();
508  let mut simple_operations = a;
509  complex_operations.retain(|_, v| v.is_task()); // all the things that aren't just symlink/copy
510  simple_operations.retain(|_, v| !v.is_task()); // all the things that are quick (don't need to thread off)
511  if target_task.is_some() {
512    let tt_name = target_task.clone().unwrap();
513    // only keep the target task, if it's in here...
514    // we can't do this with complex tasks, because the target may have deps we want
515    // we'll handle that later in get_task_batches
516    simple_operations.retain(|_, v| v.name == tt_name);
517  }
518  for (_name, _mo) in simple_operations.into_iter() {
519    // lol postmaclone
520    let p = _mo.post.clone();
521    let a = perform_operation_on(_mo).map_err(|e| {
522      hmerror::error(
523        format!("Failed to perform operation on {:#?}", _name).as_str(),
524        e.to_string().as_str(),
525      )
526    });
527    if a.is_ok() {
528      hmerror::happy_print(format!("Successfully performed operation on {:#?}", _name).as_str());
529      if !p.is_empty() {
530        println!("↳ Executing post {} for {}... ", p, _name);
531        let _ = execute_solution(p);
532      }
533    }
534  }
535  let (tx, rx) = mpsc::channel();
536  let mp: MultiProgress = MultiProgress::new();
537  let mut t: HashSet<String> = HashSet::new();
538  let _v = get_task_batches(complex_operations, target_task).unwrap_or_else(|er| {
539    hmerror::error(
540      "Error occurred attempting to get task batches",
541      format!("{}{}", "\n", er.to_string().as_str()).as_str(),
542    );
543    exit(3);
544  });
545  for _a in _v {
546    for _b in _a {
547      t.insert(_b.name.to_string());
548      let _p: ProgressBar = mp.add(ProgressBar::new_spinner());
549      send_tasks_off_to_college(&_b, &tx, _p).expect("ohtehnoes");
550    }
551  }
552
553  let mut v: HashMap<String, config::Worker> = HashMap::new();
554
555  // create a map of each Worker, and just poll them until they're done
556  // the hashmap ensures we have only one of each worker
557  loop {
558    if let Ok(_t) = rx.try_recv() {
559      v.insert(_t.name.clone(), _t.clone());
560    }
561    std::thread::sleep(time::Duration::from_millis(10));
562    if !all_workers_done(&v) {
563      continue;
564    }
565    break;
566  }
567  mp.join().unwrap();
568  Ok(())
569}
570
571///
572/// Iterate through all the workers passed in. If any isn't marked as complete, `return false;`.
573/// Let's take a reference, because we're only reading, and don't need ownership.
574///
575fn all_workers_done(workers: &HashMap<String, config::Worker>) -> bool {
576  for w in workers.values() {
577    if !w.completed {
578      return false;
579    }
580  }
581  true
582}