From 2e0e9ba5700f3aa06c082d906610ef167f90cc66 Mon Sep 17 00:00:00 2001 From: PolpOnline Date: Wed, 3 May 2023 21:11:59 +0200 Subject: [PATCH 01/12] Add the ability to have the config file in $XDG_CONFIG_HOME/topgrade/topgrade.toml --- src/config.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/config.rs b/src/config.rs index 278345e3..04579a5d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -353,8 +353,15 @@ impl ConfigFile { let config_directory = config_directory(); let config_path = config_directory.join("topgrade.toml"); + let alt_config_path = config_directory.join("topgrade/topgrade.toml"); - if !config_path.exists() { + 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 { debug!("No configuration exists"); write(&config_path, EXAMPLE_CONFIG).map_err(|e| { debug!( @@ -364,11 +371,8 @@ impl ConfigFile { ); e })?; - } else { - debug!("Configuration at {}", config_path.display()); + Ok(config_path) } - - Ok(config_path) } /// Read the configuration file. From 3842ab669ad329564471eb4f089b295304f5af6b Mon Sep 17 00:00:00 2001 From: PolpOnline Date: Thu, 4 May 2023 23:12:59 +0200 Subject: [PATCH 02/12] Added ability to include directories as an extension of the config file --- Cargo.lock | 24 +++++++ Cargo.toml | 2 + config.example.toml | 5 ++ src/config.rs | 170 +++++++++++++++++++++++++++++++++++++++----- src/utils.rs | 38 ++++++++++ 5 files changed, 221 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31e432a0..2c922cfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1088,6 +1088,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "merge" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9" +dependencies = [ + "merge_derive", + "num-traits", +] + +[[package]] +name = "merge_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "mime" version = "0.3.17" @@ -2137,6 +2159,7 @@ dependencies = [ "home", "lazy_static", "libc", + "merge", "nix 0.24.3", "notify-rust", "once_cell", @@ -2146,6 +2169,7 @@ dependencies = [ "self_update", "semver", "serde", + "serde_json", "shell-words", "shellexpand", "strum 0.24.1", diff --git a/Cargo.toml b/Cargo.toml index 256da2cc..8211df08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ home = "~0.5" etcetera = "~0.8" once_cell = "~1.17" serde = { version = "~1.0", features = ["derive"] } +serde_json = "~1.0" toml = "0.5" which_crate = { version = "~4.1", package = "which" } shellexpand = "~2.1" @@ -47,6 +48,7 @@ shell-words = "~1.1" color-eyre = "~0.6" tracing = { version = "~0.1", features = ["attributes", "log"] } tracing-subscriber = { version = "~0.3", features = ["env-filter", "time"] } +merge = "0.1.0" [target.'cfg(target_os = "macos")'.dependencies] notify-rust = "~4.5" diff --git a/config.example.toml b/config.example.toml index 87bbd71a..e6bfbe48 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,3 +1,8 @@ +# Include any additional configuration file(s) +# Note that this config file overrides the includes +# and includes that are on the left override the ones on the right in this array +#include = ["/etc/topgrade.toml"] + # Don't ask for confirmations #assume_yes = true diff --git a/src/config.rs b/src/config.rs index 04579a5d..8f33f695 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,6 +11,7 @@ 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 serde::Deserialize; use strum::{EnumIter, EnumString, EnumVariantNames, IntoEnumIterator}; @@ -165,24 +166,31 @@ pub enum Step { Yarn, } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] pub struct Git { max_concurrency: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] repos: Option>, + pull_predefined: Option, } -#[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>, + power_on: Option, always_suspend: Option, } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] pub struct Windows { accept_all_updates: Option, @@ -193,50 +201,52 @@ pub struct Windows { wsl_update_use_web_download: Option, } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] pub struct Python { enable_pip_review: Option, enable_pipupgrade: Option, } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] #[allow(clippy::upper_case_acronyms)] pub struct Distrobox { use_root: Option, + + #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] containers: Option>, } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] #[allow(clippy::upper_case_acronyms)] pub struct Yarn { use_sudo: Option, } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] #[allow(clippy::upper_case_acronyms)] pub struct NPM { use_sudo: Option, } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] #[allow(clippy::upper_case_acronyms)] pub struct Firmware { upgrade: Option, } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] #[allow(clippy::upper_case_acronyms)] pub struct Flatpak { use_sudo: Option, } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] pub struct Brew { greedy_cask: Option, @@ -257,86 +267,179 @@ 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, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] aura_aur_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] aura_pacman_arguments: Option, arch_package_manager: Option, show_arch_news: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] garuda_update_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] trizen_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] pikaur_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] pamac_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] dnf_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] nix_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] apt_arguments: Option, + enable_tlmgr: Option, redhat_distro_sync: Option, rpm_ostree: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] emerge_sync_flags: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] emerge_update_flags: Option, } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] pub struct Composer { self_update: Option, } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] pub struct Vim { force_plug_update: Option, } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] /// Configuration file pub struct ConfigFile { + #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] + include: Option>, + sudo_command: Option, + pre_sudo: Option, - pre_commands: Option, + + pre_commands: Option, // Probably Commands should be handled in another way + post_commands: Option, + commands: Option, + + #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] git_repos: Option>, + predefined_git_repos: Option, + + #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] disable: Option>, + + #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] ignore_failures: Option>, + + #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] remote_topgrades: Option>, + remote_topgrade_path: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] ssh_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] git_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] tmux_arguments: Option, + set_title: Option, + display_time: Option, + display_preamble: Option, + assume_yes: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] yay_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] aura_aur_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] aura_pacman_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] python: Option, + no_retry: Option, + run_in_tmux: Option, + cleanup: Option, + notify_each_step: Option, + accept_all_windows_updates: Option, + skip_notify: Option, + bashit_branch: Option, + + #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] only: Option>, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] composer: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] brew: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] linux: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] git: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] windows: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] npm: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] yarn: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] vim: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] firmware: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] vagrant: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] flatpak: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] distrobox: Option, + no_self_update: Option, } @@ -348,6 +451,12 @@ 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>, +} + impl ConfigFile { fn ensure() -> Result { let config_directory = config_directory(); @@ -390,11 +499,36 @@ impl ConfigFile { e })?; - let mut result: Self = toml::from_str(&contents).map_err(|e| { - tracing::error!("Failed to deserialize {}", config_path.display()); + let config_file_include_only: ConfigFileIncludeOnly = toml::from_str(&contents).map_err(|e| { + tracing::error!("Failed to deserialize the include section of {}", config_path.display()); e })?; + let mut result: Self = Default::default(); + + if let Some(includes) = &config_file_include_only.include { + for include in includes.iter().rev() { + let include_path = shellexpand::tilde::<&str>(&include.as_ref()).into_owned(); + let include_path = PathBuf::from(include_path); + let include_contents = fs::read_to_string(&include_path).map_err(|e| { + tracing::error!("Unable to read {}", include_path.display()); + e + })?; + + result.merge(toml::from_str::(&include_contents).map_err(|e| { + tracing::error!("Failed to deserialize {}", include_path.display()); + e + })?); + + debug!("Configuration include found: {}", include_path.display()); + } + } + + result.merge(toml::from_str::(&contents).map_err(|e| { + tracing::error!("Failed to deserialize {}", config_path.display()); + e + })?); + 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(); @@ -581,7 +715,7 @@ 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() }; diff --git a/src/utils.rs b/src/utils.rs index 23ec64ca..8f0e7bdc 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -152,3 +152,41 @@ pub fn hostname() -> Result { .map_err(|err| SkipStep(format!("Failed to get hostname: {err}")).into()) .map(|output| output.stdout.trim().to_owned()) } + +pub mod merge_strategies { + use merge::Merge; + + /// Prepends right to left (both Option>) + pub fn vec_prepend_opt(left: &mut Option>, right: Option>) { + if let Some(left_vec) = left { + if let Some(mut right_vec) = right { + right_vec.append(left_vec); + let _ = std::mem::replace(left, Some(right_vec)); + } + } else { + *left = right; + } + } + + /// Appends an Option to another Option + pub fn string_append_opt(left: &mut Option, right: Option) { + if let Some(left_str) = left { + if let Some(right_str) = right { + left_str.push(' '); + left_str.push_str(&right_str); + } + } else { + *left = right; + } + } + + pub fn inner_merge_opt(left: &mut Option, right: Option) where T: Merge { + if let Some(ref mut left_inner) = left { + if let Some(right_inner) = right { + left_inner.merge(right_inner); + } + } else { + *left = right; + } + } +} From e8a77e23951436dee193c3afc1573cb64fb3e398 Mon Sep 17 00:00:00 2001 From: PolpOnline Date: Thu, 4 May 2023 23:16:18 +0200 Subject: [PATCH 03/12] Removed useless dependency "serde_json" --- Cargo.lock | 1 - Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c922cfc..ca7bb3b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2169,7 +2169,6 @@ dependencies = [ "self_update", "semver", "serde", - "serde_json", "shell-words", "shellexpand", "strum 0.24.1", diff --git a/Cargo.toml b/Cargo.toml index 8211df08..83077aec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ home = "~0.5" etcetera = "~0.8" once_cell = "~1.17" serde = { version = "~1.0", features = ["derive"] } -serde_json = "~1.0" toml = "0.5" which_crate = { version = "~4.1", package = "which" } shellexpand = "~2.1" From 7f2a57c1c5fce60fcf40354f26d23e76978b9dda Mon Sep 17 00:00:00 2001 From: PolpOnline Date: Thu, 4 May 2023 23:18:51 +0200 Subject: [PATCH 04/12] fmt --- src/utils.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils.rs b/src/utils.rs index 8f0e7bdc..ec8c224a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -180,7 +180,10 @@ pub mod merge_strategies { } } - pub fn inner_merge_opt(left: &mut Option, right: Option) where T: Merge { + pub fn inner_merge_opt(left: &mut Option, right: Option) + where + T: Merge, + { if let Some(ref mut left_inner) = left { if let Some(right_inner) = right { left_inner.merge(right_inner); From e895bbbaebeb1cf0bd11f748133849f2a7fa3659 Mon Sep 17 00:00:00 2001 From: PolpOnline Date: Fri, 5 May 2023 20:31:19 +0200 Subject: [PATCH 05/12] Implemented merge strategy for the Commands type --- src/config.rs | 7 +++++-- src/utils.rs | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index 8f33f695..ebd32bba 100644 --- a/src/config.rs +++ b/src/config.rs @@ -66,7 +66,7 @@ macro_rules! get_deprecated { }; } -type Commands = BTreeMap; +pub type Commands = BTreeMap; #[derive(ArgEnum, EnumString, EnumVariantNames, Debug, Clone, PartialEq, Eq, Deserialize, EnumIter, Copy)] #[clap(rename_all = "snake_case")] @@ -336,10 +336,13 @@ pub struct ConfigFile { pre_sudo: Option, - pre_commands: Option, // Probably Commands should be handled in another way + #[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)] + pre_commands: Option, + #[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)] post_commands: Option, + #[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)] commands: Option, #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] diff --git a/src/utils.rs b/src/utils.rs index ec8c224a..42851a55 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -155,6 +155,7 @@ pub fn hostname() -> Result { pub mod merge_strategies { use merge::Merge; + use crate::config::Commands; /// Prepends right to left (both Option>) pub fn vec_prepend_opt(left: &mut Option>, right: Option>) { @@ -192,4 +193,14 @@ pub mod merge_strategies { *left = right; } } + + pub fn commands_merge_opt(left: &mut Option, right: Option) { + if let Some(ref mut left_inner) = left { + if let Some(right_inner) = right { + left_inner.extend(right_inner); + } + } else { + *left = right; + } + } } From 5b387b57ed31b01a42f2210580d13a808f1b8a10 Mon Sep 17 00:00:00 2001 From: PolpOnline Date: Fri, 5 May 2023 22:11:36 +0200 Subject: [PATCH 06/12] Do not stop loading included files if these are invalid/can't be loaded, but rather continue and load other configs --- src/config.rs | 33 +++++++++++++++++++++------------ src/utils.rs | 8 +++++--- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/config.rs b/src/config.rs index f5c78d0f..9dfd0053 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ #![allow(dead_code)] + use std::collections::BTreeMap; use std::fs::write; use std::path::PathBuf; @@ -515,24 +516,32 @@ impl ConfigFile { for include in includes.iter().rev() { let include_path = shellexpand::tilde::<&str>(&include.as_ref()).into_owned(); let include_path = PathBuf::from(include_path); - let include_contents = fs::read_to_string(&include_path).map_err(|e| { - tracing::error!("Unable to read {}", include_path.display()); - e - })?; - - result.merge(toml::from_str::(&include_contents).map_err(|e| { - tracing::error!("Failed to deserialize {}", include_path.display()); - e - })?); + let include_contents = match fs::read_to_string(&include_path) { + Ok(contents) => contents, + Err(e) => { + tracing::error!("Unable to read {}: {}", include_path.display(), e); + continue; + } + }; + let include_parsed = match toml::from_str::(&include_contents) { + Ok(contents) => contents, + Err(e) => { + tracing::error!("Failed to deserialize {}: {}", include_path.display(), e); + continue; + } + }; + + result.merge(include_parsed); debug!("Configuration include found: {}", include_path.display()); } } - result.merge(toml::from_str::(&contents).map_err(|e| { + if let Ok(contents) = toml::from_str::(&contents) { + result.merge(contents); + } else { tracing::error!("Failed to deserialize {}", config_path.display()); - e - })?); + } if let Some(ref mut paths) = &mut result.git_repos { for path in paths.iter_mut() { diff --git a/src/utils.rs b/src/utils.rs index 42851a55..f20a7423 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,12 +1,13 @@ -use crate::error::SkipStep; -use color_eyre::eyre::Result; - use std::env; use std::ffi::OsStr; use std::fmt::Debug; use std::path::{Path, PathBuf}; + +use color_eyre::eyre::Result; use tracing::{debug, error}; +use crate::error::SkipStep; + pub trait PathExt where Self: Sized, @@ -155,6 +156,7 @@ pub fn hostname() -> Result { pub mod merge_strategies { use merge::Merge; + use crate::config::Commands; /// Prepends right to left (both Option>) From 84ab841fca94f696ae96922dc91ec0044566e536 Mon Sep 17 00:00:00 2001 From: PolpOnline Date: Wed, 24 May 2023 20:36:00 +0200 Subject: [PATCH 07/12] Process includes in the order they are declared. Move all global scope config entries to a new [misc] entry. --- Cargo.lock | 14 +- Cargo.toml | 3 +- src/config.rs | 350 +++++++++++++++++++++++++++++++++----------------- 3 files changed, 249 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca7bb3b8..cf364747 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1516,9 +1516,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.6" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ "aho-corasick", "memchr", @@ -1534,6 +1534,15 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-split" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "544861d1810a3e429bb5e80266e537138b0e69e59bed9334326ae129a4c3e676" +dependencies = [ + "regex", +] + [[package]] name = "regex-syntax" version = "0.6.29" @@ -2165,6 +2174,7 @@ dependencies = [ "once_cell", "parselnk", "regex", + "regex-split", "rust-ini", "self_update", "semver", diff --git a/Cargo.toml b/Cargo.toml index 83077aec..c7c7d94c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,13 +41,14 @@ tempfile = "~3.2" cfg-if = "~1.0" tokio = { version = "~1.18", features = ["process", "rt-multi-thread"] } futures = "~0.3" -regex = "~1.5" +regex = "~1.7" semver = "~1.0" shell-words = "~1.1" color-eyre = "~0.6" tracing = { version = "~0.1", features = ["attributes", "log"] } tracing-subscriber = { version = "~0.3", features = ["env-filter", "time"] } merge = "0.1.0" +regex-split = "0.1.0" [target.'cfg(target_os = "macos")'.dependencies] notify-rust = "~4.5" diff --git a/src/config.rs b/src/config.rs index 62c6d524..9b23a2c9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,6 +14,7 @@ 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; @@ -53,18 +54,40 @@ 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; + }}; +} + +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; @@ -169,6 +192,13 @@ pub enum Step { Yarn, } +#[derive(Deserialize, Default, Debug, Merge)] +#[serde(deny_unknown_fields)] +pub struct Include { + #[merge(strategy = merge::vec::append)] + paths: Vec, +} + #[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] pub struct Git { @@ -329,26 +359,12 @@ pub struct Vim { force_plug_update: Option, } +// TODO: Auto-migrate configs without [misc] #[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] -/// Configuration file -pub struct ConfigFile { - #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] - include: Option>, - - sudo_command: Option, - +pub struct Misc { pre_sudo: Option, - #[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)] - pre_commands: Option, - - #[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)] - post_commands: Option, - - #[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)] - commands: Option, - #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] git_repos: Option>, @@ -391,9 +407,6 @@ pub struct ConfigFile { #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] aura_pacman_arguments: Option, - #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] - python: Option, - no_retry: Option, run_in_tmux: Option, @@ -411,6 +424,33 @@ pub struct ConfigFile { #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] only: Option>, + no_self_update: Option, +} + +#[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, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] + misc: Option, + + sudo_command: Option, + + #[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)] + pre_commands: Option, + + #[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)] + post_commands: Option, + + #[merge(strategy = crate::utils::merge_strategies::commands_merge_opt)] + commands: Option, + + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] + python: Option, + #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] composer: Option, @@ -446,8 +486,6 @@ pub struct ConfigFile { #[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)] distrobox: Option, - - no_self_update: Option, } fn config_directory() -> PathBuf { @@ -461,7 +499,7 @@ fn config_directory() -> PathBuf { /// 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: Option, } impl ConfigFile { @@ -493,7 +531,7 @@ impl ConfigFile { /// 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) -> Result { let config_path = if let Some(path) = config_path { path @@ -501,54 +539,59 @@ impl ConfigFile { Self::ensure()? }; - let contents = fs::read_to_string(&config_path).map_err(|e| { + let contents_non_split = fs::read_to_string(&config_path).map_err(|e| { tracing::error!("Unable to read {}", config_path.display()); e })?; - let config_file_include_only: ConfigFileIncludeOnly = toml::from_str(&contents).map_err(|e| { - tracing::error!("Failed to deserialize the include section of {}", config_path.display()); - e - })?; + let mut result = Self::default(); + + 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 + })?; - let mut result: Self = Default::default(); - - if let Some(includes) = &config_file_include_only.include { - for include in includes.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(contents) => contents, - Err(e) => { - tracing::error!("Unable to read {}: {}", include_path.display(), e); - continue; - } - }; - let include_parsed = match toml::from_str::(&include_contents) { - Ok(contents) => contents, - Err(e) => { - tracing::error!("Failed to deserialize {}: {}", include_path.display(), e); - continue; - } - }; - - result.merge(include_parsed); - - debug!("Configuration include found: {}", include_path.display()); + 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::(&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()); + } } - } - if let Ok(contents) = toml::from_str::(&contents) { - result.merge(contents); - } else { - tracing::error!("Failed to deserialize {}", config_path.display()); + match toml::from_str::(contents) { + Ok(contents) => result.merge(contents), + Err(e) => tracing::error!("Failed to deserialize {}: {}", config_path.display(), e), + } } - 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; + 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; + } } } @@ -708,7 +751,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, @@ -719,7 +762,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 { let config_directory = config_directory(); let config_file = if config_directory.is_dir() { @@ -734,11 +777,13 @@ impl Config { 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); @@ -771,7 +816,7 @@ impl Config { /// The list of additional git repositories to pull. pub fn git_repos(&self) -> &Option> { - 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. @@ -786,8 +831,10 @@ impl Config { let mut enabled_steps: Vec = 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() { @@ -796,27 +843,47 @@ impl Config { let mut disabled_steps: Vec = 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. @@ -826,32 +893,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> { - &self.config_file.remote_topgrades + pub fn remote_topgrades(&self) -> Option<&Vec> { + 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 { - &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 { - 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> { - let args = &self.config_file.tmux_arguments.as_deref().unwrap_or_default(); + pub fn tmux_arguments(&self) -> Result> { + 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: @@ -869,7 +958,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; } @@ -878,12 +967,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; } @@ -900,18 +993,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 @@ -979,7 +1076,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 @@ -1105,7 +1206,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 { self.config_file.vagrant.as_ref().and_then(|vagrant| vagrant.power_on) } @@ -1135,7 +1236,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 @@ -1162,18 +1263,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 { @@ -1199,7 +1307,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")] @@ -1285,11 +1397,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 { From 87537367882f784bdd9ccad385cc31254e37d8d3 Mon Sep 17 00:00:00 2001 From: PolpOnline Date: Wed, 24 May 2023 20:39:51 +0200 Subject: [PATCH 08/12] remove unused import --- src/config.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 9b23a2c9..d6811027 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,7 +8,6 @@ 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; From afe9e9bd0d40ce8078c46eed95e52a60308f3c5d Mon Sep 17 00:00:00 2001 From: PolpOnline Date: Wed, 24 May 2023 21:20:43 +0200 Subject: [PATCH 09/12] Auto-migrate configs without [misc] --- src/config.rs | 21 ++++++++++++++++++--- src/utils.rs | 9 +++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index d6811027..cc7e506e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,8 @@ #![allow(dead_code)] use std::collections::BTreeMap; -use std::fs::write; +use std::fs::{write, File}; +use std::io::Write; use std::path::PathBuf; use std::process::Command; use std::{env, fs}; @@ -21,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}; @@ -358,7 +360,6 @@ pub struct Vim { force_plug_update: Option, } -// TODO: Auto-migrate configs without [misc] #[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] pub struct Misc { @@ -538,11 +539,13 @@ impl ConfigFile { Self::ensure()? }; - let contents_non_split = 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 })?; + Self::ensure_misc_is_present(&mut contents_non_split, &config_path); + let mut result = Self::default(); let regex_match_include = Regex::new(r"\[include]").expect("Failed to compile regex"); @@ -621,6 +624,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. Please add \"[misc]\" section manually to the first line of the file."); + } + } } // Command line arguments diff --git a/src/utils.rs b/src/utils.rs index f20a7423..b239d051 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -102,6 +102,15 @@ pub fn require_option(option: Option, cause: String) -> Result { } } + +pub fn string_prepend_str(string: &mut String, s: &str) { + let mut new_string = String::with_capacity(string.len() + s.len()); + new_string.push_str(s); + new_string.push_str(string); + *string = new_string; +} + + /* sys-info-rs * * Copyright (c) 2015 Siyu Wang From 07d3919f0fcca344ba070ca818ace48ceb63d3c3 Mon Sep 17 00:00:00 2001 From: PolpOnline Date: Wed, 24 May 2023 21:28:19 +0200 Subject: [PATCH 10/12] Update config.example.toml to reflect changes made to the code --- config.example.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config.example.toml b/config.example.toml index a14ace64..8dd83224 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,8 +1,9 @@ # Include any additional configuration file(s) -# Note that this config file overrides the includes -# and includes that are on the left override the ones on the right in this array -#include = ["/etc/topgrade.toml"] +# [include] sections are processed in the order you write them +[include] +#paths = ["/etc/topgrade.toml"] +[misc] # Don't ask for confirmations #assume_yes = true From 33d1fc6e6ea10bdd3ba53b06c9767720f8f95a33 Mon Sep 17 00:00:00 2001 From: PolpOnline Date: Thu, 25 May 2023 00:08:30 +0200 Subject: [PATCH 11/12] Read all files in $CONFIG_DIR/topgrade/topgrade.d/ as part of the config, with the lowest priority --- config.example.toml | 1 + src/config.rs | 98 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/config.example.toml b/config.example.toml index b490d51f..5f311b68 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,5 +1,6 @@ # Include any additional configuration file(s) # [include] sections are processed in the order you write them +# Files in $CONFIG_DIR/topgrade/topgrade.d/ are automatically included at the beginning of this file [include] #paths = ["/etc/topgrade.toml"] diff --git a/src/config.rs b/src/config.rs index a24d747c..3037b9aa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use std::fs::{write, File}; use std::io::Write; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Command; use std::{env, fs}; @@ -506,40 +506,86 @@ struct ConfigFileIncludeOnly { } impl ConfigFile { - fn ensure() -> Result { + /// 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)> { + 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"), + ]; + + // 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 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 { + // If no config file exists, create 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> { + 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. fn read(config_path: Option) -> Result { + let mut should_read_include_dir = false; + let mut dir_include: Vec = Vec::new(); let config_path = if let Some(path) = config_path { path } else { - Self::ensure()? + should_read_include_dir = true; + let (path, include) = Self::ensure()?; + { + dir_include = include; + path + } }; let mut contents_non_split = fs::read_to_string(&config_path).map_err(|e| { @@ -551,6 +597,24 @@ impl ConfigFile { let mut result = Self::default(); + // The Function was called without a config_path, so we need to read the include directory + if should_read_include_dir { + 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); + } + } + + // 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()); @@ -614,7 +678,7 @@ impl ConfigFile { } fn edit() -> Result<()> { - let config_path = Self::ensure()?; + let config_path = Self::ensure()?.0; let editor = editor(); debug!("Editor: {:?}", editor); @@ -636,7 +700,7 @@ impl ConfigFile { 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. Please add \"[misc]\" section manually to the first line of the file."); + .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"); } } } From 2c45801a0b79527ab45f7ecc47e6edf9d8e826a6 Mon Sep 17 00:00:00 2001 From: PolpOnline Date: Thu, 25 May 2023 11:51:54 +0200 Subject: [PATCH 12/12] Refactor PR code, fmt --- src/config.rs | 40 ++++++++++++++++++---------------------- src/utils.rs | 2 -- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/config.rs b/src/config.rs index 3037b9aa..f37c931c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -530,7 +530,7 @@ impl ConfigFile { res.1 = Self::ensure_topgrade_d(&config_directory)?; - // If no config file exists, create default one in the 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(&res.0, EXAMPLE_CONFIG).map_err(|e| { @@ -575,30 +575,17 @@ impl ConfigFile { /// /// If the configuration file does not exist, the function returns the default ConfigFile. fn read(config_path: Option) -> Result { - let mut should_read_include_dir = false; - let mut dir_include: Vec = Vec::new(); + let mut result = Self::default(); + let config_path = if let Some(path) = config_path { path } else { - should_read_include_dir = true; - let (path, include) = Self::ensure()?; - { - dir_include = include; - path - } - }; - - let mut contents_non_split = fs::read_to_string(&config_path).map_err(|e| { - tracing::error!("Unable to read {}", config_path.display()); - e - })?; - - Self::ensure_misc_is_present(&mut contents_non_split, &config_path); - - let mut result = Self::default(); + let (path, dir_include) = Self::ensure()?; - // The Function was called without a config_path, so we need to read the include directory - if should_read_include_dir { + /* + 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()); @@ -611,7 +598,16 @@ impl ConfigFile { result.merge(include_contents_parsed); } - } + + path + }; + + let mut contents_non_split = fs::read_to_string(&config_path).map_err(|e| { + tracing::error!("Unable to read {}", config_path.display()); + e + })?; + + Self::ensure_misc_is_present(&mut contents_non_split, &config_path); // To parse [include] sections in the order as they are written, // we split the file and parse each part as a separate file diff --git a/src/utils.rs b/src/utils.rs index b239d051..472e26a0 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -102,7 +102,6 @@ pub fn require_option(option: Option, cause: String) -> Result { } } - pub fn string_prepend_str(string: &mut String, s: &str) { let mut new_string = String::with_capacity(string.len() + s.len()); new_string.push_str(s); @@ -110,7 +109,6 @@ pub fn string_prepend_str(string: &mut String, s: &str) { *string = new_string; } - /* sys-info-rs * * Copyright (c) 2015 Siyu Wang