diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/cli.rs | 8 | ||||
| -rw-r--r-- | src/config.rs | 93 | ||||
| -rw-r--r-- | src/install.rs | 49 | ||||
| -rw-r--r-- | src/main.rs | 45 | ||||
| -rw-r--r-- | src/util.rs | 34 |
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 |
