Files
topgrade/src/config.rs
Akeshihiro 09673297db Drop the Go step (#660)
* Drop the Go step

With the release of Go 1.16 the behavior of `go get` has been changed.
In previous Go versions `go get` was used not only to add module
dependencies but also to install Go tools.
As of Go 1.16 `go get` can only add and upgrade module dependencies.
To install Go tools now the `go install` command has to be used.

Further on Go 1.16 enabled the GOMODULE mode by default and will drop
the GOPATH mode completly in Go 1.17.
So the package definition `all` like in `go get -u all` does not work
anymore if the PWD is outside of a Go module project.
Because of this `go list all` also does not work for the same reason.
That being said it seems that currently there is no way to get a list of
all installed Go tools or packages at the GOPATH level.

So the only possible solution to determine the installed Go tools and
also to update them would be by inspecting the `go env GOBIN` directory
as well as the `go env GOMODCACHE` sub-directories and to filter the
results according to their possible name-to-package boundaries.
As this approach seems to be very ugly and also not to be very safe or
stable and Go currently does not support any kind of automated upgrades
of installed Go tools it is best to drop the Go step for now until Go
implements some kind of Go tool upgrade feature.

Fixes #659

* Remove Go from Step enum
2021-02-24 12:17:03 +02:00

707 lines
20 KiB
Rust

use super::utils::editor;
use anyhow::Result;
use directories::BaseDirs;
use log::{debug, LevelFilter};
use pretty_env_logger::formatted_timed_builder;
use regex::Regex;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::fs::write;
use std::path::PathBuf;
use std::process::Command;
use std::{env, fs};
use structopt::StructOpt;
use strum::{EnumIter, EnumString, EnumVariantNames, IntoEnumIterator, VariantNames};
use which_crate::which;
pub static EXAMPLE_CONFIG: &str = include_str!("../config.example.toml");
#[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())
}
};
}
macro_rules! check_deprecated {
($config:expr, $old:ident, $section:ident, $new:ident) => {
if $config.$old.is_some() {
println!(concat!(
"'",
stringify!($old),
"' configuration option is deprecated. Rename it to '",
stringify!($new),
"' and put it under the section [",
stringify!($section),
"]",
));
}
};
}
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 {
&section.$new
} else {
&None
}
}
};
}
type Commands = BTreeMap<String, String>;
#[derive(EnumString, EnumVariantNames, Debug, Clone, PartialEq, Deserialize, EnumIter, Copy)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum Step {
Asdf,
Atom,
BrewCask,
BrewFormula,
Cargo,
Chocolatey,
Choosenim,
Composer,
CustomCommands,
Deno,
Dotnet,
Emacs,
Firmware,
Flatpak,
Flutter,
Fossil,
Gcloud,
Gem,
GitRepos,
HomeManager,
Jetpack,
Krew,
MacPorts,
Micro,
MicrosoftAutoUpdate,
MicrosoftStore,
Myrepos,
Nix,
Node,
Opam,
Pearl,
Pipx,
Pkg,
Powershell,
Remotes,
Restarts,
Rtcl,
Rustup,
Scoop,
Sdkman,
Sheldon,
Shell,
Snap,
Stack,
System,
Tldr,
Tlmgr,
Tmux,
Vagrant,
Vcpkg,
Vim,
Vscode,
Vscodium,
Wsl,
Yadm,
}
#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct Git {
max_concurrency: Option<usize>,
arguments: Option<String>,
repos: Option<Vec<String>>,
pull_predefined: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct Vagrant {
directories: Option<Vec<String>>,
power_on: Option<bool>,
always_suspend: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct Windows {
accept_all_updates: Option<bool>,
self_rename: Option<bool>,
open_remotes_in_new_terminal: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct Brew {
greedy_cask: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct Linux {
yay_arguments: Option<String>,
trizen_arguments: Option<String>,
dnf_arguments: Option<String>,
enable_tlmgr: Option<bool>,
redhat_distro_sync: Option<bool>,
emerge_sync_flags: Option<String>,
emerge_update_flags: Option<String>,
}
#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct Composer {
self_update: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
/// Configuration file
pub struct ConfigFile {
pre_commands: Option<Commands>,
post_commands: Option<Commands>,
commands: Option<Commands>,
git_repos: Option<Vec<String>>,
predefined_git_repos: Option<bool>,
disable: Option<Vec<Step>>,
ignore_failures: Option<Vec<Step>>,
remote_topgrades: Option<Vec<String>>,
remote_topgrade_path: Option<String>,
ssh_arguments: Option<String>,
git_arguments: Option<String>,
tmux_arguments: Option<String>,
set_title: Option<bool>,
assume_yes: Option<bool>,
yay_arguments: Option<String>,
no_retry: Option<bool>,
run_in_tmux: Option<bool>,
cleanup: Option<bool>,
notify_each_step: Option<bool>,
accept_all_windows_updates: Option<bool>,
bashit_branch: Option<String>,
only: Option<Vec<Step>>,
composer: Option<Composer>,
brew: Option<Brew>,
linux: Option<Linux>,
git: Option<Git>,
windows: Option<Windows>,
vagrant: Option<Vagrant>,
}
fn config_directory(base_dirs: &BaseDirs) -> PathBuf {
#[cfg(not(target_os = "macos"))]
return base_dirs.config_dir().to_owned();
#[cfg(target_os = "macos")]
return base_dirs.home_dir().join(".config");
}
impl ConfigFile {
fn ensure(base_dirs: &BaseDirs) -> Result<PathBuf> {
let config_directory = config_directory(base_dirs);
let config_path = config_directory.join("topgrade.toml");
if !config_path.exists() {
debug!("No configuration exists");
write(&config_path, EXAMPLE_CONFIG).map_err(|e| {
debug!(
"Unable to write the example configuration file to {}: {}. Using blank config.",
config_path.display(),
e
);
e
})?;
} else {
debug!("Configuration at {}", config_path.display());
}
Ok(config_path)
}
/// Read the configuration file.
///
/// If the configuration file does not exist the function returns the default ConfigFile.
fn read(base_dirs: &BaseDirs, config_path: Option<PathBuf>) -> Result<ConfigFile> {
let config_path = if let Some(path) = config_path {
path
} else {
Self::ensure(base_dirs)?
};
let contents = fs::read_to_string(&config_path).map_err(|e| {
log::error!("Unable to read {}", config_path.display());
e
})?;
let mut result: Self = toml::from_str(&contents).map_err(|e| {
log::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(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(base_dirs: &BaseDirs) -> Result<()> {
let config_path = Self::ensure(base_dirs)?;
let editor = editor();
let command = which(&editor[0])?;
let args: Vec<&String> = editor.iter().skip(1).collect();
Command::new(command)
.args(args)
.arg(config_path)
.spawn()
.and_then(|mut p| p.wait())?;
Ok(())
}
}
#[derive(StructOpt, Debug)]
#[structopt(name = "Topgrade", setting = structopt::clap::AppSettings::ColoredHelp)]
/// Command line arguments
pub struct CommandLineArgs {
/// Edit the configuration file
#[structopt(long = "edit-config")]
edit_config: bool,
/// Show config reference
#[structopt(long = "config-reference")]
show_config_reference: bool,
/// Run inside tmux
#[structopt(short = "t", long = "tmux")]
run_in_tmux: bool,
/// Cleanup temporary or old files
#[structopt(short = "c", long = "cleanup")]
cleanup: bool,
/// Print what would be done
#[structopt(short = "n", long = "dry-run")]
dry_run: bool,
/// Do not ask to retry failed steps
#[structopt(long = "no-retry")]
no_retry: bool,
/// Do not perform upgrades for the given steps
#[structopt(long = "disable", possible_values = &Step::VARIANTS)]
disable: Vec<Step>,
/// Perform only the specified steps (experimental)
#[structopt(long = "only", possible_values = &Step::VARIANTS)]
only: Vec<Step>,
/// Output logs
#[structopt(short = "v", long = "verbose")]
verbose: bool,
/// Prompt for a key before exiting
#[structopt(short = "k", long = "keep")]
keep_at_end: bool,
/// Say yes to package manager's prompt (experimental)
#[structopt(short = "y", long = "yes")]
yes: bool,
/// Don't pull the predefined git repos
#[structopt(long = "disable-predefined-git-repos")]
disable_predefined_git_repos: bool,
/// Alternative configuration file
#[structopt(long = "config")]
config: Option<PathBuf>,
/// A regular expression for restricting remote host execution
#[structopt(long = "remote-host-limit", parse(try_from_str))]
remote_host_limit: Option<Regex>,
/// Show the reason for skipped steps
#[structopt(long = "show-skipped")]
show_skipped: bool,
}
impl CommandLineArgs {
pub fn edit_config(&self) -> bool {
self.edit_config
}
pub fn show_config_reference(&self) -> bool {
self.show_config_reference
}
}
/// 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
/// command line arguments.
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 reading the configuration file.
pub fn load(base_dirs: &BaseDirs, opt: CommandLineArgs) -> Result<Self> {
let mut builder = formatted_timed_builder();
if opt.verbose {
builder.filter(Some("topgrade"), LevelFilter::Trace);
}
builder.init();
let config_directory = config_directory(base_dirs);
let config_file = if config_directory.is_dir() {
ConfigFile::read(base_dirs, 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
log::error!("failed to load configuration: {}", e);
ConfigFile::default()
})
} else {
log::debug!("Configuration directory {} does not exist", config_directory.display());
ConfigFile::default()
};
check_deprecated!(config_file, git_arguments, git, arguments);
check_deprecated!(config_file, git_repos, git, repos);
check_deprecated!(config_file, predefined_git_repos, git, pull_predefined);
check_deprecated!(config_file, yay_arguments, linux, yay_arguments);
check_deprecated!(config_file, accept_all_windows_updates, windows, accept_all_updates);
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(base_dirs: &BaseDirs) -> Result<()> {
ConfigFile::edit(base_dirs)
}
/// 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>> {
get_deprecated!(&self.config_file, git_repos, git, repos)
}
/// 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> = if !opt.only.is_empty() {
opt.only.clone()
} else {
config_file
.only
.as_ref()
.map_or_else(|| Step::iter().collect(), |v| v.clone())
};
let disabled_steps: Vec<Step> = if !opt.disable.is_empty() {
opt.disable.clone()
} else {
config_file.disable.as_ref().map_or_else(Vec::new, |v| v.clone())
};
enabled_steps.retain(|e| !disabled_steps.contains(e) || opt.only.contains(e));
enabled_steps
}
/// 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)
}
/// Tell whether we should perform cleanup steps.
pub fn cleanup(&self) -> bool {
self.opt.cleanup || self.config_file.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.no_retry.unwrap_or(false)
}
/// List of remote hosts to run Topgrade in
pub fn remote_topgrades(&self) -> &Option<Vec<String>> {
&self.config_file.remote_topgrades
}
/// 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")
}
/// Extra SSH arguments
pub fn ssh_arguments(&self) -> &Option<String> {
&self.config_file.ssh_arguments
}
/// Extra Git arguments
pub fn git_arguments(&self) -> &Option<String> {
get_deprecated!(&self.config_file, git_arguments, git, arguments)
}
/// Extra Tmux arguments
#[allow(dead_code)]
pub fn tmux_arguments(&self) -> &Option<String> {
&self.config_file.tmux_arguments
}
/// 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()
}
/// Whether to set the terminal title
pub fn set_title(&self) -> bool {
self.config_file.set_title.unwrap_or(true)
}
/// Whether to say yes to package managers
#[allow(dead_code)]
pub fn yes(&self) -> bool {
self.config_file.assume_yes.unwrap_or(self.opt.yes)
}
/// Bash-it branch
#[allow(dead_code)]
pub fn bashit_branch(&self) -> &str {
self.config_file.bashit_branch.as_deref().unwrap_or("stable")
}
/// Whether to accept all Windows updates
#[allow(dead_code)]
pub fn accept_all_windows_updates(&self) -> bool {
get_deprecated!(
self.config_file,
accept_all_windows_updates,
windows,
accept_all_updates
)
.unwrap_or(true)
}
/// Whether to self rename the Topgrade executable during the run
#[allow(dead_code)]
pub fn self_rename(&self) -> bool {
self.config_file
.windows
.as_ref()
.and_then(|w| w.self_rename)
.unwrap_or(false)
}
/// Whether Brew cask should be greedy
#[allow(dead_code)]
pub fn brew_cask_greedy(&self) -> bool {
self.config_file
.brew
.as_ref()
.and_then(|c| c.greedy_cask)
.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 send a desktop notification at the beginning of every step
#[allow(dead_code)]
pub fn notify_each_step(&self) -> bool {
self.config_file.notify_each_step.unwrap_or(false)
}
/// Extra trizen arguments
#[allow(dead_code)]
pub fn trizen_arguments(&self) -> &str {
&self
.config_file
.linux
.as_ref()
.and_then(|s| s.trizen_arguments.as_deref())
.unwrap_or("")
}
/// Extra yay arguments
#[allow(dead_code)]
pub fn yay_arguments(&self) -> &str {
get_deprecated!(self.config_file, yay_arguments, linux, yay_arguments)
.as_deref()
.unwrap_or("--devel")
}
/// Extra yay arguments
#[allow(dead_code)]
pub fn dnf_arguments(&self) -> Option<&str> {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.dnf_arguments.as_deref())
}
/// Concurrency limit for git
pub fn git_concurrency_limit(&self) -> Option<usize> {
self.config_file.git.as_ref().and_then(|git| git.max_concurrency)
}
/// Should we power on vagrant boxes if needed
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
#[allow(dead_code)]
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 distrbutions
#[allow(dead_code)]
pub fn redhat_distro_sync(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.redhat_distro_sync)
.unwrap_or(false)
}
/// Should we ignore failures for this step
pub fn ignore_failure(&self, step: Step) -> bool {
self.config_file
.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)
}
pub fn verbose(&self) -> bool {
self.opt.verbose
}
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)
}
#[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 {
self.opt
.remote_host_limit
.as_ref()
.map(|h| h.is_match(remote))
.unwrap_or(true)
}
}