diff --git a/crates/punktf-lib/src/profile/dotfile.rs b/crates/punktf-lib/src/profile/dotfile.rs index eebfab6..a47dcdd 100644 --- a/crates/punktf-lib/src/profile/dotfile.rs +++ b/crates/punktf-lib/src/profile/dotfile.rs @@ -18,20 +18,16 @@ pub struct Dotfile { /// directory. pub path: PathBuf, - /// Alternative relative name/path for the dotfile. This name will be used - /// instead of [`Dotfile::path`](`crate::profile::dotfile::Dotfile::path`) - /// when deploying. If this is set and the - /// dotfile is a directory, it will be deployed under the given name and - /// not in the - /// [`PunktfSource::root`](`crate::profile::source::PunktfSource::root`) - /// directory. + /// Used to overwrite the default target location of a dotfile. + /// The resolved/actual output path of the [`Dotfile`] depends on the given path: + /// + /// - If the given path is absolute, [`super::Profile::target`] will be completely ignored and this path will be used instead + /// - If the given path is relative, it will be appended to [`super::Profile::target`] + /// + /// NOTE: Additionally, setting this option, will completely ignore the relative path of the dotfile within the + /// `dotfiles` folder for target path resolution. #[serde(skip_serializing_if = "Option::is_none", default)] - pub rename: Option, - - /// Alternative absolute deploy target path. This will be used instead of - /// [`Profile::target`](`crate::profile::Profile::target`) when deploying. - #[serde(alias = "target", skip_serializing_if = "Option::is_none", default)] - pub overwrite_target: Option, + pub target: Option, /// Priority of the dotfile. Dotfiles with higher priority as others are /// allowed to overwrite an already deployed dotfile if the diff --git a/crates/punktf-lib/src/profile/mod.rs b/crates/punktf-lib/src/profile/mod.rs index 84f0d87..b9a74d3 100644 --- a/crates/punktf-lib/src/profile/mod.rs +++ b/crates/punktf-lib/src/profile/mod.rs @@ -7,7 +7,7 @@ pub mod source; pub mod transform; pub mod variables; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::fs::File; use std::ops::Deref; use std::path::{Path, PathBuf}; @@ -84,8 +84,7 @@ pub struct Profile { pub transformers: Vec, /// Target root path of the deployment. Will be used as file stem for the dotfiles - /// when not overwritten by - /// [`Dotfile::overwrite_target`](`crate::profile::dotfile::Dotfile::overwrite_target`). + /// when not overwritten by [`dotfile::Dotfile::target`]. #[serde(skip_serializing_if = "Option::is_none", default)] pub target: Option, @@ -377,7 +376,6 @@ impl LayeredProfileBuilder { }) .collect(); - let mut added_dotfile_paths = HashSet::new(); let mut dotfiles = Vec::new(); for (idx, dfiles) in self @@ -387,10 +385,7 @@ impl LayeredProfileBuilder { .map(|(idx, profile)| (idx, &profile.dotfiles)) { for dotfile in dfiles.iter() { - if !added_dotfile_paths.contains(&dotfile.path) { - dotfiles.push((idx, dotfile.clone())); - added_dotfile_paths.insert(dotfile.path.clone()); - } + dotfiles.push((idx, dotfile.clone())); } } @@ -612,7 +607,7 @@ mod tests { #[test] #[cfg(feature = "profile-json")] - fn profile_serde() { + fn profile_serde_json() { crate::tests::setup_test_env(); let mut profile_vars = HashMap::new(); @@ -636,8 +631,7 @@ mod tests { dotfiles: vec![ Dotfile { path: PathBuf::from("init.vim.ubuntu"), - rename: Some(PathBuf::from("init.vim")), - overwrite_target: None, + target: Some(PathBuf::from("init.vim")), priority: Some(Priority::new(2)), variables: None, transformers: Vec::new(), @@ -646,8 +640,7 @@ mod tests { }, Dotfile { path: PathBuf::from(".bashrc"), - rename: None, - overwrite_target: Some(PathBuf::from("/home/demo")), + target: Some(PathBuf::from("/home/demo")), priority: None, variables: Some(Variables { inner: dotfile_vars, @@ -666,4 +659,59 @@ mod tests { assert_eq!(parsed, profile); } + + #[test] + #[cfg(feature = "profile-yaml")] + fn profile_serde_yaml() { + crate::tests::setup_test_env(); + + let mut profile_vars = HashMap::new(); + profile_vars.insert(String::from("RUSTC_VERSION"), String::from("XX.YY")); + profile_vars.insert(String::from("RUSTC_PATH"), String::from("/usr/bin/rustc")); + + let mut dotfile_vars = HashMap::new(); + dotfile_vars.insert(String::from("RUSTC_VERSION"), String::from("55.22")); + dotfile_vars.insert(String::from("USERNAME"), String::from("demo")); + + let profile = Profile { + extends: Vec::new(), + aliases: vec![], + variables: Some(Variables { + inner: profile_vars, + }), + transformers: Vec::new(), + target: Some(PathBuf::from("/home/demo/.config")), + pre_hooks: vec![Hook::new("echo \"Foo\"")], + post_hooks: vec![Hook::new("profiles/test.sh")], + dotfiles: vec![ + Dotfile { + path: PathBuf::from("init.vim.ubuntu"), + target: Some(PathBuf::from("init.vim")), + priority: Some(Priority::new(2)), + variables: None, + transformers: Vec::new(), + merge: Some(MergeMode::Overwrite), + template: None, + }, + Dotfile { + path: PathBuf::from(".bashrc"), + target: Some(PathBuf::from("/home/demo")), + priority: None, + variables: Some(Variables { + inner: dotfile_vars, + }), + transformers: Vec::new(), + merge: Some(MergeMode::Overwrite), + template: Some(false), + }, + ], + symlinks: vec![], + }; + + let json = serde_yaml::to_string(&profile).expect("Profile to be serializeable"); + + let parsed: Profile = serde_yaml::from_str(&json).expect("Profile to be deserializable"); + + assert_eq!(parsed, profile); + } } diff --git a/crates/punktf-lib/src/template/resolve.rs b/crates/punktf-lib/src/template/resolve.rs index acfd65f..b1df9f7 100644 --- a/crates/punktf-lib/src/template/resolve.rs +++ b/crates/punktf-lib/src/template/resolve.rs @@ -81,7 +81,7 @@ macro_rules! family { "unix" } else if #[cfg(target_os = "windows")] { "windows" - } else if #[cfg(target_os = "wasm")] { + } else if #[cfg(target_os = "wasi")] { "wasm" } else { "unknown" diff --git a/crates/punktf-lib/src/visit/deploy/mod.rs b/crates/punktf-lib/src/visit/deploy/mod.rs index f837c74..06f2587 100644 --- a/crates/punktf-lib/src/visit/deploy/mod.rs +++ b/crates/punktf-lib/src/visit/deploy/mod.rs @@ -16,6 +16,32 @@ use std::path::Path; use crate::visit::{ResolvingVisitor, TemplateVisitor}; +/// Represents the contents of a file as returned by [`safe_read`]. +enum SafeRead { + /// File was a normal text file. + String(String), + + /// File was unable to be interpreted as a text file. + Binary(Vec), +} + +/// Reads the contents of a file, first trying to interpret them as a string and if that fails +/// returning the raw bytes. +fn safe_read>(path: P) -> io::Result { + /// Inner function to reduce size of monomorphization. + fn inner(path: &Path) -> io::Result { + match std::fs::read_to_string(path) { + Ok(s) => Ok(SafeRead::String(s)), + Err(err) if err.kind() == io::ErrorKind::InvalidData => { + std::fs::read(path).map(SafeRead::Binary) + } + Err(err) => Err(err), + } + } + + inner(path.as_ref()) +} + impl<'a> Item<'a> { /// Adds this item to the given /// [`DeploymentBuilder`](`crate::visit::deploy::deployment::DeploymentBuilder`). @@ -373,8 +399,23 @@ where } } } else { - let content = match std::fs::read_to_string(&file.source_path) { - Ok(content) => content, + let content = match safe_read(&file.source_path) { + Ok(SafeRead::Binary(b)) => { + log::info!( + "[{}] Not evaluated as template - Binary data", + file.relative_source_path.display() + ); + + b + } + Ok(SafeRead::String(s)) => { + let Ok(content) = self.transform_content(profile, file, s) else { + // Error is already recorded + return Ok(()); + }; + + content.into_bytes() + } Err(err) => { log::info!( "{}: Failed to read file", @@ -385,13 +426,8 @@ where } }; - let Ok(content) = self.transform_content(profile, file, content) else { - // Error is already recorded - return Ok(()); - }; - if !self.options.dry_run { - if let Err(err) = std::fs::write(&file.target_path, content.as_bytes()) { + if let Err(err) = std::fs::write(&file.target_path, content) { log::info!( "{}: Failed to write content", file.relative_source_path.display() @@ -661,38 +697,51 @@ where return Ok(()); } - let content = match std::fs::read_to_string(&file.source_path) { - Ok(content) => content, - Err(err) => { - log::info!("{}: Failed read file", file.relative_source_path.display()); + let content = match safe_read(&file.source_path) { + Ok(SafeRead::Binary(b)) => { + log::info!( + "[{}] Not evaluated as template - Binary data", + file.relative_source_path.display() + ); - failed!(&mut self.builder, file, format!("Failed to read: {err}")); + b } - }; + Ok(SafeRead::String(s)) => { + let content = match resolve_content(&s) { + Ok(content) => content, + Err(err) => { + log::info!( + "{}: Failed to resolve template", + file.relative_source_path.display() + ); + + failed!( + &mut self.builder, + file, + format!("Failed to resolve template: {err}") + ); + } + }; - let content = match resolve_content(&content) { - Ok(content) => content, + let Ok(content) = self.transform_content(profile, file, content) else { + // Error is already recorded + return Ok(()); + }; + + content.into_bytes() + } Err(err) => { log::info!( - "{}: Failed to resolve template", + "{}: Failed to read file", file.relative_source_path.display() ); - failed!( - &mut self.builder, - file, - format!("Failed to resolve template: {err}") - ); + failed!(&mut self.builder, file, format!("Failed to read: {err}")); } }; - let Ok(content) = self.transform_content(profile, file, content) else { - // Error is already recorded - return Ok(()); - }; - if !self.options.dry_run { - if let Err(err) = std::fs::write(&file.target_path, content.as_bytes()) { + if let Err(err) = std::fs::write(&file.target_path, content) { log::info!( "{}: Failed to write content", file.relative_source_path.display() diff --git a/crates/punktf-lib/src/visit/mod.rs b/crates/punktf-lib/src/visit/mod.rs index c78c08b..857012d 100644 --- a/crates/punktf-lib/src/visit/mod.rs +++ b/crates/punktf-lib/src/visit/mod.rs @@ -435,7 +435,7 @@ impl<'a> Walker<'a> { } }; - let target_path = match self.resolve_target_path(dotfile, source_path.is_dir()) { + let target_path = match self.resolve_target_path(dotfile) { Ok(p) => p, Err(err) => { let paths = Paths::new(dotfile.path.clone(), dotfile.path.clone()); @@ -659,20 +659,23 @@ impl<'a> Walker<'a> { } /// Resolves the dotfile to a absolute target path. - /// - /// Some special logic is applied for directories. - fn resolve_target_path(&self, dotfile: &Dotfile, is_dir: bool) -> io::Result { - let path = if is_dir && dotfile.rename.is_none() && dotfile.overwrite_target.is_none() { + fn resolve_target_path(&self, dotfile: &Dotfile) -> io::Result { + let path = if let Some(alt_target) = &dotfile.target { + if alt_target.is_absolute() { + alt_target.to_path_buf() + } else { + self.profile + .target_path() + .expect("No target path set") + .to_path_buf() + .join(alt_target) + } + } else { self.profile .target_path() .expect("No target path set") .to_path_buf() - } else { - dotfile - .overwrite_target - .as_deref() - .unwrap_or_else(|| self.profile.target_path().expect("No target path set")) - .join(dotfile.rename.as_ref().unwrap_or(&dotfile.path)) + .join(&dotfile.path) }; self.resolve_path(&path) diff --git a/examples/03_single_template/profiles/simple.yml b/examples/03_single_template/profiles/simple.yml index 517bff4..97f150c 100644 --- a/examples/03_single_template/profiles/simple.yml +++ b/examples/03_single_template/profiles/simple.yml @@ -2,4 +2,8 @@ dotfiles: - path: "hello.template" # Instead of using the name from the source file (`hello.template`) # this option can be used to overwrite it. - rename: "hello.txt" + # This can either be a relative path, then it will be appended to the profile target: + target: "hello.txt" + - path: "hello.template" + # ... or it can be a absolute path, which will completely ignore the profile target: + target: "/hello.txt" diff --git a/examples/80_simple_complete/dotfiles/home/.bashrc b/examples/80_simple_complete/dotfiles/home/.bashrc new file mode 100644 index 0000000..e69de29 diff --git a/examples/80_simple_complete/profiles/simple.yml b/examples/80_simple_complete/profiles/simple.yml index 4b7a0a0..5c7b567 100644 --- a/examples/80_simple_complete/profiles/simple.yml +++ b/examples/80_simple_complete/profiles/simple.yml @@ -7,23 +7,17 @@ post_hooks: - echo "Finished deployment of simple complete example" dotfiles: - - path: "config" - # Obsolete, replaced with `target` - overwrite_target: "/custom_other/target" - template: false - - path: "config" - # New version of `overwrite_target` - target: "/custom_other/target" + - path: "home" + target: "." template: false - path: "config" - rename: ".config" + target: ".config" template: false - path: "zsh/zshrc" - rename: ".config/zsh/.zshrc" + target: ".config/zsh/.zshrc" template: false - path: "shellcheckrc" - overwrite_target: "~" - rename: ".shellcheckrc" + target: "~/.shellcheckrc" variables: SHELLCHECK_DISABLE: "SC1090,SC1091" template: true diff --git a/examples/85_multi_os/profiles/base.yml b/examples/85_multi_os/profiles/base.yml index f83bdf7..6ee4b4c 100644 --- a/examples/85_multi_os/profiles/base.yml +++ b/examples/85_multi_os/profiles/base.yml @@ -1,2 +1,4 @@ dotfiles: - path: "shared/root" + # Put the files directly into the profile target dir instead the "shared/root" subfolder + target: "." diff --git a/examples/85_multi_os/profiles/linux.yml b/examples/85_multi_os/profiles/linux.yml index d9c31f0..b1738c5 100644 --- a/examples/85_multi_os/profiles/linux.yml +++ b/examples/85_multi_os/profiles/linux.yml @@ -6,6 +6,7 @@ target: "$HOME" dotfiles: # `shared/root` will be deployed by `base` - path: "shared/nvim" - rename: ".config/nvim" + target: ".config/nvim" template: true - path: "linux" + target: "." diff --git a/examples/85_multi_os/profiles/windows.yml b/examples/85_multi_os/profiles/windows.yml index 0a7a00a..c9c1b53 100644 --- a/examples/85_multi_os/profiles/windows.yml +++ b/examples/85_multi_os/profiles/windows.yml @@ -6,6 +6,7 @@ target: "$HOMEPATH" dotfiles: # `shared/root` will be deployed by `base` - path: "shared\\nvim" - rename: "AppData\\Local\\nvim" + target: "AppData\\Local\\nvim" template: true - path: "windows" + target: "."