aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.com>2023-07-20 22:04:45 +0100
committerLeonardo Bishop <me@leonardobishop.com>2023-07-20 22:15:25 +0100
commitc3a55debe9e4194f83164d412293f27b797627af (patch)
tree027ca943a051bf51c809eec0f590d598df2db9a3 /src
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/cli.rs8
-rw-r--r--src/config.rs93
-rw-r--r--src/install.rs49
-rw-r--r--src/main.rs45
-rw-r--r--src/util.rs34
5 files changed, 229 insertions, 0 deletions
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..dc53357
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,8 @@
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+pub struct Args {
+ #[arg(short, long)]
+ pub file: String,
+} \ No newline at end of file
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..2a0e38b
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,93 @@
+use std::{fs::File, path::PathBuf};
+use std::fmt;
+use std::error::Error;
+
+use serde::{Serialize, Deserialize};
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CopyPath {
+ pub from: String,
+ pub to: String,
+ pub recursive: bool,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub enum Step {
+ Link(CopyPath),
+ Copy(CopyPath),
+ Shell(String),
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Stage {
+ pub name: Option<String>,
+ pub steps: Option<Vec<Step>>,
+ pub from_file: Option<String>,
+
+ #[serde(skip_deserializing)]
+ pub base_path: PathBuf,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Config {
+ pub stages: Vec<Stage>,
+
+ #[serde(skip_deserializing)]
+ pub base_path: PathBuf,
+}
+
+#[derive(Debug)]
+struct StageFileError {
+ stage_file: String,
+ cause: Box<dyn std::error::Error>,
+}
+
+impl Error for StageFileError {}
+
+impl fmt::Display for StageFileError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let stage_file = &self.stage_file;
+ let cause = &self.cause.to_string();
+
+ write!(f, "Failed to load stage from file '{stage_file}': {cause}")
+ }
+}
+
+impl Config {
+ fn resolve_stage_file(base_path: PathBuf, file_name: &str) -> Result<Stage, Box<dyn std::error::Error>> {
+ let mut file_path = base_path.clone();
+ file_path.push(file_name);
+ let file = File::open(&file_path)?;
+ let stage: Stage = serde_yaml::from_reader(file)?;
+ let parent_path = file_path.parent().unwrap();
+
+ Ok(Stage { base_path: parent_path.to_path_buf(), ..stage })
+ }
+
+ pub fn from_file(file_name: &str) -> Result<Self, Box<dyn std::error::Error>> {
+ let base_path = std::env::current_dir()?;
+ let mut file_path = base_path.clone();
+ file_path.push(file_name);
+
+ let file = File::open(&file_path)?;
+ let loaded_config: Config = serde_yaml::from_reader(file)?;
+
+ let mut stages: Vec<Stage> = Vec::new();
+
+ for stage in loaded_config.stages {
+ if let Some(file_name) = stage.from_file {
+ match Self::resolve_stage_file(base_path.clone(), &file_name) {
+ Ok(loaded_stage) => stages.push(loaded_stage),
+ Err(err) => return Err(Box::new(StageFileError {
+ stage_file: file_name,
+ cause: err
+ }))
+ }
+ } else {
+ stages.push(Stage { base_path: base_path.clone(), ..stage });
+ }
+ }
+
+ Ok(Config { stages, base_path: base_path })
+ }
+} \ No newline at end of file
diff --git a/src/install.rs b/src/install.rs
new file mode 100644
index 0000000..57da431
--- /dev/null
+++ b/src/install.rs
@@ -0,0 +1,49 @@
+use crate::config::{Step, CopyPath};
+use std::{fs, path::PathBuf};
+use crate::util::expand_home;
+
+fn resolve_paths_and_mkdir(paths: &CopyPath, base_path: &PathBuf) -> Result<(PathBuf, PathBuf), Box<dyn std::error::Error>> {
+ let expanded_home = &expand_home(&paths.to);
+ let destination = PathBuf::from(expanded_home);
+ let mut source = base_path.clone();
+ source.push(&paths.from);
+
+ let dest_parent = destination.parent().unwrap();
+ fs::create_dir_all(dest_parent)?;
+ Ok((source, destination))
+}
+
+fn ln(paths: &CopyPath, base_path: &PathBuf) -> Result<bool, Box<dyn std::error::Error>> {
+ let (source, destination) = resolve_paths_and_mkdir(paths, base_path)?;
+ let source = source.as_path();
+ let destination = destination.as_path();
+
+ let _ = fs::remove_file(destination);
+ fs::hard_link(source, destination)?;
+ Ok(true)
+}
+
+fn cp(paths: &CopyPath, base_path: &PathBuf) -> Result<bool, Box<dyn std::error::Error>> {
+ let (source, destination) = resolve_paths_and_mkdir(paths, base_path)?;
+ let source = source.as_path();
+ let destination = destination.as_path();
+
+ if fs::metadata(destination).is_ok() {
+ return Ok(false);
+ }
+ fs::copy(source, destination)?;
+ Ok(true)
+}
+
+fn run_shell(command: &String) -> Result<bool, Box<dyn std::error::Error>> {
+ let exit = std::process::Command::new("/bin/sh").arg("-c").arg(command).output()?;
+ return Ok(exit.status.success());
+}
+
+pub fn run_step(step: &Step, base_path: &PathBuf) -> Result<bool, Box<dyn std::error::Error>> {
+ match step {
+ Step::Link(path) => { ln(path, base_path) },
+ Step::Copy(path) => { cp(path, base_path) },
+ Step::Shell(command) => run_shell(command),
+ }
+} \ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..08a4f51
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,45 @@
+mod config;
+mod install;
+mod util;
+mod cli;
+
+use clap::Parser;
+use colored::*;
+use std::process;
+use config::Config;
+use util::*;
+
+fn main() {
+ let args = cli::Args::parse();
+ let install_profile = &args.file;
+
+ let loaded_config: Config = Config::from_file(install_profile).unwrap_or_else(|err| {
+ eprintln!("Cannot load '{install_profile}': {}", err.to_string());
+ process::exit(EXIT_IO_ERROR);
+ });
+
+ let num_stages = loaded_config.stages.len();
+ for i in 1..=num_stages {
+ let stage = loaded_config.stages.get(i-1).unwrap();
+ let name = stage.name.clone().unwrap();
+ let count = format!("[{i}/{num_stages}]").yellow();
+
+ println!("{} {}", count.bold(), name.bold());
+ for step in stage.steps.as_ref().unwrap() {
+ let step_result = install::run_step(&step, &stage.base_path);
+
+ println!("{}", fmt_step(step, &step_result));
+ if step_result.is_err() {
+ eprintln!("Step failed: {}", step_result.unwrap_err().to_string());
+ println!();
+ println!("{} {} {} {}{}", "Install stopped at stage".red(), i.to_string().red(), "of".red(), num_stages.to_string().red(), ".".red());
+ process::exit(EXIT_INSTALL_FAILED);
+ }
+ }
+
+ println!()
+ }
+
+ println!("{} {} {}", "Install of".bright_green(), install_profile.bright_green(), "completed.".bright_green())
+}
+
diff --git a/src/util.rs b/src/util.rs
new file mode 100644
index 0000000..9c0ed1c
--- /dev/null
+++ b/src/util.rs
@@ -0,0 +1,34 @@
+use crate::config::Step;
+use colored::*;
+
+pub const EXIT_IO_ERROR: i32 = 1;
+pub const EXIT_INSTALL_FAILED: i32 = 2;
+
+// todo: figure out a better way of doing this
+pub fn str_step(step: &Step) -> String {
+ match step {
+ Step::Link(path) => format!("Link {} to {}", &path.from, &path.to),
+ Step::Copy(path) => format!("Copy {} to {}", &path.from, &path.to),
+ Step::Shell(command) => format!("Run {}", command),
+ }
+}
+
+pub fn fmt_step(step: &Step, result: &Result<bool, Box<dyn std::error::Error>>) -> String {
+ let action = str_step(step);
+
+ match result {
+ Ok(true) => { format!("{}{} {} {}", "[".bright_black(), "✔".green(), "]".bright_black(), action) }
+ Ok(false) => { format!("{}{} {} {}", "[".bright_black(), "▼".bright_black(), "]".bright_black(), action) }
+ Err(_) => { format!("{}{} {} {}", "[".bright_black(), "✘".red(), "]".bright_black(), action) }
+ }
+}
+
+pub fn expand_home(path: &str) -> String {
+ let home = std::env::var("HOME").expect("no $HOME");
+ if path.starts_with("~") {
+ let rest_of_path = &path[1..];
+ format!("{}{}", home, rest_of_path)
+ } else {
+ path.to_owned()
+ }
+} \ No newline at end of file