Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions crates/punktf-lib/src/profile/dotfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf>,

/// 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<PathBuf>,
pub target: Option<PathBuf>,

/// Priority of the dotfile. Dotfiles with higher priority as others are
/// allowed to overwrite an already deployed dotfile if the
Expand Down
74 changes: 61 additions & 13 deletions crates/punktf-lib/src/profile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -84,8 +84,7 @@ pub struct Profile {
pub transformers: Vec<ContentTransformer>,

/// 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<PathBuf>,

Expand Down Expand Up @@ -377,7 +376,6 @@ impl LayeredProfileBuilder {
})
.collect();

let mut added_dotfile_paths = HashSet::new();
let mut dotfiles = Vec::new();

for (idx, dfiles) in self
Expand All @@ -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()));
}
}

Expand Down Expand Up @@ -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();
Expand All @@ -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(),
Expand All @@ -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,
Expand All @@ -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);
}
}
2 changes: 1 addition & 1 deletion crates/punktf-lib/src/template/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
105 changes: 77 additions & 28 deletions crates/punktf-lib/src/visit/deploy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>),
}

/// 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<P: AsRef<Path>>(path: P) -> io::Result<SafeRead> {
/// Inner function to reduce size of monomorphization.
fn inner(path: &Path) -> io::Result<SafeRead> {
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`).
Expand Down Expand Up @@ -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",
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
25 changes: 14 additions & 11 deletions crates/punktf-lib/src/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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<PathBuf> {
let path = if is_dir && dotfile.rename.is_none() && dotfile.overwrite_target.is_none() {
fn resolve_target_path(&self, dotfile: &Dotfile) -> io::Result<PathBuf> {
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)
Expand Down
6 changes: 5 additions & 1 deletion examples/03_single_template/profiles/simple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Empty file.
16 changes: 5 additions & 11 deletions examples/80_simple_complete/profiles/simple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/85_multi_os/profiles/base.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
dotfiles:
- path: "shared/root"
# Put the files directly into the profile target dir instead the "shared/root" subfolder
target: "."
Loading