#![allow(rustdoc::private_intra_doc_links)]
#![deny(
dead_code,
deprecated_in_future,
exported_private_dependencies,
future_incompatible,
missing_copy_implementations,
rustdoc::missing_crate_level_docs,
rustdoc::broken_intra_doc_links,
missing_docs,
clippy::missing_docs_in_private_items,
missing_debug_implementations,
private_in_public,
rust_2018_compatibility,
rust_2018_idioms,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications,
// clippy attributes
clippy::missing_const_for_fn,
clippy::redundant_pub_crate,
clippy::use_self
)]
#![cfg_attr(docsrs, feature(doc_cfg), feature(doc_alias))]
mod diff;
mod opt;
mod util;
use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
use clap::{CommandFactory, Parser};
use color_eyre::eyre::eyre;
use color_eyre::Result;
use opt::Command;
use punktf_lib::profile::dotfile::Dotfile;
use punktf_lib::profile::source::PunktfSource;
use punktf_lib::profile::{resolve_profile, LayeredProfile, Profile};
use punktf_lib::template::source::Source;
use punktf_lib::template::Template;
use punktf_lib::visit::deploy::{deployment::Deployment, *};
use punktf_lib::visit::diff::Diff;
const BINARY_NAME: &str = env!("CARGO_BIN_NAME");
pub const PUNKTF_SOURCE_ENVVAR: &str = "PUNKTF_SOURCE";
pub const PUNKTF_TARGET_ENVVAR: &str = "PUNKTF_TARGET";
pub const PUNKTF_PROFILE_ENVVAR: &str = "PUNKTF_PROFILE";
fn main() -> Result<()> {
color_eyre::install()?;
let opts = opt::Opts::parse();
let log_level = if opts.shared.quite {
log::Level::Error
} else {
match opts.shared.verbose {
0 => log::Level::Warn,
1 => log::Level::Info,
2 => log::Level::Debug,
_ => log::Level::Trace,
}
};
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level.as_str()))
.init();
log::debug!("Parsed Opts:\n{:#?}", opts);
handle_command(opts.command)
}
fn handle_command(command: Command) -> Result<()> {
match command {
opt::Command::Deploy(c) => handle_command_deploy(c),
opt::Command::Render(c) => handle_command_render(c),
opt::Command::Verify(c) => handle_command_verify(c),
opt::Command::Diff(c) => handle_command_diff(c),
opt::Command::Man(c) => handle_command_man(c),
opt::Command::Completions(c) => handle_command_completions(c),
}
}
fn setup_profile(
profile_name: &str,
source: &PunktfSource,
target: Option<PathBuf>,
) -> Result<LayeredProfile> {
let mut builder = LayeredProfile::build();
let target_cli_profile = Profile {
target,
..Default::default()
};
builder.add(String::from("target_cli_argument"), target_cli_profile);
resolve_profile(&mut builder, source, profile_name)?;
let target_env_profile = Profile {
target: util::get_target_path(),
..Default::default()
};
builder.add(
String::from("target_environment_variable"),
target_env_profile,
);
Ok(builder.finish())
}
fn setup_env(source: &PunktfSource, profile: &LayeredProfile, profile_name: &str) {
std::env::set_var("PUNKTF_CURRENT_SOURCE", source.root());
if let Some(target) = profile.target_path() {
std::env::set_var("PUNKTF_CURRENT_TARGET", target);
}
std::env::set_var("PUNKTF_CURRENT_PROFILE", profile_name);
}
fn handle_output(
opt::OutputShared {
json_output,
yaml_output,
}: opt::OutputShared,
deployment: &Deployment,
) {
fn create_file(path: &Path) -> std::io::Result<File> {
OpenOptions::new().create_new(true).write(true).open(path)
}
'json: {
if let Some(json_path) = json_output {
let mut file = match create_file(&json_path) {
Ok(file) => file,
Err(err) => {
log::error!("Failed to create json output file: {err}");
break 'json;
}
};
if let Err(err) = serde_json::to_writer_pretty(&mut file, deployment) {
log::error!("Failed to write deployment status to json output file: {err}");
break 'json;
}
}
}
'yaml: {
if let Some(yaml_path) = yaml_output {
let mut file = match create_file(&yaml_path) {
Ok(file) => file,
Err(err) => {
log::error!("Failed to create yaml output file: {err}");
break 'yaml;
}
};
if let Err(err) = serde_yaml::to_writer(&mut file, deployment) {
log::error!("Failed to write deployment status to yaml output file: {err}");
break 'yaml;
}
}
}
}
fn handle_command_deploy(
opt::Deploy {
shared: opt::RepoShared {
source,
profile: profile_name,
..
},
target,
dry_run,
output,
}: opt::Deploy,
) -> Result<()> {
let ptf_src = PunktfSource::from_root(source)?;
let mut profile = setup_profile(&profile_name, &ptf_src, target)?;
if profile.target_path().is_none() {
panic!(
"No target path for the deployment set. Either use the command line argument \
`-t/--target`, the profile attribute `target` or the environment variable \
`{}`",
PUNKTF_TARGET_ENVVAR
)
}
log::debug!("Profile:\n{:#?}", profile);
log::debug!("Source: {}", ptf_src.root().display());
log::debug!("Target: {:?}", profile.target_path());
setup_env(&ptf_src, &profile, &profile_name);
let options = DeployOptions { dry_run };
let deployment = Deployer::new(options, util::ask_user_merge).deploy(&ptf_src, &mut profile);
log::debug!("Deployment:\n{:#?}", deployment);
util::log_deployment(&deployment, true);
handle_output(output, &deployment);
if options.dry_run {
log::info!("Note: No files were actually deployed, since dry run mode was enabled");
}
if deployment.status().is_failed() {
Err(eyre!("Some dotfiles failed to deploy"))
} else {
Ok(())
}
}
fn handle_command_render(
opt::Render {
shared: opt::RepoShared {
source,
profile: profile_name,
..
},
dotfile,
}: opt::Render,
) -> Result<()> {
fn find_dotfile<'a, 'b>(
dotfiles: impl Iterator<Item = &'a Dotfile>,
relative_source_path: &'b Path,
) -> Option<&'a Dotfile> {
dotfiles
.filter_map(|d| {
relative_source_path
.strip_prefix(&d.path)
.map(|p| (d, p))
.ok()
})
.reduce(|a, i| {
match (i.1.as_os_str().len(), a.1.as_os_str().len()) {
(i_len, a_len) if i_len == a_len => match (i.0.priority, a.0.priority) {
(Some(ip), Some(ap)) if ip >= ap => i,
(Some(_), None) | (None, None) => i,
_ => a,
},
(i_len, a_len) if i_len < a_len => i,
_ => a,
}
})
.map(|(d, _)| d)
}
let ptf_src = PunktfSource::from_root(source)?;
let profile = setup_profile(&profile_name, &ptf_src, None)?;
log::debug!("Profile:\n{:#?}", profile);
log::debug!("Source: {}", ptf_src.root().display());
log::debug!("Target: {:?}", profile.target_path());
setup_env(&ptf_src, &profile, &profile_name);
let dotfile_vars = if let Some(dotfile) = find_dotfile(profile.dotfiles(), &dotfile) {
log::debug!("Dotfile found in profile");
dotfile.variables.as_ref()
} else {
log::warn!("Dotfile not found in profile");
None
};
let file = ptf_src.dotfiles().join(dotfile);
let content = std::fs::read_to_string(&file)?;
let file_source = Source::file(&file, &content);
let template = Template::parse(file_source)?;
let resolved = template.resolve(Some(profile.variables()), dotfile_vars)?;
print!("{resolved}");
Ok(())
}
fn handle_command_verify(
opt::Verify {
shared: opt::RepoShared {
source,
profile: profile_name,
..
},
output,
}: opt::Verify,
) -> Result<()> {
let ptf_src = PunktfSource::from_root(source)?;
let mut profile = setup_profile(&profile_name, &ptf_src, None)?;
log::debug!("Profile:\n{:#?}", profile);
log::debug!("Source: {}", ptf_src.root().display());
log::debug!("Target: {:?}", profile.target_path());
setup_env(&ptf_src, &profile, &profile_name);
let options = DeployOptions { dry_run: true };
let deployment = Deployer::new(options, util::ask_user_merge).deploy(&ptf_src, &mut profile);
log::debug!("Deployment:\n{:#?}", deployment);
util::log_deployment(&deployment, true);
handle_output(output, &deployment);
Ok(())
}
fn handle_command_diff(
opt::Diff {
shared: opt::RepoShared {
source,
profile: profile_name,
..
},
format,
}: opt::Diff,
) -> Result<()> {
let ptf_src = PunktfSource::from_root(source)?;
let mut profile = setup_profile(&profile_name, &ptf_src, None)?;
log::debug!("Profile:\n{:#?}", profile);
log::debug!("Source: {}", ptf_src.root().display());
log::debug!("Target: {:?}", profile.target_path());
setup_env(&ptf_src, &profile, &profile_name);
Diff::new(|event| diff::diff(format, event)).diff(&ptf_src, &mut profile);
Ok(())
}
fn handle_command_man(opt::Man { output }: opt::Man) -> Result<()> {
let output = output.join(format!("{}.1", BINARY_NAME));
let man = clap_mangen::Man::new(opt::Opts::command());
let mut buffer: Vec<u8> = Default::default();
man.render(&mut buffer)?;
std::fs::write(output, buffer)?;
Ok(())
}
fn handle_command_completions(opt::Completions { shell, output }: opt::Completions) -> Result<()> {
clap_complete::generate_to(shell, &mut opt::Opts::command(), BINARY_NAME, output)?;
Ok(())
}