Files
topgrade/src/config.rs
Ned Wolpert 78dc567226 Added an Audit step for FreeBSD and DragonFly packagees. (#640)
* Added an Audit step for FreeBSD and DragonFly.

Allows for auditing the packages to be disabled since they are breaking steps.
Current behaivor is the default, where if the audit fails topgrade stops. Can
be disabled in the [misc] section independenly from other sections
2024-01-08 09:40:01 +08:00

1519 lines
43 KiB
Rust

#![allow(dead_code)]
use std::collections::BTreeMap;
use std::fs::{write, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
use clap::{Parser, ValueEnum};
use clap_complete::Shell;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
use merge::Merge;
use regex::Regex;
use regex_split::RegexSplit;
use serde::Deserialize;
use strum::{EnumIter, EnumString, EnumVariantNames, IntoEnumIterator};
use which_crate::which;
use super::utils::editor;
use crate::command::CommandExt;
use crate::sudo::SudoKind;
use crate::utils::{hostname, string_prepend_str};
use tracing::{debug, error};
pub static EXAMPLE_CONFIG: &str = include_str!("../config.example.toml");
/// Topgrade's default log level.
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 = BTreeMap<String, String>;
#[derive(ValueEnum, EnumString, EnumVariantNames, Debug, Clone, PartialEq, Eq, Deserialize, EnumIter, Copy)]
#[clap(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum Step {
AM,
AppMan,
Asdf,
Atom,
Audit,
Bin,
Bob,
BrewCask,
BrewFormula,
Bun,
BunPackages,
Cargo,
Chezmoi,
Chocolatey,
Choosenim,
Composer,
Conda,
ConfigUpdate,
Containers,
CustomCommands,
DebGet,
Deno,
Distrobox,
DkpPacman,
Dotnet,
Emacs,
Firmware,
Flatpak,
Flutter,
Fossil,
Gcloud,
Gem,
Ghcup,
GithubCliExtensions,
GitRepos,
GnomeShellExtensions,
Go,
Guix,
Haxelib,
Helm,
HomeManager,
Jetpack,
Julia,
Juliaup,
Kakoune,
Helix,
Krew,
Lure,
Macports,
Mamba,
Miktex,
Mas,
Maza,
Micro,
Myrepos,
Nix,
Node,
Opam,
Pacdef,
Pacstall,
Pearl,
Pip3,
PipReview,
PipReviewLocal,
Pipupgrade,
Pipx,
Pkg,
Pkgin,
Pnpm,
Powershell,
Protonup,
Raco,
Rcm,
Remotes,
Restarts,
Rtcl,
RubyGems,
Rustup,
Scoop,
Sdkman,
SelfUpdate,
Sheldon,
Shell,
Snap,
Sparkle,
Spicetify,
Stack,
Stew,
System,
Tldr,
Tlmgr,
Tmux,
Toolbx,
Vagrant,
Vcpkg,
Vim,
Vscode,
Winget,
Wsl,
WslUpdate,
Yadm,
Yarn,
}
#[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>>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Git {
max_concurrency: Option<usize>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
repos: Option<Vec<String>>,
pull_predefined: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Vagrant {
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
directories: Option<Vec<String>>,
power_on: Option<bool>,
always_suspend: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Windows {
accept_all_updates: Option<bool>,
self_rename: Option<bool>,
open_remotes_in_new_terminal: Option<bool>,
enable_winget: Option<bool>,
wsl_update_pre_release: Option<bool>,
wsl_update_use_web_download: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Python {
enable_pip_review: Option<bool>,
enable_pip_review_local: Option<bool>,
enable_pipupgrade: Option<bool>,
pipupgrade_arguments: Option<String>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Distrobox {
use_root: Option<bool>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
containers: Option<Vec<String>>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Yarn {
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct NPM {
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Firmware {
upgrade: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Flatpak {
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Brew {
greedy_cask: Option<bool>,
autoremove: Option<bool>,
}
#[derive(Debug, Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum ArchPackageManager {
Autodetect,
Aura,
GarudaUpdate,
Pacman,
Pamac,
Paru,
Pikaur,
Trizen,
Yay,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Linux {
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
yay_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
aura_aur_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
aura_pacman_arguments: Option<String>,
arch_package_manager: Option<ArchPackageManager>,
show_arch_news: Option<bool>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
garuda_update_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
trizen_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
pikaur_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
pamac_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
dnf_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
nix_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
nix_env_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
apt_arguments: Option<String>,
enable_tlmgr: Option<bool>,
redhat_distro_sync: Option<bool>,
suse_dup: Option<bool>,
rpm_ostree: Option<bool>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
emerge_sync_flags: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
emerge_update_flags: Option<String>,
#[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 {
self_update: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Vim {
force_plug_update: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Misc {
pre_sudo: Option<bool>,
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)]
ignore_failures: Option<Vec<Step>>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
remote_topgrades: Option<Vec<String>>,
remote_topgrade_path: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
ssh_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
tmux_arguments: Option<String>,
set_title: Option<bool>,
display_time: Option<bool>,
assume_yes: Option<bool>,
no_retry: Option<bool>,
run_in_tmux: Option<bool>,
cleanup: Option<bool>,
notify_each_step: Option<bool>,
skip_notify: Option<bool>,
bashit_branch: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
only: Option<Vec<Step>>,
no_self_update: Option<bool>,
log_filters: Option<Vec<String>>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
/// Configuration file
pub struct ConfigFile {
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
include: Option<Include>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
misc: Option<Misc>,
#[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)]
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)]
git: Option<Git>,
#[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)]
yarn: Option<Yarn>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
vim: Option<Vim>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
firmware: Option<Firmware>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
vagrant: Option<Vagrant>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
flatpak: Option<Flatpak>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
distrobox: Option<Distrobox>,
}
fn config_directory() -> PathBuf {
#[cfg(unix)]
return crate::XDG_DIRS.config_dir();
#[cfg(windows)]
return crate::WINDOWS_DIRS.config_dir();
}
/// The only purpose of this struct is to deserialize only the `include` field of the config file.
#[derive(Deserialize, Default, Debug)]
struct ConfigFileIncludeOnly {
include: Option<Include>,
}
impl ConfigFile {
/// Returns the main config file and any additional config files
/// 0 = main config file
/// 1 = additional config files coming from topgrade.d
fn ensure() -> Result<(PathBuf, Vec<PathBuf>)> {
let mut res = (PathBuf::new(), Vec::new());
let config_directory = config_directory();
let 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 no config file exists, create a default one in the config directory
if !res.0.exists() && res.1.is_empty() {
res.0 = possible_config_paths[0].clone();
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)
}
/// Searches topgrade.d for additional config files
fn ensure_topgrade_d(config_directory: &Path) -> Result<Vec<PathBuf>> {
let mut res = Vec::new();
let dir_to_search = config_directory.join("topgrade.d");
if dir_to_search.exists() {
for entry in fs::read_dir(dir_to_search)? {
let entry = entry?;
if entry.file_type()?.is_file() {
debug!(
"Found additional (directory) configuration file at {}",
entry.path().display()
);
res.push(entry.path());
}
}
res.sort();
} else {
debug!("No additional configuration directory exists, creating one");
fs::create_dir_all(&dir_to_search)?;
}
Ok(res)
}
/// Read the configuration file.
///
/// If the configuration file does not exist, the function returns the default ConfigFile.
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()?;
/*
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| {
error!("Unable to read {}", include.display());
e
})?;
let include_contents_parsed = toml::from_str(include_contents.as_str()).map_err(|e| {
error!("Failed to deserialize {}", include.display());
e
})?;
result.merge(include_contents_parsed);
}
path
};
if config_path == PathBuf::default() {
// Here we expect topgrade.d and consequently result is not empty.
// If empty, Self:: ensure() would have created the default config.
return Ok(result);
}
let mut contents_non_split = fs::read_to_string(&config_path).map_err(|e| {
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
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).map_err(|e| {
error!("Failed to deserialize an include section of {}", config_path.display());
e
})?;
if let Some(includes) = &config_file_include_only.include {
// Parses the [include] section present in the slice
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 {}: {}", include_path.display(), e);
continue;
}
};
match toml::from_str::<Self>(&include_contents) {
Ok(include_parsed) => result.merge(include_parsed),
Err(e) => {
error!("Failed to deserialize {}: {}", include_path.display(), e);
continue;
}
};
}
}
}
match toml::from_str::<Self>(contents) {
Ok(contents) => result.merge(contents),
Err(e) => error!("Failed to deserialize {}: {}", config_path.display(), e),
}
}
if let Some(paths) = result.git.as_mut().and_then(|git| git.repos.as_mut()) {
for path in paths.iter_mut() {
let expanded = shellexpand::tilde::<&str>(&path.as_ref()).into_owned();
debug!("Path {} expanded to {}", path, expanded);
*path = expanded;
}
}
debug!("Loaded configuration: {:?}", result);
Ok(result)
}
fn edit() -> Result<()> {
let config_path = Self::ensure()?.0;
let editor = editor();
debug!("Editor: {:?}", editor);
let command = which(&editor[0])?;
let args: Vec<&String> = editor.iter().skip(1).collect();
Command::new(command)
.args(args)
.arg(config_path)
.status_checked()
.context("Failed to open configuration file editor")
}
/// [Misc] was added later, here we check if it is present in the config file and add it if not
fn ensure_misc_is_present(contents: &mut String, path: &PathBuf) {
if !contents.contains("[misc]") {
debug!("Adding [misc] section to {}", path.display());
string_prepend_str(contents, "[misc]\n");
File::create(path)
.and_then(|mut f| f.write_all(contents.as_bytes()))
.expect("Tried to auto-migrate the config file, unable to write to config file.\nPlease add \"[misc]\" section manually to the first line of the file.\nError");
}
}
}
// Command line arguments
#[derive(Parser, Debug)]
#[clap(name = "Topgrade", version)]
pub struct CommandLineArgs {
/// Edit the configuration file
#[clap(long = "edit-config")]
edit_config: bool,
/// Show config reference
#[clap(long = "config-reference")]
show_config_reference: bool,
/// Run inside tmux
#[clap(short = 't', long = "tmux")]
run_in_tmux: bool,
/// Cleanup temporary or old files
#[clap(short = 'c', long = "cleanup")]
cleanup: bool,
/// Print what would be done
#[clap(short = 'n', long = "dry-run")]
dry_run: bool,
/// Do not ask to retry failed steps
#[clap(long = "no-retry")]
no_retry: bool,
/// Do not perform upgrades for the given steps
#[clap(long = "disable", value_name = "STEP", value_enum, num_args = 1..)]
disable: Vec<Step>,
/// Perform only the specified steps (experimental)
#[clap(long = "only", value_name = "STEP", value_enum, num_args = 1..)]
only: Vec<Step>,
/// Run only specific custom commands
#[clap(long = "custom-commands", value_name = "NAME", num_args = 1..)]
custom_commands: Vec<String>,
/// Set environment variables
#[clap(long = "env", value_name = "NAME=VALUE", num_args = 1..)]
env: Vec<String>,
/// Output debug logs. Alias for `--log-filter debug`.
#[clap(short = 'v', long = "verbose")]
pub verbose: bool,
/// Prompt for a key before exiting
#[clap(short = 'k', long = "keep")]
keep_at_end: bool,
/// Skip sending a notification at the end of a run
#[clap(long = "skip-notify")]
skip_notify: bool,
/// Say yes to package manager's prompt
#[clap(
short = 'y',
long = "yes",
value_name = "STEP",
value_enum,
num_args = 0..,
)]
yes: Option<Vec<Step>>,
/// Don't pull the predefined git repos
#[clap(long = "disable-predefined-git-repos")]
disable_predefined_git_repos: bool,
/// Alternative configuration file
#[clap(long = "config", value_name = "PATH")]
config: Option<PathBuf>,
/// A regular expression for restricting remote host execution
#[clap(long = "remote-host-limit", value_name = "REGEX")]
remote_host_limit: Option<Regex>,
/// Show the reason for skipped steps
#[clap(long = "show-skipped")]
show_skipped: bool,
/// Tracing filter directives.
///
/// See: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/struct.EnvFilter.html
#[clap(long, default_value = DEFAULT_LOG_LEVEL)]
pub log_filter: String,
/// Print completion script for the given shell and exit
#[clap(long, value_enum, hide = true)]
pub gen_completion: Option<Shell>,
/// Print roff manpage and exit
#[clap(long, hide = true)]
pub gen_manpage: bool,
/// Don't update Topgrade
#[clap(long = "no-self-update")]
pub no_self_update: bool,
}
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> {
&self.env
}
/// In Topgrade, filter directives come from 3 places:
/// 1. CLI option `--log-filter`
/// 2. Config file
/// 3. `debug` if the `--verbose` option is present
///
/// Before loading the configuration file, we need our logger to work, so this
/// function will return directives coming from part 1 and 2.
///
///
/// When the configuration file is loaded, `Config::tracing_filter_directives()`
/// will return all the 3 parts.
pub fn tracing_filter_directives(&self) -> String {
let mut ret = self.log_filter.clone();
if self.verbose {
ret.push(',');
ret.push_str("debug");
}
ret
}
}
/// 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 configuration file and the
/// command line arguments.
#[derive(Debug)]
pub struct Config {
opt: CommandLineArgs,
config_file: ConfigFile,
allowed_steps: Vec<Step>,
}
impl Config {
/// Load the configuration.
///
/// The function parses the command line arguments and reads the configuration file.
pub fn load(opt: CommandLineArgs) -> Result<Self> {
let config_directory = config_directory();
let config_file = if config_directory.is_dir() {
ConfigFile::read(opt.config.clone()).unwrap_or_else(|e| {
// Inform the user about errors when loading the configuration,
// but fallback to the default config to at least attempt to do something
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,
})
}
/// Launch an editor to edit the configuration
pub fn edit() -> Result<()> {
ConfigFile::edit()
}
/// The list of commands to run before performing any step.
pub fn pre_commands(&self) -> &Option<Commands> {
&self.config_file.pre_commands
}
/// The list of commands to run at the end of all steps
pub fn post_commands(&self) -> &Option<Commands> {
&self.config_file.post_commands
}
/// The list of custom steps.
pub fn commands(&self) -> &Option<Commands> {
&self.config_file.commands
}
/// The list of additional git repositories to pull.
pub fn git_repos(&self) -> Option<&Vec<String>> {
self.config_file.git.as_ref().and_then(|git| git.repos.as_ref())
}
/// The list of docker/podman containers to ignore.
pub fn containers_ignored_tags(&self) -> Option<&Vec<String>> {
self.config_file
.containers
.as_ref()
.and_then(|containers| containers.ignored_containers.as_ref())
}
/// Tell whether the specified step should run.
///
/// If the step appears either in the `--disable` command line argument
/// or the `disable` option in the configuration, the function returns 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() {
if let Some(only) = misc.only.as_ref() {
enabled_steps.extend(only);
}
}
if enabled_steps.is_empty() {
enabled_steps.extend(Step::iter());
}
let mut disabled_steps: Vec<Step> = Vec::new();
disabled_steps.extend(&opt.disable);
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.
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)
}
/// Tell whether we should run in tmux.
pub fn run_in_tmux(&self) -> bool {
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
.misc
.as_ref()
.and_then(|misc| misc.cleanup)
.unwrap_or(false)
}
/// Tell whether we are dry-running.
pub fn dry_run(&self) -> bool {
self.opt.dry_run
}
/// Tell whether we should not attempt to retry anything.
pub fn no_retry(&self) -> bool {
self.opt.no_retry
|| self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.no_retry)
.unwrap_or(false)
}
/// List of remote hosts to run Topgrade in
pub fn remote_topgrades(&self) -> Option<&Vec<String>> {
self.config_file
.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
.misc
.as_ref()
.and_then(|misc| misc.remote_topgrade_path.as_deref())
.unwrap_or("topgrade")
}
/// Extra SSH arguments
pub fn ssh_arguments(&self) -> Option<&String> {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.ssh_arguments.as_ref())
}
/// Extra Git arguments
pub fn git_arguments(&self) -> Option<&String> {
self.config_file.git.as_ref().and_then(|git| git.arguments.as_ref())
}
/// Extra Tmux arguments
pub fn tmux_arguments(&self) -> Result<Vec<String>> {
let args = &self
.config_file
.misc
.as_ref()
.and_then(|misc| misc.tmux_arguments.as_ref())
.map(String::to_owned)
.unwrap_or_default();
shell_words::split(args)
// The only time the parse failed is in case of a missing close quote.
// The error message looks like this:
// Error: Failed to parse `tmux_arguments`: `'foo`
//
// Caused by:
// missing closing quote
.with_context(|| format!("Failed to parse `tmux_arguments`: `{args}`"))
}
/// Prompt for a key before exiting
pub fn keep_at_end(&self) -> bool {
self.opt.keep_at_end || env::var("TOPGRADE_KEEP_END").is_ok()
}
/// Skip sending a notification at the end of a run
pub fn skip_notify(&self) -> bool {
if let Some(yes) = self.config_file.misc.as_ref().and_then(|misc| misc.skip_notify) {
return yes;
}
self.opt.skip_notify
}
/// Whether to set the terminal title
pub fn set_title(&self) -> bool {
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.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
}
/// Bash-it branch
pub fn bashit_branch(&self) -> &str {
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 {
self.config_file
.windows
.as_ref()
.and_then(|windows| windows.accept_all_updates)
.unwrap_or(true)
}
/// Whether to self rename the Topgrade executable during the run
pub fn self_rename(&self) -> bool {
self.config_file
.windows
.as_ref()
.and_then(|w| w.self_rename)
.unwrap_or(false)
}
// Should wsl --update should use the --pre-release flag
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)
}
// Should wsl --update use the --web-download flag
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)
}
/// Whether Brew cask should be greedy
pub fn brew_cask_greedy(&self) -> bool {
self.config_file
.brew
.as_ref()
.and_then(|c| c.greedy_cask)
.unwrap_or(false)
}
/// Whether Brew should autoremove
pub fn brew_autoremove(&self) -> bool {
self.config_file
.brew
.as_ref()
.and_then(|c| c.autoremove)
.unwrap_or(false)
}
/// Whether Composer should update itself
pub fn composer_self_update(&self) -> bool {
self.config_file
.composer
.as_ref()
.and_then(|c| c.self_update)
.unwrap_or(false)
}
/// Whether to force plug update in Vim
pub fn force_vim_plug_update(&self) -> bool {
self.config_file
.vim
.as_ref()
.and_then(|c| c.force_plug_update)
.unwrap_or_default()
}
/// Whether to send a desktop notification at the beginning of every step
pub fn notify_each_step(&self) -> bool {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.notify_each_step)
.unwrap_or(false)
}
/// Extra garuda-update arguments
pub fn garuda_update_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.garuda_update_arguments.as_deref())
.unwrap_or("")
}
/// Extra trizen arguments
pub fn trizen_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.trizen_arguments.as_deref())
.unwrap_or("")
}
/// Extra Pikaur arguments
#[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("")
}
/// Extra Pamac arguments
pub fn pamac_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.pamac_arguments.as_deref())
.unwrap_or("")
}
/// Show news on Arch Linux
pub fn show_arch_news(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|s| s.show_arch_news)
.unwrap_or(true)
}
/// Get the package manager of an Arch Linux system
pub fn arch_package_manager(&self) -> ArchPackageManager {
self.config_file
.linux
.as_ref()
.and_then(|s| s.arch_package_manager)
.unwrap_or(ArchPackageManager::Autodetect)
}
/// Extra yay arguments
pub fn yay_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.yay_arguments.as_deref())
.unwrap_or("")
}
/// Extra aura arguments for AUR and pacman
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("")
}
/// Extra apt arguments
pub fn apt_arguments(&self) -> Option<&str> {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.apt_arguments.as_deref())
}
/// Extra dnf arguments
pub fn dnf_arguments(&self) -> Option<&str> {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.dnf_arguments.as_deref())
}
/// Extra nix arguments
pub fn nix_arguments(&self) -> Option<&str> {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.nix_arguments.as_deref())
}
/// Extra nix-env arguments
pub fn nix_env_arguments(&self) -> Option<&str> {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.nix_env_arguments.as_deref())
}
/// Extra Home Manager arguments
pub fn home_manager(&self) -> Option<&Vec<String>> {
self.config_file
.linux
.as_ref()
.and_then(|misc| misc.home_manager_arguments.as_ref())
}
/// Distrobox use root
pub fn distrobox_root(&self) -> bool {
self.config_file
.distrobox
.as_ref()
.and_then(|r| r.use_root)
.unwrap_or(false)
}
/// Distrobox containers
pub fn distrobox_containers(&self) -> Option<&Vec<String>> {
self.config_file.distrobox.as_ref().and_then(|r| r.containers.as_ref())
}
/// Concurrency limit for git
pub fn git_concurrency_limit(&self) -> Option<usize> {
self.config_file.git.as_ref().and_then(|git| git.max_concurrency)
}
/// Determine whether we should power on vagrant boxes
pub fn vagrant_power_on(&self) -> Option<bool> {
self.config_file.vagrant.as_ref().and_then(|vagrant| vagrant.power_on)
}
/// Vagrant directories
pub fn vagrant_directories(&self) -> Option<&Vec<String>> {
self.config_file
.vagrant
.as_ref()
.and_then(|vagrant| vagrant.directories.as_ref())
}
/// Always suspend vagrant boxes instead of powering off
pub fn vagrant_always_suspend(&self) -> Option<bool> {
self.config_file
.vagrant
.as_ref()
.and_then(|vagrant| vagrant.always_suspend)
}
/// Enable tlmgr on Linux
pub fn enable_tlmgr_linux(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.enable_tlmgr)
.unwrap_or(false)
}
/// Use distro-sync in Red Hat based distributions
pub fn redhat_distro_sync(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.redhat_distro_sync)
.unwrap_or(false)
}
/// Use zypper dist-upgrade (same as distro-sync on RH) instead of update (default: false on SLE/Leap, ignored on Tumbleweed (dup is always ran))
pub fn suse_dup(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.suse_dup)
.unwrap_or(false)
}
/// Use rpm-ostree in *when rpm-ostree is detected* (default: true)
pub fn rpm_ostree(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.rpm_ostree)
.unwrap_or(false)
}
/// Determine if we should ignore failures for this step
pub fn ignore_failure(&self, step: Step) -> bool {
self.config_file
.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
&& self
.config_file
.git
.as_ref()
.and_then(|git| git.pull_predefined)
.unwrap_or(true)
}
pub fn verbose(&self) -> bool {
self.opt.verbose
}
/// After loading the config file, filter directives consist of 3 parts:
///
/// 1. directives from the configuration file
/// 2. directives from the CLI options `--log-filter`
/// 3. `debug`, which would be enabled if the `--verbose` option is present
///
/// Previous directive will be overwritten if a directive with the same target
/// appear later.
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
}
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 sudo_command(&self) -> Option<SudoKind> {
self.config_file.misc.as_ref().and_then(|misc| misc.sudo_command)
}
/// 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
.misc
.as_ref()
.and_then(|misc| misc.pre_sudo)
.unwrap_or(false)
}
#[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 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, remote: &str) -> bool {
if let Ok(hostname) = hostname() {
if remote == hostname {
return false;
}
}
if let Some(limit) = self.opt.remote_host_limit.as_ref() {
return limit.is_match(remote);
}
true
}
#[cfg(windows)]
pub fn enable_winget(&self) -> bool {
return self
.config_file
.windows
.as_ref()
.and_then(|w| w.enable_winget)
.unwrap_or(false);
}
pub fn enable_pipupgrade(&self) -> bool {
return 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 {
return self
.config_file
.python
.as_ref()
.and_then(|python| python.enable_pip_review)
.unwrap_or(false);
}
pub fn enable_pip_review_local(&self) -> bool {
return self
.config_file
.python
.as_ref()
.and_then(|python| python.enable_pip_review_local)
.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)
}
}
#[cfg(test)]
mod test {
use crate::config::ConfigFile;
/// Test the default configuration in `config.example.toml` is valid.
#[test]
fn test_default_config() {
let str = include_str!("../config.example.toml");
assert!(toml::from_str::<ConfigFile>(str).is_ok());
}
}