#![allow(dead_code)]
use std::collections::HashSet;
use std::fs::{File, write};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::{env, fmt, fs};
use clap::{Parser, ValueEnum};
use clap_complete::Shell;
use color_eyre::eyre::Result;
use color_eyre::eyre::{Context, OptionExt};
use etcetera::base_strategy::BaseStrategy;
use indexmap::IndexMap;
use merge::Merge;
use regex::Regex;
use regex_split::RegexSplit;
use rust_i18n::t;
use serde::Deserialize;
use strum::IntoEnumIterator;
use tracing::{debug, error};
use crate::execution_context::RunType;
use crate::step::{DEPRECATED_STEPS, Step};
use crate::sudo::SudoKind;
use crate::terminal::print_warning;
use crate::utils::string_prepend_str;
pub static EXAMPLE_CONFIG: &str = include_str!("../config.example.toml");
pub const DEFAULT_LOG_LEVEL: &str = "warn";
#[allow(unused_macros)]
macro_rules! str_value {
($section:ident, $value:ident) => {
pub fn $value(&self) -> Option<&str> {
self.config_file
.$section
.as_ref()
.and_then(|section| section.$value.as_deref())
}
};
}
pub type Commands = IndexMap<String, String>;
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Include {
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
paths: Option<Vec<String>>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Containers {
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
ignored_containers: Option<Vec<String>>,
#[merge(strategy = merge::option::overwrite_none)]
runtime: Option<ContainerRuntime>,
#[merge(strategy = merge::option::overwrite_none)]
system_prune: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Mandb {
#[merge(strategy = merge::option::overwrite_none)]
enable: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Git {
#[merge(strategy = merge::option::overwrite_none)]
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>>,
#[merge(strategy = merge::option::overwrite_none)]
pull_predefined: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
fetch_only: Option<bool>,
}
#[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>>,
#[merge(strategy = merge::option::overwrite_none)]
power_on: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
always_suspend: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum UpdatesAutoReboot {
Yes,
#[default]
No,
Ask,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Windows {
#[merge(strategy = merge::option::overwrite_none)]
accept_all_updates: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
updates_auto_reboot: Option<UpdatesAutoReboot>,
#[merge(strategy = merge::option::overwrite_none)]
self_rename: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
open_remotes_in_new_terminal: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
wsl_update_pre_release: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
wsl_update_use_web_download: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
winget_silent_install: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
winget_use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Python {
#[merge(strategy = merge::option::overwrite_none)]
enable_pip_review: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
enable_pip_review_local: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
enable_pipupgrade: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
pipupgrade_arguments: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
poetry_force_self_update: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Conda {
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
env_names: Option<Vec<String>>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
env_paths: Option<Vec<String>>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Distrobox {
#[merge(strategy = merge::option::overwrite_none)]
use_root: Option<bool>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
containers: Option<Vec<String>>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Yarn {
#[merge(strategy = merge::option::overwrite_none)]
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct VitePlus {
#[merge(strategy = merge::option::overwrite_none)]
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct NPM {
#[merge(strategy = merge::option::overwrite_none)]
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Deno {
#[merge(strategy = merge::option::overwrite_none)]
version: Option<String>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Chezmoi {
#[merge(strategy = merge::option::overwrite_none)]
exclude_encrypted: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Mise {
#[merge(strategy = merge::option::overwrite_none)]
bump: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
interactive: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
jobs: Option<u32>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Firmware {
#[merge(strategy = merge::option::overwrite_none)]
upgrade: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Flatpak {
#[merge(strategy = merge::option::overwrite_none)]
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Pixi {
#[merge(strategy = merge::option::overwrite_none)]
include_release_notes: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Brew {
#[merge(strategy = merge::option::overwrite_none)]
greedy_cask: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
greedy_latest: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
greedy_auto_updates: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
autoremove: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
fetch_head: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Go {
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
gup_exclude: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, Clone, Copy, Default)]
#[serde(rename_all = "snake_case")]
pub enum ArchPackageManager {
#[default]
Autodetect,
Aura,
GarudaUpdate,
Pacman,
Pamac,
Paru,
Pikaur,
Shelly,
Trizen,
Yay,
}
#[derive(Clone, Copy, Debug, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ContainerRuntime {
#[default] Docker,
Podman,
}
impl fmt::Display for ContainerRuntime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContainerRuntime::Docker => write!(f, "docker"),
ContainerRuntime::Podman => write!(f, "podman"),
}
}
}
#[derive(Debug, Deserialize, Clone, Copy, Default, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum NixHandler {
#[default]
Autodetect,
Nh,
Vanilla,
}
#[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>,
#[merge(strategy = merge::option::overwrite_none)]
arch_package_manager: Option<ArchPackageManager>,
#[merge(strategy = merge::option::overwrite_none)]
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)]
shelly_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)]
nix_env_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
apt_arguments: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
enable_tlmgr: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
redhat_distro_sync: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
suse_dup: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
rpm_ostree: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
bootc: 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>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
home_manager_arguments: Option<Vec<String>>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Composer {
#[merge(strategy = merge::option::overwrite_none)]
self_update: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Vim {
#[merge(strategy = merge::option::overwrite_none)]
force_plug_update: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Misc {
#[merge(strategy = merge::option::overwrite_none)]
allow_root: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
pre_sudo: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
sudo_loop: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
sudo_loop_interval: Option<u16>,
#[merge(strategy = merge::option::overwrite_none)]
sudo_command: Option<SudoKind>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
disable: Option<Vec<Step>>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
first: Option<Vec<Step>>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
last: 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>>,
#[merge(strategy = merge::option::overwrite_none)]
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)]
tmux_arguments: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
set_title: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
display_time: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
assume_yes: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
ask_retry: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
auto_retry: Option<u16>,
#[merge(strategy = merge::option::overwrite_none)]
no_retry: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
show_skipped: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
run_in_tmux: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
tmux_session_mode: Option<TmuxSessionMode>,
#[merge(strategy = merge::option::overwrite_none)]
cleanup: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
notify_each_step: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
skip_notify: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
notify_end: Option<NotifyEnd>,
#[merge(strategy = merge::option::overwrite_none)]
bashit_branch: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
only: Option<Vec<Step>>,
#[merge(strategy = merge::option::overwrite_none)]
no_self_update: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
log_filters: Option<Vec<String>>,
#[merge(strategy = merge::option::overwrite_none)]
show_distribution_summary: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
nix_handler: Option<NixHandler>,
}
#[derive(Clone, Copy, Debug, Deserialize, ValueEnum, Default)]
#[clap(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum TmuxSessionMode {
#[default]
AttachIfNotInSession,
AttachAlways,
}
#[derive(Clone, Copy, Debug, Deserialize, ValueEnum, Default)]
#[clap(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum NotifyEnd {
#[default]
Always,
Never,
OnFailure,
}
pub struct TmuxConfig {
pub args: Vec<String>,
pub session_mode: TmuxSessionMode,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Lensfun {
#[merge(strategy = merge::option::overwrite_none)]
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct JuliaConfig {
#[merge(strategy = merge::option::overwrite_none)]
startup_file: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Zigup {
#[merge(strategy = merge::option::overwrite_none)]
target_versions: Option<Vec<String>>,
#[merge(strategy = merge::option::overwrite_none)]
install_dir: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
path_link: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
cleanup: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct VscodeConfig {
#[merge(strategy = merge::option::overwrite_none)]
profile: Option<String>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct DoomConfig {
#[merge(strategy = merge::option::overwrite_none)]
aot: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Cargo {
#[merge(strategy = merge::option::overwrite_none)]
git: Option<bool>,
#[merge(strategy = merge::option::overwrite_none)]
quiet: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Rustup {
#[merge(strategy = merge::option::overwrite_none)]
channels: Option<Vec<String>>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Pkgfile {
#[merge(strategy = merge::option::overwrite_none)]
enable: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
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>,
#[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>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
conda: Option<Conda>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
python: Option<Python>,
#[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)]
mandb: Option<Mandb>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
git: Option<Git>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
go: Option<Go>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
containers: Option<Containers>,
#[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)]
chezmoi: Option<Chezmoi>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
mise: Option<Mise>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
yarn: Option<Yarn>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
deno: Option<Deno>,
#[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)]
pixi: Option<Pixi>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
distrobox: Option<Distrobox>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
lensfun: Option<Lensfun>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
julia: Option<JuliaConfig>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
zigup: Option<Zigup>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
vscode: Option<VscodeConfig>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
doom: Option<DoomConfig>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
cargo: Option<Cargo>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
rustup: Option<Rustup>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
pkgfile: Option<Pkgfile>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
viteplus: Option<VitePlus>,
}
fn config_directory() -> PathBuf {
#[cfg(unix)]
return crate::XDG_DIRS.config_dir();
#[cfg(windows)]
return crate::WINDOWS_DIRS.config_dir();
}
#[derive(Deserialize, Default, Debug)]
struct ConfigFileIncludeOnly {
include: Option<Include>,
}
impl ConfigFile {
fn ensure() -> Result<(PathBuf, Vec<PathBuf>)> {
let mut res = (PathBuf::new(), Vec::new());
let config_directory = config_directory();
let possible_config_paths = [
config_directory.join("topgrade.toml"),
config_directory.join("topgrade/topgrade.toml"),
];
for path in &possible_config_paths {
if path.exists() {
debug!("Configuration at {}", path.display());
res.0.clone_from(path);
break;
}
}
res.1 = Self::ensure_topgrade_d(&config_directory)?;
if !res.0.exists() && res.1.is_empty() {
res.0.clone_from(&possible_config_paths[0]);
debug!("No configuration exists");
write(&res.0, EXAMPLE_CONFIG).map_err(|e| {
debug!(
"Unable to write the example configuration file to {}: {}. Using blank config.",
&res.0.display(),
e
);
e
})?;
}
Ok(res)
}
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.path().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)
}
fn read(config_path: Option<PathBuf>) -> Result<ConfigFile> {
let mut result = Self::default();
let config_path = if let Some(path) = config_path {
path
} else {
let (path, dir_include) = Self::ensure()?;
for include in dir_include {
let include_contents = fs::read_to_string(&include).inspect_err(|_| {
error!("Unable to read {}", include.display());
})?;
let include_contents_parsed = toml::from_str(include_contents.as_str()).inspect_err(|_| {
error!("Failed to deserialize {}", include.display());
})?;
result.merge(include_contents_parsed);
}
path
};
if config_path == PathBuf::default() {
return Ok(result);
}
let mut contents_non_split = fs::read_to_string(&config_path).inspect_err(|_| {
error!("Unable to read {}", config_path.display());
})?;
Self::ensure_misc_is_present(&mut contents_non_split, &config_path);
let regex_match_include = Regex::new(r"^\s*\[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).inspect_err(|_| {
error!("Failed to deserialize an include section of {}", config_path.display());
})?;
if let Some(includes) = &config_file_include_only.include {
if let Some(ref paths) = includes.paths {
for include in 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) => {
error!("Unable to read {}: {e}", include_path.display(),);
continue;
}
};
match toml::from_str::<Self>(&include_contents) {
Ok(include_parsed) => result.merge(include_parsed),
Err(e) => {
error!("Failed to deserialize {}: {e}", include_path.display(),);
continue;
}
};
}
}
}
match toml::from_str::<Self>(contents) {
Ok(contents) => result.merge(contents),
Err(e) => error!("Failed to deserialize {}: {e}", config_path.display(),),
}
}
debug!("Loaded configuration: {:?}", result);
Ok(result)
}
fn edit() -> Result<()> {
let config_path = Self::ensure()?.0;
debug!("Editing config file: {:?}", config_path);
edit::edit_file(&config_path).context("Failed to open configuration file editor")
}
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");
}
}
}
#[derive(Parser, Debug)]
#[command(name = "topgrade", version, styles = clap_cargo::style::CLAP_STYLING)]
pub struct CommandLineArgs {
#[arg(long = "edit-config")]
edit_config: bool,
#[arg(long = "config-reference")]
show_config_reference: bool,
#[arg(short = 't', long = "tmux")]
run_in_tmux: bool,
#[arg(long = "no-tmux")]
no_tmux: bool,
#[arg(short = 'c', long = "cleanup")]
cleanup: bool,
#[arg(short = 'n', long = "dry-run")]
dry_run: bool,
#[arg(short = 'r', long = "run-type", value_enum, default_value_t)]
run_type: RunType,
#[arg(long = "no-retry", hide = true)]
no_retry: bool,
#[arg(long = "no-ask-retry")]
no_ask_retry: bool,
#[arg(long = "auto-retry", value_name = "COUNT")]
auto_retry: Option<u16>,
#[arg(long = "disable", value_name = "STEP", value_enum, num_args = 1..)]
disable: Vec<Step>,
#[arg(long = "only", value_name = "STEP", value_enum, num_args = 1..)]
only: Vec<Step>,
#[arg(long = "custom-commands", value_name = "NAME", num_args = 1..)]
custom_commands: Vec<String>,
#[arg(long = "env", value_name = "NAME=VALUE", value_parser = env_args_parser, num_args = 1..)]
env: Vec<(String, String)>,
#[arg(short = 'v', long = "verbose")]
pub verbose: bool,
#[arg(short = 'k', long = "keep")]
keep_at_end: bool,
#[arg(long = "skip-notify", hide = true)]
skip_notify: bool,
#[arg(long = "notify-end", value_enum, default_value_t)]
notify_end: NotifyEnd,
#[arg(
short = 'y',
long = "yes",
value_name = "STEP",
value_enum,
num_args = 0..,
)]
yes: Option<Vec<Step>>,
#[arg(long = "disable-predefined-git-repos")]
disable_predefined_git_repos: bool,
#[arg(long = "config", value_name = "PATH")]
config: Option<PathBuf>,
#[arg(long = "remote-host-limit", value_name = "REGEX")]
remote_host_limit: Option<Regex>,
#[arg(long = "show-skipped")]
show_skipped: bool,
#[arg(long = "allow-root")]
allow_root: bool,
#[arg(long = "sudoloop")]
sudo_loop: bool,
#[arg(long = "sudoloop-interval", value_name = "SECONDS")]
sudo_loop_interval: Option<u16>,
#[arg(long, default_value = DEFAULT_LOG_LEVEL)]
pub log_filter: String,
#[arg(long, value_enum, hide = true)]
pub gen_completion: Option<Shell>,
#[arg(long, hide = true)]
pub gen_manpage: bool,
#[arg(long = "no-self-update")]
pub no_self_update: bool,
}
fn env_args_parser(arg: &str) -> Result<(String, String)> {
let (key, value) = arg
.split_once("=")
.ok_or_eyre("Environment variable must be in the format NAME=VALUE")?;
Ok((key.to_string(), value.to_string()))
}
impl CommandLineArgs {
pub fn edit_config(&self) -> bool {
self.edit_config
}
pub fn show_config_reference(&self) -> bool {
self.show_config_reference
}
pub fn env_variables(&self) -> &Vec<(String, String)> {
&self.env
}
pub fn tracing_filter_directives(&self) -> String {
let mut ret = self.log_filter.clone();
if self.verbose {
ret.push(',');
ret.push_str("debug");
}
ret
}
}
#[derive(Debug)]
pub struct Config {
opt: CommandLineArgs,
config_file: ConfigFile,
allowed_steps: Vec<Step>,
}
impl Config {
pub fn load(opt: CommandLineArgs) -> Result<Self> {
let config_directory = config_directory();
let config_file = if config_directory.is_dir() {
ConfigFile::read(opt.config.clone()).unwrap_or_else(|e| {
error!("failed to load configuration: {e}");
ConfigFile::default()
})
} else {
debug!("Configuration directory {} does not exist", config_directory.display());
ConfigFile::default()
};
let allowed_steps = Self::allowed_steps(&opt, &config_file);
Ok(Self {
opt,
config_file,
allowed_steps,
})
}
pub fn edit() -> Result<()> {
ConfigFile::edit()
}
pub fn pre_commands(&self) -> &Option<Commands> {
&self.config_file.pre_commands
}
pub fn post_commands(&self) -> &Option<Commands> {
&self.config_file.post_commands
}
pub fn commands(&self) -> &Option<Commands> {
&self.config_file.commands
}
pub fn conda_env_names(&self) -> Option<&Vec<String>> {
self.config_file
.conda
.as_ref()
.and_then(|conda| conda.env_names.as_ref())
}
pub fn conda_env_paths(&self) -> Option<&Vec<String>> {
self.config_file
.conda
.as_ref()
.and_then(|conda| conda.env_paths.as_ref())
}
pub fn git_repos(&self) -> Option<&Vec<String>> {
self.config_file.git.as_ref().and_then(|git| git.repos.as_ref())
}
pub fn containers_ignored_tags(&self) -> Option<&Vec<String>> {
self.config_file
.containers
.as_ref()
.and_then(|containers| containers.ignored_containers.as_ref())
}
pub fn containers_runtime(&self) -> ContainerRuntime {
self.config_file
.containers
.as_ref()
.and_then(|containers| containers.runtime)
.unwrap_or_default()
}
pub fn containers_system_prune(&self) -> bool {
self.config_file
.containers
.as_ref()
.and_then(|containers| containers.system_prune)
.unwrap_or(false)
}
pub fn containers_use_sudo(&self) -> bool {
self.config_file
.containers
.as_ref()
.and_then(|containers| containers.use_sudo)
.unwrap_or(false)
}
pub fn should_run(&self, step: Step) -> bool {
self.allowed_steps.contains(&step)
}
fn allowed_steps(opt: &CommandLineArgs, config_file: &ConfigFile) -> Vec<Step> {
let mut enabled_steps: Vec<Step> = Vec::new();
enabled_steps.extend(&opt.only);
if let Some(misc) = config_file.misc.as_ref()
&& let Some(only) = misc.only.as_ref()
{
enabled_steps.extend(only);
}
let step_is_deprecated = |x| DEPRECATED_STEPS.contains(&x);
if enabled_steps.is_empty() {
enabled_steps.extend(Step::iter());
enabled_steps.retain(|x| !step_is_deprecated(*x));
}
let mut disabled_steps: Vec<Step> = Vec::new();
disabled_steps.extend(&opt.disable);
if let Some(misc) = config_file.misc.as_ref()
&& let Some(disabled) = misc.disable.as_ref()
{
disabled_steps.extend(disabled);
}
for step in enabled_steps
.iter()
.chain(disabled_steps.iter())
.filter(|x| step_is_deprecated(**x))
{
print_warning(t!("`{step}` step is deprecated", step = format!("{step:?}")));
}
enabled_steps.retain(|e| !disabled_steps.contains(e) || opt.only.contains(e));
enabled_steps
}
pub fn no_self_update(&self) -> bool {
self.opt.no_self_update
|| self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.no_self_update)
.unwrap_or(false)
}
pub fn run_in_tmux(&self) -> bool {
!self.opt.no_tmux
&& (self.opt.run_in_tmux
|| self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.run_in_tmux)
.unwrap_or(false))
}
fn tmux_session_mode(&self) -> TmuxSessionMode {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.tmux_session_mode)
.unwrap_or_default()
}
pub fn cleanup(&self) -> bool {
self.opt.cleanup
|| self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.cleanup)
.unwrap_or(false)
}
pub fn run_type(&self) -> RunType {
if self.opt.dry_run {
RunType::Dry
} else {
self.opt.run_type
}
}
pub fn auto_retry(&self) -> u16 {
self.opt
.auto_retry
.or_else(|| self.config_file.misc.as_ref().and_then(|misc| misc.auto_retry))
.unwrap_or(0)
}
pub fn ask_retry(&self) -> bool {
if self.opt.no_ask_retry {
return false;
}
if self.opt.no_retry {
return false;
}
if self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.no_retry)
.unwrap_or(false)
{
return false;
}
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.ask_retry)
.unwrap_or(true)
}
pub fn env_variables(&self) -> &Vec<(String, String)> {
self.opt.env_variables()
}
pub fn remote_topgrades(&self) -> Option<&Vec<String>> {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.remote_topgrades.as_ref())
}
pub fn remote_topgrade_path(&self) -> &str {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.remote_topgrade_path.as_deref())
.unwrap_or("topgrade")
}
pub fn ssh_arguments(&self) -> Option<&String> {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.ssh_arguments.as_ref())
}
pub fn git_arguments(&self) -> Option<&String> {
self.config_file.git.as_ref().and_then(|git| git.arguments.as_ref())
}
pub fn git_fetch_only(&self) -> bool {
self.config_file
.git
.as_ref()
.and_then(|git| git.fetch_only)
.unwrap_or(false)
}
pub fn tmux_config(&self) -> Result<TmuxConfig> {
let args = self.tmux_arguments()?;
Ok(TmuxConfig {
args,
session_mode: self.tmux_session_mode(),
})
}
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)
.with_context(|| format!("Failed to parse `tmux_arguments`: `{args}`"))
}
pub fn keep_at_end(&self) -> bool {
self.opt.keep_at_end || env::var("TOPGRADE_KEEP_END").is_ok()
}
pub fn notify_end(&self) -> NotifyEnd {
let skip_notify = self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.skip_notify)
.unwrap_or(self.opt.skip_notify);
let notify_end = self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.notify_end)
.unwrap_or(self.opt.notify_end);
if skip_notify {
return NotifyEnd::Never;
}
notify_end
}
pub fn set_title(&self) -> bool {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.set_title)
.unwrap_or(true)
}
pub fn yes(&self, step: Step) -> bool {
if let Some(yes) = self.config_file.misc.as_ref().and_then(|misc| misc.assume_yes) {
return yes;
}
if let Some(yes_list) = &self.opt.yes {
if yes_list.is_empty() {
return true;
}
return yes_list.contains(&step);
}
false
}
pub fn bashit_branch(&self) -> &str {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.bashit_branch.as_deref())
.unwrap_or("stable")
}
pub fn accept_all_windows_updates(&self) -> bool {
self.config_file
.windows
.as_ref()
.and_then(|windows| windows.accept_all_updates)
.unwrap_or(true)
}
pub fn windows_updates_auto_reboot(&self) -> UpdatesAutoReboot {
self.config_file
.windows
.as_ref()
.and_then(|windows| windows.updates_auto_reboot)
.unwrap_or_default()
}
pub fn self_rename(&self) -> bool {
self.config_file
.windows
.as_ref()
.and_then(|w| w.self_rename)
.unwrap_or(false)
}
pub fn wsl_update_pre_release(&self) -> bool {
self.config_file
.windows
.as_ref()
.and_then(|w| w.wsl_update_pre_release)
.unwrap_or(false)
}
pub fn wsl_update_use_web_download(&self) -> bool {
self.config_file
.windows
.as_ref()
.and_then(|w| w.wsl_update_use_web_download)
.unwrap_or(false)
}
pub fn winget_use_sudo(&self) -> bool {
self.config_file
.windows
.as_ref()
.and_then(|w| w.winget_use_sudo)
.unwrap_or(false)
}
pub fn brew_cask_greedy(&self) -> bool {
self.config_file
.brew
.as_ref()
.and_then(|c| c.greedy_cask)
.unwrap_or(false)
}
pub fn brew_greedy_latest(&self) -> bool {
self.config_file
.brew
.as_ref()
.and_then(|c| c.greedy_latest)
.unwrap_or(false)
}
pub fn brew_greedy_auto_updates(&self) -> bool {
self.config_file
.brew
.as_ref()
.and_then(|c| c.greedy_auto_updates)
.unwrap_or(false)
}
pub fn brew_autoremove(&self) -> bool {
self.config_file
.brew
.as_ref()
.and_then(|c| c.autoremove)
.unwrap_or(false)
}
pub fn brew_fetch_head(&self) -> bool {
self.config_file
.brew
.as_ref()
.and_then(|c| c.fetch_head)
.unwrap_or(false)
}
pub fn composer_self_update(&self) -> bool {
self.config_file
.composer
.as_ref()
.and_then(|c| c.self_update)
.unwrap_or(false)
}
pub fn force_vim_plug_update(&self) -> bool {
self.config_file
.vim
.as_ref()
.and_then(|c| c.force_plug_update)
.unwrap_or(false)
}
pub fn gup_exclude(&self) -> &[String] {
self.config_file
.go
.as_ref()
.and_then(|g| g.gup_exclude.as_deref())
.unwrap_or(&[])
}
pub fn notify_each_step(&self) -> bool {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.notify_each_step)
.unwrap_or(false)
}
pub fn garuda_update_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.garuda_update_arguments.as_deref())
.unwrap_or("")
}
pub fn trizen_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.trizen_arguments.as_deref())
.unwrap_or("")
}
#[allow(dead_code)]
pub fn pikaur_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.pikaur_arguments.as_deref())
.unwrap_or("")
}
pub fn pamac_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.pamac_arguments.as_deref())
.unwrap_or("")
}
pub fn shelly_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.shelly_arguments.as_deref())
.unwrap_or("")
}
pub fn show_pixi_release_notes(&self) -> bool {
self.config_file
.pixi
.as_ref()
.and_then(|s| s.include_release_notes)
.unwrap_or(false)
}
pub fn show_arch_news(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|s| s.show_arch_news)
.unwrap_or(true)
}
pub fn arch_package_manager(&self) -> ArchPackageManager {
self.config_file
.linux
.as_ref()
.and_then(|s| s.arch_package_manager)
.unwrap_or_default()
}
pub fn yay_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.yay_arguments.as_deref())
.unwrap_or("")
}
pub fn aura_aur_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.aura_aur_arguments.as_deref())
.unwrap_or("")
}
pub fn aura_pacman_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.aura_pacman_arguments.as_deref())
.unwrap_or("")
}
pub fn apt_arguments(&self) -> Option<&str> {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.apt_arguments.as_deref())
}
pub fn dnf_arguments(&self) -> Option<&str> {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.dnf_arguments.as_deref())
}
pub fn nix_handler(&self) -> NixHandler {
self.config_file
.misc
.as_ref()
.and_then(|s| s.nix_handler)
.unwrap_or_default()
}
pub fn nix_arguments(&self) -> Option<&str> {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.nix_arguments.as_deref())
}
pub fn nix_env_arguments(&self) -> Option<&str> {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.nix_env_arguments.as_deref())
}
pub fn home_manager(&self) -> Option<&Vec<String>> {
self.config_file
.linux
.as_ref()
.and_then(|misc| misc.home_manager_arguments.as_ref())
}
pub fn distrobox_root(&self) -> bool {
self.config_file
.distrobox
.as_ref()
.and_then(|r| r.use_root)
.unwrap_or(false)
}
pub fn distrobox_containers(&self) -> Option<&Vec<String>> {
self.config_file.distrobox.as_ref().and_then(|r| r.containers.as_ref())
}
pub fn git_concurrency_limit(&self) -> Option<usize> {
self.config_file.git.as_ref().and_then(|git| git.max_concurrency)
}
pub fn vagrant_power_on(&self) -> Option<bool> {
self.config_file.vagrant.as_ref().and_then(|vagrant| vagrant.power_on)
}
pub fn vagrant_directories(&self) -> Option<&Vec<String>> {
self.config_file
.vagrant
.as_ref()
.and_then(|vagrant| vagrant.directories.as_ref())
}
pub fn vagrant_always_suspend(&self) -> Option<bool> {
self.config_file
.vagrant
.as_ref()
.and_then(|vagrant| vagrant.always_suspend)
}
pub fn enable_tlmgr_linux(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.enable_tlmgr)
.unwrap_or(false)
}
pub fn redhat_distro_sync(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.redhat_distro_sync)
.unwrap_or(false)
}
pub fn suse_dup(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.suse_dup)
.unwrap_or(false)
}
pub fn rpm_ostree(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.rpm_ostree)
.unwrap_or(false)
}
pub fn bootc(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.bootc)
.unwrap_or(false)
}
pub fn steps(&self) -> Result<impl Iterator<Item = Step> + '_> {
let misc = self.config_file.misc.as_ref();
let first = misc.and_then(|m| m.first.as_deref()).unwrap_or_default();
let last = misc.and_then(|m| m.last.as_deref()).unwrap_or_default();
let specified: HashSet<Step> = first.iter().chain(last).copied().collect();
if specified.len() != first.len() + last.len() {
color_eyre::eyre::bail!("All steps included in `misc.first` and `misc.last` must be unique");
}
Ok(first
.iter()
.copied()
.chain(
crate::step::default_steps()
.into_iter()
.filter(move |s| !specified.contains(s)),
)
.chain(last.iter().copied()))
}
pub fn ignore_failure(&self, step: Step) -> bool {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.ignore_failures.as_ref())
.is_some_and(|v| v.contains(&step))
}
pub fn use_predefined_git_repos(&self) -> bool {
!self.opt.disable_predefined_git_repos
&& self
.config_file
.git
.as_ref()
.and_then(|git| git.pull_predefined)
.unwrap_or(true)
}
pub fn cargo_update_git(&self) -> bool {
self.config_file
.cargo
.as_ref()
.and_then(|cargo| cargo.git)
.unwrap_or(true)
}
pub fn cargo_update_quiet(&self) -> bool {
self.config_file
.cargo
.as_ref()
.and_then(|cargo| cargo.quiet)
.unwrap_or(false)
}
pub fn rustup_channels(&self) -> Vec<String> {
self.config_file
.rustup
.as_ref()
.and_then(|rustup| rustup.channels.clone())
.unwrap_or_default()
}
pub fn verbose(&self) -> bool {
self.opt.verbose
}
pub fn tracing_filter_directives(&self) -> String {
let mut ret = String::new();
if let Some(directives) = self.config_file.misc.as_ref().and_then(|m| m.log_filters.as_ref()) {
ret.push_str(&directives.join(","));
}
ret.push(',');
ret.push_str(&self.opt.log_filter);
if self.verbose() {
ret.push_str(",debug");
}
ret
}
pub fn show_skipped(&self) -> bool {
self.opt.show_skipped
|| self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.show_skipped)
.unwrap_or(false)
}
pub fn enable_mandb(&self) -> bool {
self.config_file
.mandb
.as_ref()
.and_then(|mandb| mandb.enable)
.unwrap_or(false)
}
pub fn open_remotes_in_new_terminal(&self) -> bool {
self.config_file
.windows
.as_ref()
.and_then(|windows| windows.open_remotes_in_new_terminal)
.unwrap_or(false)
}
pub fn winget_silent_install(&self) -> bool {
self.config_file
.windows
.as_ref()
.and_then(|windows| windows.winget_silent_install)
.unwrap_or(true)
}
pub fn allow_root(&self) -> bool {
self.opt.allow_root
|| self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.allow_root)
.unwrap_or(false)
}
pub fn sudo_command(&self) -> Option<SudoKind> {
self.config_file.misc.as_ref().and_then(|misc| misc.sudo_command)
}
pub fn pre_sudo(&self) -> bool {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.pre_sudo)
.unwrap_or(false)
}
pub fn sudo_loop(&self) -> bool {
self.opt.sudo_loop
|| self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.sudo_loop)
.unwrap_or(false)
}
pub fn sudo_loop_interval(&self) -> u16 {
self.opt
.sudo_loop_interval
.or_else(|| self.config_file.misc.as_ref().and_then(|misc| misc.sudo_loop_interval))
.unwrap_or(240)
}
pub fn show_distribution_summary(&self) -> bool {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.show_distribution_summary)
.unwrap_or(true)
}
#[cfg(target_os = "linux")]
pub fn npm_use_sudo(&self) -> bool {
self.config_file
.npm
.as_ref()
.and_then(|npm| npm.use_sudo)
.unwrap_or(false)
}
#[cfg(target_os = "linux")]
pub fn yarn_use_sudo(&self) -> bool {
self.config_file
.yarn
.as_ref()
.and_then(|yarn| yarn.use_sudo)
.unwrap_or(false)
}
#[cfg(target_os = "linux")]
pub fn viteplus_use_sudo(&self) -> bool {
self.config_file
.viteplus
.as_ref()
.and_then(|viteplus| viteplus.use_sudo)
.unwrap_or(false)
}
pub fn deno_version(&self) -> Option<&str> {
self.config_file.deno.as_ref().and_then(|deno| deno.version.as_deref())
}
#[cfg(target_os = "linux")]
pub fn firmware_upgrade(&self) -> bool {
self.config_file
.firmware
.as_ref()
.and_then(|firmware| firmware.upgrade)
.unwrap_or(false)
}
#[cfg(target_os = "linux")]
pub fn flatpak_use_sudo(&self) -> bool {
self.config_file
.flatpak
.as_ref()
.and_then(|flatpak| flatpak.use_sudo)
.unwrap_or(false)
}
#[cfg(target_os = "linux")]
str_value!(linux, emerge_sync_flags);
#[cfg(target_os = "linux")]
str_value!(linux, emerge_update_flags);
pub fn should_execute_remote(&self, hostname: Result<String>, remote: &str) -> bool {
let remote_host = remote.split_once('@').map_or(remote, |(_, host)| host);
if let Ok(hostname) = hostname
&& remote_host == hostname
{
return false;
}
if let Some(limit) = &self.opt.remote_host_limit.as_ref() {
return limit.is_match(remote_host);
}
true
}
pub fn enable_pipupgrade(&self) -> bool {
self.config_file
.python
.as_ref()
.and_then(|python| python.enable_pipupgrade)
.unwrap_or(false)
}
pub fn pipupgrade_arguments(&self) -> &str {
self.config_file
.python
.as_ref()
.and_then(|s| s.pipupgrade_arguments.as_deref())
.unwrap_or("")
}
pub fn enable_pip_review(&self) -> bool {
self.config_file
.python
.as_ref()
.and_then(|python| python.enable_pip_review)
.unwrap_or(false)
}
pub fn enable_pip_review_local(&self) -> bool {
self.config_file
.python
.as_ref()
.and_then(|python| python.enable_pip_review_local)
.unwrap_or(false)
}
pub fn poetry_force_self_update(&self) -> bool {
self.config_file
.python
.as_ref()
.and_then(|python| python.poetry_force_self_update)
.unwrap_or(false)
}
pub fn display_time(&self) -> bool {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.display_time)
.unwrap_or(true)
}
pub fn should_run_custom_command(&self, name: &str) -> bool {
if self.opt.custom_commands.is_empty() {
return true;
}
self.opt.custom_commands.iter().any(|s| s == name)
}
pub fn lensfun_use_sudo(&self) -> bool {
self.config_file
.lensfun
.as_ref()
.and_then(|lensfun| lensfun.use_sudo)
.unwrap_or(false)
}
pub fn julia_use_startup_file(&self) -> bool {
self.config_file
.julia
.as_ref()
.and_then(|julia| julia.startup_file)
.unwrap_or(true)
}
pub fn zigup_target_versions(&self) -> Vec<String> {
self.config_file
.zigup
.as_ref()
.and_then(|zigup| zigup.target_versions.clone())
.unwrap_or(vec!["master".to_owned()])
}
pub fn zigup_install_dir(&self) -> Option<&str> {
self.config_file
.zigup
.as_ref()
.and_then(|zigup| zigup.install_dir.as_deref())
}
pub fn zigup_path_link(&self) -> Option<&str> {
self.config_file
.zigup
.as_ref()
.and_then(|zigup| zigup.path_link.as_deref())
}
pub fn zigup_cleanup(&self) -> bool {
self.config_file
.zigup
.as_ref()
.and_then(|zigup| zigup.cleanup)
.unwrap_or(false)
}
pub fn chezmoi_exclude_encrypted(&self) -> bool {
self.config_file
.chezmoi
.as_ref()
.and_then(|chezmoi| chezmoi.exclude_encrypted)
.unwrap_or(false)
}
pub fn mise_bump(&self) -> bool {
self.config_file
.mise
.as_ref()
.and_then(|mise| mise.bump)
.unwrap_or(false)
}
pub fn mise_jobs(&self) -> u32 {
self.config_file.mise.as_ref().and_then(|mise| mise.jobs).unwrap_or(4)
}
pub fn mise_interactive(&self) -> bool {
self.config_file
.mise
.as_ref()
.and_then(|mise| mise.interactive)
.unwrap_or(false)
}
pub fn vscode_profile(&self) -> Option<&str> {
let vscode_cfg = self.config_file.vscode.as_ref()?;
let profile = vscode_cfg.profile.as_ref()?;
if profile.is_empty() {
None
} else {
Some(profile.as_str())
}
}
pub fn doom_aot(&self) -> bool {
self.config_file
.doom
.as_ref()
.and_then(|doom| doom.aot)
.unwrap_or(false)
}
pub fn enable_pkgfile(&self) -> bool {
self.config_file
.pkgfile
.as_ref()
.and_then(|pkgfile| pkgfile.enable)
.unwrap_or(false)
}
}
#[cfg(test)]
mod test {
use crate::config::*;
use color_eyre::eyre::eyre;
use merge::Merge;
#[test]
fn merge_overwrite_none_preserves_left_values() {
let mut left = Containers {
ignored_containers: None,
runtime: Some(ContainerRuntime::Podman),
system_prune: Some(false),
use_sudo: None,
};
let right = Containers {
ignored_containers: None,
runtime: Some(ContainerRuntime::Docker),
system_prune: None,
use_sudo: Some(true),
};
left.merge(right);
assert!(matches!(left.runtime, Some(ContainerRuntime::Podman)));
assert_eq!(left.system_prune, Some(false));
assert_eq!(left.use_sudo, Some(true));
}
#[test]
fn test_default_config() {
let str = include_str!("../config.example.toml");
assert!(toml::from_str::<ConfigFile>(str).is_ok());
}
fn config() -> Config {
Config {
opt: CommandLineArgs::parse_from::<_, String>([]),
config_file: ConfigFile::default(),
allowed_steps: Vec::new(),
}
}
#[test]
fn test_should_execute_remote_different_hostname() {
assert!(config().should_execute_remote(Ok("hostname".to_string()), "remote_hostname"));
}
#[test]
fn test_should_execute_remote_different_hostname_with_user() {
assert!(config().should_execute_remote(Ok("hostname".to_string()), "user@remote_hostname"));
}
#[test]
fn test_should_execute_remote_unknown_hostname() {
assert!(config().should_execute_remote(Err(eyre!("failed to get hostname")), "remote_hostname"));
}
#[test]
fn test_should_not_execute_remote_same_hostname() {
assert!(!config().should_execute_remote(Ok("hostname".to_string()), "hostname"));
}
#[test]
fn test_should_not_execute_remote_same_hostname_with_user() {
assert!(!config().should_execute_remote(Ok("hostname".to_string()), "user@hostname"));
}
#[test]
fn test_should_execute_remote_matching_limit() {
let mut config = config();
config.opt = CommandLineArgs::parse_from(["topgrade", "--remote-host-limit", "remote_hostname"]);
assert!(config.should_execute_remote(Ok("hostname".to_string()), "user@remote_hostname"));
}
#[test]
fn test_should_not_execute_remote_not_matching_limit() {
let mut config = config();
config.opt = CommandLineArgs::parse_from(["topgrade", "--remote-host-limit", "other_hostname"]);
assert!(!config.should_execute_remote(Ok("hostname".to_string()), "user@remote_hostname"));
}
#[test]
fn test_custom_commands_order() {
let toml_str = r#"
[commands]
z = "cmd_z"
y = "cmd_y"
x = "cmd_x"
"#;
let order: Vec<_> = toml::from_str::<ConfigFile>(toml_str)
.expect("toml parse error")
.commands
.expect("commands field missing")
.keys()
.cloned()
.collect();
assert_eq!(order, vec!["z", "y", "x"]);
}
#[test]
fn test_env_variable_parser() {
let mut config = config();
config.opt = CommandLineArgs::parse_from(["topgrade", "--env", "VAR1=foo", "--env", "VAR2=bar"]);
let env_vars = config.env_variables();
assert_eq!(env_vars.len(), 2);
assert_eq!(env_vars[0], ("VAR1".to_string(), "foo".to_string()));
assert_eq!(env_vars[1], ("VAR2".to_string(), "bar".to_string()));
}
fn config_from_toml(toml_str: &str) -> Config {
Config {
opt: CommandLineArgs::parse_from::<_, String>([]),
config_file: toml::from_str(toml_str).expect("toml parse error"),
allowed_steps: Vec::new(),
}
}
#[test]
fn test_steps_default_order_without_first_or_last() {
let steps: Vec<Step> = config().steps().unwrap().collect();
assert_eq!(steps, crate::step::default_steps());
}
#[test]
fn test_steps_first_and_last_reorder() {
let config = config_from_toml(
r#"
[misc]
first = ["cargo", "rustup"]
last = ["chezmoi", "vim"]
"#,
);
let steps: Vec<Step> = config.steps().unwrap().collect();
assert_eq!(&steps[..2], &[Step::Cargo, Step::Rustup]);
assert_eq!(&steps[steps.len() - 2..], &[Step::Chezmoi, Step::Vim]);
for reordered in [Step::Cargo, Step::Rustup, Step::Chezmoi, Step::Vim] {
assert_eq!(steps.iter().filter(|&&s| s == reordered).count(), 1);
}
assert!(steps.contains(&Step::Remotes));
}
#[test]
fn test_steps_rejects_duplicates_in_first() {
let config = config_from_toml(
r#"
[misc]
first = ["cargo", "cargo"]
"#,
);
assert!(config.steps().is_err());
}
#[test]
fn test_steps_rejects_overlap_between_first_and_last() {
let config = config_from_toml(
r#"
[misc]
first = ["cargo"]
last = ["cargo"]
"#,
);
assert!(config.steps().is_err());
}
}