Added ability to include directories as an extension of the config file (#421)
This commit is contained in:
588
src/config.rs
588
src/config.rs
@@ -1,17 +1,20 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::write;
|
||||
use std::path::PathBuf;
|
||||
use std::fs::{write, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::{env, fs};
|
||||
|
||||
use clap::{ArgEnum, Parser};
|
||||
use clap_complete::Shell;
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::Context;
|
||||
use color_eyre::eyre::Result;
|
||||
use etcetera::base_strategy::BaseStrategy;
|
||||
use merge::Merge;
|
||||
use regex::Regex;
|
||||
use regex_split::RegexSplit;
|
||||
use serde::Deserialize;
|
||||
use strum::{EnumIter, EnumString, EnumVariantNames, IntoEnumIterator};
|
||||
use tracing::debug;
|
||||
@@ -19,6 +22,7 @@ use which_crate::which;
|
||||
|
||||
use crate::command::CommandExt;
|
||||
use crate::sudo::SudoKind;
|
||||
use crate::utils::string_prepend_str;
|
||||
|
||||
use super::utils::{editor, hostname};
|
||||
|
||||
@@ -51,21 +55,43 @@ macro_rules! check_deprecated {
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! get_deprecated {
|
||||
($config:expr, $old:ident, $section:ident, $new:ident) => {
|
||||
if $config.$old.is_some() {
|
||||
&$config.$old
|
||||
} else {
|
||||
if let Some(section) = &$config.$section {
|
||||
§ion.$new
|
||||
} else {
|
||||
&None
|
||||
|
||||
/// Get a deprecated option moved from a section to another
|
||||
macro_rules! get_deprecated_moved_opt {
|
||||
($old_section:expr, $old:ident, $new_section:expr, $new:ident) => {{
|
||||
if let Some(old_section) = &$old_section {
|
||||
if old_section.$old.is_some() {
|
||||
return &old_section.$old;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(new_section) = &$new_section {
|
||||
return &new_section.$new;
|
||||
}
|
||||
|
||||
return &None;
|
||||
}};
|
||||
}
|
||||
|
||||
type Commands = BTreeMap<String, String>;
|
||||
macro_rules! get_deprecated_moved_or_default_to {
|
||||
($old_section:expr, $old:ident, $new_section:expr, $new:ident, $default_ret:ident) => {{
|
||||
if let Some(old_section) = &$old_section {
|
||||
if let Some(old) = old_section.$old {
|
||||
return old;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_section) = &$new_section {
|
||||
if let Some(new) = new_section.$new {
|
||||
return new;
|
||||
}
|
||||
}
|
||||
|
||||
return $default_ret;
|
||||
}};
|
||||
}
|
||||
|
||||
pub type Commands = BTreeMap<String, String>;
|
||||
|
||||
#[derive(ArgEnum, EnumString, EnumVariantNames, Debug, Clone, PartialEq, Eq, Deserialize, EnumIter, Copy)]
|
||||
#[clap(rename_all = "snake_case")]
|
||||
@@ -169,24 +195,38 @@ pub enum Step {
|
||||
Yarn,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Include {
|
||||
#[merge(strategy = merge::vec::append)]
|
||||
paths: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Git {
|
||||
max_concurrency: Option<usize>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
|
||||
repos: Option<Vec<String>>,
|
||||
|
||||
pull_predefined: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Vagrant {
|
||||
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
|
||||
directories: Option<Vec<String>>,
|
||||
|
||||
power_on: Option<bool>,
|
||||
always_suspend: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Windows {
|
||||
accept_all_updates: Option<bool>,
|
||||
@@ -197,7 +237,7 @@ pub struct Windows {
|
||||
wsl_update_use_web_download: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Python {
|
||||
enable_pip_review: Option<bool>,
|
||||
@@ -205,43 +245,45 @@ pub struct Python {
|
||||
enable_pipupgrade: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct Distrobox {
|
||||
use_root: Option<bool>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
|
||||
containers: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct Yarn {
|
||||
use_sudo: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct NPM {
|
||||
use_sudo: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct Firmware {
|
||||
upgrade: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct Flatpak {
|
||||
use_sudo: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Brew {
|
||||
greedy_cask: Option<bool>,
|
||||
@@ -262,88 +304,191 @@ pub enum ArchPackageManager {
|
||||
Yay,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Linux {
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
yay_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
aura_aur_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
aura_pacman_arguments: Option<String>,
|
||||
arch_package_manager: Option<ArchPackageManager>,
|
||||
show_arch_news: Option<bool>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
garuda_update_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
trizen_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
pikaur_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
pamac_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
dnf_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
nix_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
apt_arguments: Option<String>,
|
||||
|
||||
enable_tlmgr: Option<bool>,
|
||||
redhat_distro_sync: Option<bool>,
|
||||
suse_dup: Option<bool>,
|
||||
rpm_ostree: Option<bool>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
emerge_sync_flags: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
emerge_update_flags: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Composer {
|
||||
self_update: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Vim {
|
||||
force_plug_update: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Misc {
|
||||
pre_sudo: Option<bool>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
|
||||
git_repos: Option<Vec<String>>,
|
||||
|
||||
predefined_git_repos: Option<bool>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
|
||||
disable: Option<Vec<Step>>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
|
||||
ignore_failures: Option<Vec<Step>>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
|
||||
remote_topgrades: Option<Vec<String>>,
|
||||
|
||||
remote_topgrade_path: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
ssh_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
git_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
tmux_arguments: Option<String>,
|
||||
|
||||
set_title: Option<bool>,
|
||||
|
||||
display_time: Option<bool>,
|
||||
|
||||
display_preamble: Option<bool>,
|
||||
|
||||
assume_yes: Option<bool>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
yay_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
aura_aur_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
|
||||
aura_pacman_arguments: Option<String>,
|
||||
|
||||
no_retry: Option<bool>,
|
||||
|
||||
run_in_tmux: Option<bool>,
|
||||
|
||||
cleanup: Option<bool>,
|
||||
|
||||
notify_each_step: Option<bool>,
|
||||
|
||||
accept_all_windows_updates: Option<bool>,
|
||||
|
||||
skip_notify: Option<bool>,
|
||||
|
||||
bashit_branch: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
|
||||
only: Option<Vec<Step>>,
|
||||
|
||||
no_self_update: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug, Merge)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
/// Configuration file
|
||||
pub struct ConfigFile {
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
include: Option<Include>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
misc: Option<Misc>,
|
||||
|
||||
sudo_command: Option<SudoKind>,
|
||||
pre_sudo: Option<bool>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)]
|
||||
pre_commands: Option<Commands>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)]
|
||||
post_commands: Option<Commands>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)]
|
||||
commands: Option<Commands>,
|
||||
git_repos: Option<Vec<String>>,
|
||||
predefined_git_repos: Option<bool>,
|
||||
disable: Option<Vec<Step>>,
|
||||
ignore_failures: Option<Vec<Step>>,
|
||||
remote_topgrades: Option<Vec<String>>,
|
||||
remote_topgrade_path: Option<String>,
|
||||
ssh_arguments: Option<String>,
|
||||
git_arguments: Option<String>,
|
||||
tmux_arguments: Option<String>,
|
||||
set_title: Option<bool>,
|
||||
display_time: Option<bool>,
|
||||
display_preamble: Option<bool>,
|
||||
assume_yes: Option<bool>,
|
||||
yay_arguments: Option<String>,
|
||||
aura_aur_arguments: Option<String>,
|
||||
aura_pacman_arguments: Option<String>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
python: Option<Python>,
|
||||
no_retry: Option<bool>,
|
||||
run_in_tmux: Option<bool>,
|
||||
cleanup: Option<bool>,
|
||||
notify_each_step: Option<bool>,
|
||||
accept_all_windows_updates: Option<bool>,
|
||||
skip_notify: Option<bool>,
|
||||
bashit_branch: Option<String>,
|
||||
only: Option<Vec<Step>>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
composer: Option<Composer>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
brew: Option<Brew>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
linux: Option<Linux>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
git: Option<Git>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
windows: Option<Windows>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
npm: Option<NPM>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
yarn: Option<Yarn>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
vim: Option<Vim>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
firmware: Option<Firmware>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
vagrant: Option<Vagrant>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
flatpak: Option<Flatpak>,
|
||||
|
||||
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
|
||||
distrobox: Option<Distrobox>,
|
||||
no_self_update: Option<bool>,
|
||||
}
|
||||
|
||||
fn config_directory() -> PathBuf {
|
||||
@@ -354,58 +499,164 @@ fn config_directory() -> PathBuf {
|
||||
return crate::WINDOWS_DIRS.config_dir();
|
||||
}
|
||||
|
||||
/// The only purpose of this struct is to deserialize only the `include` field of the config file.
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
struct ConfigFileIncludeOnly {
|
||||
include: Option<Include>,
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
fn ensure() -> Result<PathBuf> {
|
||||
/// Returns the main config file and any additional config files
|
||||
/// 0 = main config file
|
||||
/// 1 = additional config files coming from topgrade.d
|
||||
fn ensure() -> Result<(PathBuf, Vec<PathBuf>)> {
|
||||
let mut res = (PathBuf::new(), Vec::new());
|
||||
|
||||
let config_directory = config_directory();
|
||||
|
||||
let config_path = config_directory.join("topgrade.toml");
|
||||
let alt_config_path = config_directory.join("topgrade/topgrade.toml");
|
||||
let possible_config_paths = vec![
|
||||
config_directory.join("topgrade.toml"),
|
||||
config_directory.join("topgrade/topgrade.toml"),
|
||||
];
|
||||
|
||||
if config_path.exists() {
|
||||
debug!("Configuration at {}", config_path.display());
|
||||
Ok(config_path)
|
||||
} else if alt_config_path.exists() {
|
||||
debug!("Configuration at {}", alt_config_path.display());
|
||||
Ok(alt_config_path)
|
||||
} else {
|
||||
// Search for the main config file
|
||||
for path in possible_config_paths.iter() {
|
||||
if path.exists() {
|
||||
debug!("Configuration at {}", path.display());
|
||||
res.0 = path.clone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
res.1 = Self::ensure_topgrade_d(&config_directory)?;
|
||||
|
||||
// If no config file exists, create a default one in the config directory
|
||||
if !res.0.exists() && res.1.is_empty() {
|
||||
debug!("No configuration exists");
|
||||
write(&config_path, EXAMPLE_CONFIG).map_err(|e| {
|
||||
write(&res.0, EXAMPLE_CONFIG).map_err(|e| {
|
||||
debug!(
|
||||
"Unable to write the example configuration file to {}: {}. Using blank config.",
|
||||
config_path.display(),
|
||||
&res.0.display(),
|
||||
e
|
||||
);
|
||||
e
|
||||
})?;
|
||||
Ok(config_path)
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Searches topgrade.d for additional config files
|
||||
fn ensure_topgrade_d(config_directory: &Path) -> Result<Vec<PathBuf>> {
|
||||
let mut res = Vec::new();
|
||||
let dir_to_search = config_directory.join("topgrade.d");
|
||||
|
||||
if dir_to_search.exists() {
|
||||
for entry in fs::read_dir(dir_to_search)? {
|
||||
let entry = entry?;
|
||||
if entry.file_type()?.is_file() {
|
||||
debug!(
|
||||
"Found additional (directory) configuration file at {}",
|
||||
entry.path().display()
|
||||
);
|
||||
res.push(entry.path());
|
||||
}
|
||||
}
|
||||
res.sort();
|
||||
} else {
|
||||
debug!("No additional configuration directory exists, creating one");
|
||||
fs::create_dir_all(&dir_to_search)?;
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Read the configuration file.
|
||||
///
|
||||
/// If the configuration file does not exist the function returns the default ConfigFile.
|
||||
/// If the configuration file does not exist, the function returns the default ConfigFile.
|
||||
fn read(config_path: Option<PathBuf>) -> Result<ConfigFile> {
|
||||
let mut result = Self::default();
|
||||
|
||||
let config_path = if let Some(path) = config_path {
|
||||
path
|
||||
} else {
|
||||
Self::ensure()?
|
||||
let (path, dir_include) = Self::ensure()?;
|
||||
|
||||
/*
|
||||
The Function was called without a config_path, we need
|
||||
to read the include directory before returning the main config path
|
||||
*/
|
||||
for include in dir_include {
|
||||
let include_contents = fs::read_to_string(&include).map_err(|e| {
|
||||
tracing::error!("Unable to read {}", include.display());
|
||||
e
|
||||
})?;
|
||||
let include_contents_parsed = toml::from_str(include_contents.as_str()).map_err(|e| {
|
||||
tracing::error!("Failed to deserialize {}", include.display());
|
||||
e
|
||||
})?;
|
||||
|
||||
result.merge(include_contents_parsed);
|
||||
}
|
||||
|
||||
path
|
||||
};
|
||||
|
||||
let contents = fs::read_to_string(&config_path).map_err(|e| {
|
||||
let mut contents_non_split = fs::read_to_string(&config_path).map_err(|e| {
|
||||
tracing::error!("Unable to read {}", config_path.display());
|
||||
e
|
||||
})?;
|
||||
|
||||
let mut result: Self = toml::from_str(&contents).map_err(|e| {
|
||||
tracing::error!("Failed to deserialize {}", config_path.display());
|
||||
e
|
||||
})?;
|
||||
Self::ensure_misc_is_present(&mut contents_non_split, &config_path);
|
||||
|
||||
if let Some(ref mut paths) = &mut result.git_repos {
|
||||
for path in paths.iter_mut() {
|
||||
let expanded = shellexpand::tilde::<&str>(&path.as_ref()).into_owned();
|
||||
debug!("Path {} expanded to {}", path, expanded);
|
||||
*path = expanded;
|
||||
// To parse [include] sections in the order as they are written,
|
||||
// we split the file and parse each part as a separate file
|
||||
let regex_match_include = Regex::new(r"\[include]").expect("Failed to compile regex");
|
||||
let contents_split = regex_match_include.split_inclusive_left(contents_non_split.as_str());
|
||||
|
||||
for contents in contents_split {
|
||||
let config_file_include_only: ConfigFileIncludeOnly = toml::from_str(contents).map_err(|e| {
|
||||
tracing::error!("Failed to deserialize an include section of {}", config_path.display());
|
||||
e
|
||||
})?;
|
||||
|
||||
if let Some(includes) = &config_file_include_only.include {
|
||||
// Parses the [include] section present in the slice
|
||||
for include in includes.paths.iter().rev() {
|
||||
let include_path = shellexpand::tilde::<&str>(&include.as_ref()).into_owned();
|
||||
let include_path = PathBuf::from(include_path);
|
||||
let include_contents = match fs::read_to_string(&include_path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::error!("Unable to read {}: {}", include_path.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match toml::from_str::<Self>(&include_contents) {
|
||||
Ok(include_parsed) => result.merge(include_parsed),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to deserialize {}: {}", include_path.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Configuration include found: {}", include_path.display());
|
||||
}
|
||||
}
|
||||
|
||||
match toml::from_str::<Self>(contents) {
|
||||
Ok(contents) => result.merge(contents),
|
||||
Err(e) => tracing::error!("Failed to deserialize {}: {}", config_path.display(), e),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(misc) = &mut result.misc {
|
||||
if let Some(ref mut paths) = &mut misc.git_repos {
|
||||
for path in paths.iter_mut() {
|
||||
let expanded = shellexpand::tilde::<&str>(&path.as_ref()).into_owned();
|
||||
debug!("Path {} expanded to {}", path, expanded);
|
||||
*path = expanded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +674,7 @@ impl ConfigFile {
|
||||
}
|
||||
|
||||
fn edit() -> Result<()> {
|
||||
let config_path = Self::ensure()?;
|
||||
let config_path = Self::ensure()?.0;
|
||||
let editor = editor();
|
||||
debug!("Editor: {:?}", editor);
|
||||
|
||||
@@ -436,6 +687,18 @@ impl ConfigFile {
|
||||
.status_checked()
|
||||
.context("Failed to open configuration file editor")
|
||||
}
|
||||
|
||||
/// [Misc] was added later, here we check if it is present in the config file and add it if not
|
||||
fn ensure_misc_is_present(contents: &mut String, path: &PathBuf) {
|
||||
if !contents.contains("[misc]") {
|
||||
debug!("Adding [misc] section to {}", path.display());
|
||||
string_prepend_str(contents, "[misc]\n");
|
||||
|
||||
File::create(path)
|
||||
.and_then(|mut f| f.write_all(contents.as_bytes()))
|
||||
.expect("Tried to auto-migrate the config file, unable to write to config file.\nPlease add \"[misc]\" section manually to the first line of the file.\nError");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Command line arguments
|
||||
@@ -565,7 +828,7 @@ impl CommandLineArgs {
|
||||
/// Represents the application configuration
|
||||
///
|
||||
/// The struct holds the loaded configuration file, as well as the arguments parsed from the command line.
|
||||
/// Its provided methods decide the appropriate options based on combining the configuraiton file and the
|
||||
/// Its provided methods decide the appropriate options based on combining the configuration file and the
|
||||
/// command line arguments.
|
||||
pub struct Config {
|
||||
opt: CommandLineArgs,
|
||||
@@ -576,7 +839,7 @@ pub struct Config {
|
||||
impl Config {
|
||||
/// Load the configuration.
|
||||
///
|
||||
/// The function parses the command line arguments and reading the configuration file.
|
||||
/// The function parses the command line arguments and reads the configuration file.
|
||||
pub fn load(opt: CommandLineArgs) -> Result<Self> {
|
||||
let config_directory = config_directory();
|
||||
let config_file = if config_directory.is_dir() {
|
||||
@@ -587,15 +850,17 @@ impl Config {
|
||||
ConfigFile::default()
|
||||
})
|
||||
} else {
|
||||
tracing::debug!("Configuration directory {} does not exist", config_directory.display());
|
||||
debug!("Configuration directory {} does not exist", config_directory.display());
|
||||
ConfigFile::default()
|
||||
};
|
||||
|
||||
check_deprecated!(config_file, git_arguments, git, arguments);
|
||||
check_deprecated!(config_file, git_repos, git, repos);
|
||||
check_deprecated!(config_file, predefined_git_repos, git, pull_predefined);
|
||||
check_deprecated!(config_file, yay_arguments, linux, yay_arguments);
|
||||
check_deprecated!(config_file, accept_all_windows_updates, windows, accept_all_updates);
|
||||
if let Some(misc) = &config_file.misc {
|
||||
check_deprecated!(misc, git_arguments, git, arguments);
|
||||
check_deprecated!(misc, git_repos, git, repos);
|
||||
check_deprecated!(misc, predefined_git_repos, git, pull_predefined);
|
||||
check_deprecated!(misc, yay_arguments, linux, yay_arguments);
|
||||
check_deprecated!(misc, accept_all_windows_updates, windows, accept_all_updates);
|
||||
}
|
||||
|
||||
let allowed_steps = Self::allowed_steps(&opt, &config_file);
|
||||
|
||||
@@ -628,7 +893,7 @@ impl Config {
|
||||
|
||||
/// The list of additional git repositories to pull.
|
||||
pub fn git_repos(&self) -> &Option<Vec<String>> {
|
||||
get_deprecated!(self.config_file, git_repos, git, repos)
|
||||
get_deprecated_moved_opt!(&self.config_file.misc, git_repos, &self.config_file.git, repos)
|
||||
}
|
||||
|
||||
/// Tell whether the specified step should run.
|
||||
@@ -643,8 +908,10 @@ impl Config {
|
||||
let mut enabled_steps: Vec<Step> = Vec::new();
|
||||
enabled_steps.extend(&opt.only);
|
||||
|
||||
if let Some(only) = config_file.only.as_ref() {
|
||||
enabled_steps.extend(only)
|
||||
if let Some(misc) = config_file.misc.as_ref() {
|
||||
if let Some(only) = misc.only.as_ref() {
|
||||
enabled_steps.extend(only);
|
||||
}
|
||||
}
|
||||
|
||||
if enabled_steps.is_empty() {
|
||||
@@ -653,27 +920,47 @@ impl Config {
|
||||
|
||||
let mut disabled_steps: Vec<Step> = Vec::new();
|
||||
disabled_steps.extend(&opt.disable);
|
||||
if let Some(disabled) = config_file.disable.as_ref() {
|
||||
disabled_steps.extend(disabled);
|
||||
if let Some(misc) = config_file.misc.as_ref() {
|
||||
if let Some(disabled) = misc.disable.as_ref() {
|
||||
disabled_steps.extend(disabled);
|
||||
}
|
||||
}
|
||||
|
||||
enabled_steps.retain(|e| !disabled_steps.contains(e) || opt.only.contains(e));
|
||||
enabled_steps
|
||||
}
|
||||
|
||||
/// Tell whether we should run a self update.
|
||||
/// Tell whether we should run a self-update.
|
||||
pub fn no_self_update(&self) -> bool {
|
||||
self.opt.no_self_update || self.config_file.no_self_update.unwrap_or(false)
|
||||
self.opt.no_self_update
|
||||
|| self
|
||||
.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.no_self_update)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Tell whether we should run in tmux.
|
||||
pub fn run_in_tmux(&self) -> bool {
|
||||
self.opt.run_in_tmux || self.config_file.run_in_tmux.unwrap_or(false)
|
||||
self.opt.run_in_tmux
|
||||
|| self
|
||||
.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.run_in_tmux)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Tell whether we should perform cleanup steps.
|
||||
pub fn cleanup(&self) -> bool {
|
||||
self.opt.cleanup || self.config_file.cleanup.unwrap_or(false)
|
||||
self.opt.cleanup
|
||||
|| self
|
||||
.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.cleanup)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Tell whether we are dry-running.
|
||||
@@ -683,32 +970,54 @@ impl Config {
|
||||
|
||||
/// Tell whether we should not attempt to retry anything.
|
||||
pub fn no_retry(&self) -> bool {
|
||||
self.opt.no_retry || self.config_file.no_retry.unwrap_or(false)
|
||||
self.opt.no_retry
|
||||
|| self
|
||||
.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.no_retry)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// List of remote hosts to run Topgrade in
|
||||
pub fn remote_topgrades(&self) -> &Option<Vec<String>> {
|
||||
&self.config_file.remote_topgrades
|
||||
pub fn remote_topgrades(&self) -> Option<&Vec<String>> {
|
||||
self.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.remote_topgrades.as_ref())
|
||||
}
|
||||
|
||||
/// Path to Topgrade executable used for all remote hosts
|
||||
pub fn remote_topgrade_path(&self) -> &str {
|
||||
self.config_file.remote_topgrade_path.as_deref().unwrap_or("topgrade")
|
||||
self.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.remote_topgrade_path.as_deref())
|
||||
.unwrap_or("topgrade")
|
||||
}
|
||||
|
||||
/// Extra SSH arguments
|
||||
pub fn ssh_arguments(&self) -> &Option<String> {
|
||||
&self.config_file.ssh_arguments
|
||||
pub fn ssh_arguments(&self) -> Option<&String> {
|
||||
self.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.ssh_arguments.as_ref())
|
||||
}
|
||||
|
||||
/// Extra Git arguments
|
||||
pub fn git_arguments(&self) -> &Option<String> {
|
||||
get_deprecated!(self.config_file, git_arguments, git, arguments)
|
||||
get_deprecated_moved_opt!(&self.config_file.misc, git_arguments, &self.config_file.git, arguments)
|
||||
}
|
||||
|
||||
/// Extra Tmux arguments
|
||||
pub fn tmux_arguments(&self) -> eyre::Result<Vec<String>> {
|
||||
let args = &self.config_file.tmux_arguments.as_deref().unwrap_or_default();
|
||||
pub fn tmux_arguments(&self) -> Result<Vec<String>> {
|
||||
let args = &self
|
||||
.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.tmux_arguments.as_ref())
|
||||
.map(String::to_owned)
|
||||
.unwrap_or_default();
|
||||
shell_words::split(args)
|
||||
// The only time the parse failed is in case of a missing close quote.
|
||||
// The error message looks like this:
|
||||
@@ -726,7 +1035,7 @@ impl Config {
|
||||
|
||||
/// Skip sending a notification at the end of a run
|
||||
pub fn skip_notify(&self) -> bool {
|
||||
if let Some(yes) = self.config_file.skip_notify {
|
||||
if let Some(yes) = self.config_file.misc.as_ref().and_then(|misc| misc.skip_notify) {
|
||||
return yes;
|
||||
}
|
||||
|
||||
@@ -735,12 +1044,16 @@ impl Config {
|
||||
|
||||
/// Whether to set the terminal title
|
||||
pub fn set_title(&self) -> bool {
|
||||
self.config_file.set_title.unwrap_or(true)
|
||||
self.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.set_title)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Whether to say yes to package managers
|
||||
pub fn yes(&self, step: Step) -> bool {
|
||||
if let Some(yes) = self.config_file.assume_yes {
|
||||
if let Some(yes) = self.config_file.misc.as_ref().and_then(|misc| misc.assume_yes) {
|
||||
return yes;
|
||||
}
|
||||
|
||||
@@ -757,18 +1070,22 @@ impl Config {
|
||||
|
||||
/// Bash-it branch
|
||||
pub fn bashit_branch(&self) -> &str {
|
||||
self.config_file.bashit_branch.as_deref().unwrap_or("stable")
|
||||
self.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.bashit_branch.as_deref())
|
||||
.unwrap_or("stable")
|
||||
}
|
||||
|
||||
/// Whether to accept all Windows updates
|
||||
pub fn accept_all_windows_updates(&self) -> bool {
|
||||
get_deprecated!(
|
||||
self.config_file,
|
||||
get_deprecated_moved_or_default_to!(
|
||||
&self.config_file.misc,
|
||||
accept_all_windows_updates,
|
||||
windows,
|
||||
accept_all_updates
|
||||
&self.config_file.windows,
|
||||
accept_all_updates,
|
||||
true
|
||||
)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Whether to self rename the Topgrade executable during the run
|
||||
@@ -836,7 +1153,11 @@ impl Config {
|
||||
|
||||
/// Whether to send a desktop notification at the beginning of every step
|
||||
pub fn notify_each_step(&self) -> bool {
|
||||
self.config_file.notify_each_step.unwrap_or(false)
|
||||
self.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.notify_each_step)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Extra garuda-update arguments
|
||||
@@ -962,7 +1283,7 @@ impl Config {
|
||||
self.config_file.git.as_ref().and_then(|git| git.max_concurrency)
|
||||
}
|
||||
|
||||
/// Should we power on vagrant boxes if needed
|
||||
/// Determine whether we should power on vagrant boxes
|
||||
pub fn vagrant_power_on(&self) -> Option<bool> {
|
||||
self.config_file.vagrant.as_ref().and_then(|vagrant| vagrant.power_on)
|
||||
}
|
||||
@@ -992,7 +1313,7 @@ impl Config {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Use distro-sync in Red Hat based distrbutions
|
||||
/// Use distro-sync in Red Hat based distributions
|
||||
pub fn redhat_distro_sync(&self) -> bool {
|
||||
self.config_file
|
||||
.linux
|
||||
@@ -1019,18 +1340,25 @@ impl Config {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Should we ignore failures for this step
|
||||
/// Determine if we should ignore failures for this step
|
||||
pub fn ignore_failure(&self, step: Step) -> bool {
|
||||
self.config_file
|
||||
.ignore_failures
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.ignore_failures.as_ref())
|
||||
.map(|v| v.contains(&step))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn use_predefined_git_repos(&self) -> bool {
|
||||
!self.opt.disable_predefined_git_repos
|
||||
&& get_deprecated!(self.config_file, predefined_git_repos, git, pull_predefined).unwrap_or(true)
|
||||
&& get_deprecated_moved_or_default_to!(
|
||||
&self.config_file.misc,
|
||||
predefined_git_repos,
|
||||
&self.config_file.git,
|
||||
pull_predefined,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
pub fn verbose(&self) -> bool {
|
||||
@@ -1056,7 +1384,11 @@ impl Config {
|
||||
/// If `true`, `sudo` should be called after `pre_commands` in order to elevate at the
|
||||
/// start of the session (and not in the middle).
|
||||
pub fn pre_sudo(&self) -> bool {
|
||||
self.config_file.pre_sudo.unwrap_or(false)
|
||||
self.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.pre_sudo)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -1150,11 +1482,19 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn display_time(&self) -> bool {
|
||||
self.config_file.display_time.unwrap_or(true)
|
||||
self.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.display_time)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn display_preamble(&self) -> bool {
|
||||
self.config_file.display_preamble.unwrap_or(true)
|
||||
self.config_file
|
||||
.misc
|
||||
.as_ref()
|
||||
.and_then(|misc| misc.display_preamble)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn should_run_custom_command(&self, name: &str) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user