diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | Cargo.lock | 399 | ||||
| -rw-r--r-- | Cargo.toml | 12 | ||||
| -rw-r--r-- | PKGBUILD | 15 | ||||
| -rw-r--r-- | README.md | 15 | ||||
| -rwxr-xr-x | install.sh | 4 | ||||
| -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 |
11 files changed, 677 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34fb28a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +*.pkg.tar.zst +/pkg diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..99a163a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,399 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "clap" +version = "4.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0827b011f6f8ab38590295339817b0d26f344aa4932c3ced71b45b0c54b4a9" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9441b403be87be858db6a23edb493e7f694761acdc3343d5a0fcaafd304cbc9e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "colored" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +dependencies = [ + "is-terminal", + "lazy_static", + "windows-sys", +] + +[[package]] +name = "dotfiles-installer" +version = "0.1.0" +dependencies = [ + "clap", + "colored", + "serde", + "serde_yaml", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "serde" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_yaml" +version = "0.9.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5f51e3fdb5b9cdd1577e1cb7a733474191b1aca6a72c2e50913241632c1180" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a5478e4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dotfiles-installer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.9.24" +colored = "2" +clap = { version = "4.3.17", features = ["derive"] } diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..addd2f9 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,15 @@ +pkgname=dotfiles-installer +pkgver=0.1.0 +pkgrel=1 +makedepends=('rust' 'cargo') +arch=('any') + +build() { + cargo build --frozen --release +} + +package() { + cd $srcdir/.. + install -Dm755 "target/release/$pkgname" \ + -t "$pkgdir/usr/bin" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..798aa26 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# dotfiles-installer + +Small program to automatically install my [dotfiles](https://github.com/LMBishop/dotfiles). Unless you happen to be me, I wouldn't recommend you use this. + +## Install + +Arch Linux only: +``` +$ ./install.sh +``` + +## Usage +``` +$ dotfiles-installer --file <FILE> +```
\ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..836b464 --- /dev/null +++ b/install.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +makepkg +sudo pacman -U dotfiles-installer*.pkg.tar*
\ No newline at end of file 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 |
