Merge pull request #1 from r-darwish/master

Bring jasikpark/topgrade up-to-date with r-darwish/topgrade
This commit is contained in:
Caleb Jasik
2019-03-15 18:59:01 -05:00
committed by GitHub
38 changed files with 3220 additions and 1532 deletions

View File

@@ -23,6 +23,7 @@ matrix:
- env: TARGET=armv7-unknown-linux-gnueabihf - env: TARGET=armv7-unknown-linux-gnueabihf
- env: TARGET=x86_64-unknown-linux-gnu - env: TARGET=x86_64-unknown-linux-gnu
- env: TARGET=x86_64-unknown-linux-musl - env: TARGET=x86_64-unknown-linux-musl
- env: TARGET=x86_64-unknown-freebsd
# OSX # OSX
- env: TARGET=x86_64-apple-darwin - env: TARGET=x86_64-apple-darwin
@@ -72,3 +73,10 @@ before_cache:
notifications: notifications:
email: email:
on_success: never on_success: never
branches:
only:
- staging
- trying
- master
- /^v[\d.]+$/

1578
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,36 @@
[package] [package]
name = "topgrade" name = "topgrade"
description = "Upgrade all the things" description = "Upgrade all the things"
license-file = "LICENCE" license-file = "LICENSE"
repository = "https://github.com/r-darwish/topgrade" repository = "https://github.com/r-darwish/topgrade"
version = "0.16.0" version = "1.9.0"
authors = ["Roey Darwish Dror <roey.ghost@gmail.com>"] authors = ["Roey Darwish Dror <roey.ghost@gmail.com>"]
exclude = ["doc/screenshot.gif"] exclude = ["doc/screenshot.gif"]
edition = "2018"
[dependencies] [dependencies]
directories = "1.0.2" directories = "1.0.2"
failure = "0.1.2" failure = "0.1.5"
failure_derive = "0.1.2" failure_derive = "0.1.5"
serde = "1.0.79" serde = { version = "1.0.88", features = ["derive"] }
serde_derive = "1.0.79" toml = "0.4.10"
toml = "0.4.8" which_crate = { version = "2.0.1", package = "which" }
which = "2.0.0"
shellexpand = "1.0.0" shellexpand = "1.0.0"
structopt = "0.2.10" structopt = "0.2.14"
log = "0.4.5" log = "0.4.6"
env_logger = "0.5.13" env_logger = "0.6.0"
term_size = "0.3.1" walkdir = "2.2.7"
termcolor = "1.0.4" console = "0.7.5"
walkdir = "2.2.5" self_update_crate = { version = "0.5.1", optional = true, package = "self_update" }
console = "0.6.2" lazy_static = "1.2.0"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
nix = "0.11" nix = "0.13.0"
lazy_static = "1.1.0"
[profile.release] [profile.release]
lto = true lto = true
[features]
default = []
self-update = ["self_update_crate"]

View File

View File

@@ -12,13 +12,30 @@ Keeping your system up to date mostly involves invoking more than a single packa
usually results in big shell one-liners saved in your shell history. Topgrade tries to solve this usually results in big shell one-liners saved in your shell history. Topgrade tries to solve this
problem by detecting which tools you use and run their appropriate package managers. problem by detecting which tools you use and run their appropriate package managers.
## Supported Platforms
Topgrade should probably work on whichever platform it can be build. The real question is whether
Topgrade knows that platform and can utilize its unique features, such as the operating system's
pacakge manager. Topgrade is tested on and knows the following platforms:
* Linux
* Arch
* CentOS/RHEL
* Fedora
* Debian/Ubuntu
* Gentoo
* openSUSE
* Void
* FreeBSD
* macOS
* Windows
## Installation ## Installation
Arch Linux users can use the [AUR](https://aur.archlinux.org/packages/topgrade/) package. Arch Linux users can use the [AUR](https://aur.archlinux.org/packages/topgrade/) package.
macOS users can install topgrade via Homebrew. macOS users can install topgrade via Homebrew.
Other systems users can either use `cargo install` or use the compiled binaries from the release Other systems users can either use `cargo install` or use the compiled binaries from the release
page. page. The compiled binaries contain a self-upgrading feature.
Topgrade isn't guaranteed to work on Rust versions older than the latest stable release. If you Topgrade isn't guaranteed to work on Rust versions older than the latest stable release. If you
intend to install Topgrade using Cargo then you should either install Rust using rustup or use a intend to install Topgrade using Cargo then you should either install Rust using rustup or use a
@@ -27,15 +44,24 @@ distribution which ships the latest version of Rust, such as Arch Linux.
## Usage ## Usage
Just run `topgrade`. It will run the following steps: Just run `topgrade`. It will run the following steps:
* *Linux*: Run the system package manager: * Try to self-upgrade if compiled with this feature. On Unix systems Topgrade will also respawn
* *Arch*: Run [yay](https://github.com/Jguer/yay) or fall back to pacman itself if it was upgraded
* *CentOS/RHEL*: Run `yum upgrade` * **Linux**: Run the system package manager:
* *Fedora* - Run `dnf upgrade` * **Arch**: Run [yay](https://github.com/Jguer/yay) or fall back to pacman
* *Debian/Ubuntu*: Run `apt update && apt dist-upgrade` * **CentOS/RHEL**: Run `yum upgrade`
* *Linux*: Run [etc-update](https://dev.gentoo.org/~zmedico/portage/doc/man/etc-update.1.html): * **Fedora**: Run `dnf upgrade`
* *Unix*: Run `brew update && brew upgrade`. This should handle both Homebrew and Linuxbrew * **Debian/Ubuntu**: Run `apt update && apt dist-upgrade`
* *Windows*: Upgrade Powershell modules * **Gentoo**: Run `layman -s ALL && emerge --sync -q && eix-update && emerge -uDNa world`
* *Windows*: Upgrade all [Chocolatey](https://chocolatey.org/) packages * **openSUSE**: Run `zypper refresh && zypper dist-upgrade`
* **Void**: Run `xbps-install -Su`
* **Linux**: Run [etc-update](https://dev.gentoo.org/~zmedico/portage/doc/man/etc-update.1.html):
* **FreeBSD**: Upgrade and audit packages
* **Unix**: Run `brew update && brew upgrade`. This should handle both Homebrew and Linuxbrew
* **Unix**: Run `nix upgrade-nix && nix --upgrade`.
* **Unix**: Run [Pearl](https://github.com/pearl-core/pearl) `pearl update`.
* **Windows**: Upgrade Powershell modules
* **Windows**: Upgrade all [Chocolatey](https://chocolatey.org/) packages
* **Windows**: Upgrade all [Scoop](https://scoop.sh) packages
* Check if the following paths are tracked by Git. If so, pull them: * Check if the following paths are tracked by Git. If so, pull them:
* ~/.emacs.d (Should work whether you use [Spacemacs](http://spacemacs.org/) or a custom configuration) * ~/.emacs.d (Should work whether you use [Spacemacs](http://spacemacs.org/) or a custom configuration)
* ~/.zshrc * ~/.zshrc
@@ -45,15 +71,21 @@ Just run `topgrade`. It will run the following steps:
* ~/.config/nvim * ~/.config/nvim
* ~/.vim * ~/.vim
* ~/.config/openbox * ~/.config/openbox
* ~/.config/bspwm
* ~/.config/i3
* Powershell Profile * Powershell Profile
* Custom defined paths * Custom defined paths
* *Unix*: Run [zplug](https://github.com/zplug/zplug) update * **Unix**: Run [zplug](https://github.com/zplug/zplug) update
* *Unix*: Run [fisher](https://github.com/jorgebucaran/fisher) * **Unix**: Run [fisher](https://github.com/jorgebucaran/fisher)
* *Unix*: Upgrade tmux plugins with [TPM](https://github.com/tmux-plugins/tpm) * **Unix**: Upgrade tmux plugins with [TPM](https://github.com/tmux-plugins/tpm). *Note*: Do not use
the `-b` flag in your configuration as suggested by the TPM readme.
* Update Rustup by running `rustup update`. This will also attempt to run `rustup self update` when Rustup is installed inside the home directory. * Update Rustup by running `rustup update`. This will also attempt to run `rustup self update` when Rustup is installed inside the home directory.
* Run Cargo [install-update](https://github.com/nabijaczleweli/cargo-update) * Run Cargo [install-update](https://github.com/nabijaczleweli/cargo-update)
* Upgrade Emacs packages (You'll get a better output if you have [Paradox](https://github.com/Malabarba/paradox) installed) * Upgrade Emacs packages (You'll get a better output if you have [Paradox](https://github.com/Malabarba/paradox) installed)
* Upgrade [OCaml packages](https://opam.ocaml.org/) * Upgrade [OCaml packages](https://opam.ocaml.org/)
* Upgrade [vcpkg](https://github.com/Microsoft/vcpkg) globally installed packages
* Upgrade Python packages installed using [pipx](https://github.com/cs01/pipx)
* Upgrade [R globally installed packages](https://github.com/ankane/jetpack)
* Upgrade Vim/Neovim packages. Works with the following plugin frameworks: * Upgrade Vim/Neovim packages. Works with the following plugin frameworks:
* [NeoBundle](https://github.com/Shougo/neobundle.vim) * [NeoBundle](https://github.com/Shougo/neobundle.vim)
* [Vundle](https://github.com/VundleVim/Vundle.vim) * [Vundle](https://github.com/VundleVim/Vundle.vim)
@@ -61,29 +93,36 @@ Just run `topgrade`. It will run the following steps:
* Node * Node
* Run `yarn global update` if yarn is installed. * Run `yarn global update` if yarn is installed.
* Run `npm update -g` if NPM is installed and `npm root -g` is a path inside your home directory. * Run `npm update -g` if NPM is installed and `npm root -g` is a path inside your home directory.
* Run `composer global update` if Composer's home directory is inside the home directory of the user. * Run `composer global update` if Composer's home directory is inside the home directory of the
user. Run `valet install` after.
* Upgrade Atom packages * Upgrade Atom packages
* Run `gem upgrade --user-install` if `~/.gem` exists * Run `gem upgrade --user-install` if `~/.gem` exists
* *Linux*: Update Flatpak packages * **Linux**: Update Flatpak packages
* *Linux*: Update snap packages * **Linux**: Update snap packages
* *Linux*: Run [fwupdmgr](https://github.com/hughsie/fwupd) to show firmware upgrade. (View * **Linux**: Run [fwupdmgr](https://github.com/hughsie/fwupd) to show firmware upgrade. (View
only. No upgrades will actually be performed) only. No upgrades will actually be performed)
* Run custom defined commands * Run custom defined commands
* Final stage * Final stage
* *Linux*: Run [needrestart](https://github.com/liske/needrestart) * **Linux**: Run [needrestart](https://github.com/liske/needrestart)
* *Windows*: Run Windows Update (You'll have to install [PSWindowsUpdate](https://marckean.com/2016/06/01/use-powershell-to-install-windows-updates/)) * **Windows**: Run Windows Update (You'll have to install [PSWindowsUpdate](https://marckean.com/2016/06/01/use-powershell-to-install-windows-updates/))
* *macOS*: Upgrade App Store applications * **macOS**: Upgrade App Store applications
* **FreeBSD**: Run `freebsd-upgrade`
## Flags ## Flags
* `-t/--tmux` - Topgrade will launch itself in a new tmux session. This flag has no effect if * `-t/--tmux` - Topgrade will launch itself in a new tmux session. This flag has no effect if
Topgrade already runs inside tmux. This is useful when using topgrade on remote systems. Topgrade already runs inside tmux. This is useful when using topgrade on remote systems.
* `-c/--cleanup` - Topgrade will instruct package managers to remove old or unused files
* `-n/--dry-run` - Print what should be run. * `-n/--dry-run` - Print what should be run.
* `--no-system` - Skip the system upgrade phase. * `--disable [STEPS]` - Disable one or more steps:
* `--no-git-repos` - Don't pull custom git repositories. * `system` - Skip the system upgrade phase.
* `--no-emacs` - Don't upgrade Emacs packages or configuration files. * `git-repos` - Don't pull custom git repositories.
* `emacs` - Don't upgrade Emacs packages or configuration files.
* `vim` - Don't upgrade Vim/NeoVim packages or configuration files.
* `gem` - Don't upgrade ruby gems.
* `--no-retry` - Don't ask to retry failed steps.
## Customization ## Customization
You can place a configuration file at `~/.config/topgrade.toml` (on macOS `~/Library/Preferences/topgrade.toml`).. Here's an example: Here's an example for a configuration file:
``` toml ``` toml
@@ -91,6 +130,9 @@ git_repos = [
"~/dev/topgrade", "~/dev/topgrade",
] ]
# Same options as the command line flag
disable = ["system", "emacs"]
[pre_commands] [pre_commands]
"Emacs Snapshot" = "rm -rf ~/.emacs.d/elpa.bak && cp -rl ~/.emacs.d/elpa ~/.emacs.d/elpa.bak" "Emacs Snapshot" = "rm -rf ~/.emacs.d/elpa.bak && cp -rl ~/.emacs.d/elpa ~/.emacs.d/elpa.bak"
@@ -102,3 +144,11 @@ git_repos = [
will not proceed will not proceed
* `commands` - Custom upgrade steps. If any command fails it will be reported in the summary as all * `commands` - Custom upgrade steps. If any command fails it will be reported in the summary as all
upgrade steps are reported, but it will not cause Topgrade to stop. upgrade steps are reported, but it will not cause Topgrade to stop.
### Configuration path
The configuration should be placed in the following paths depending by the operating system:
* **macOS** - `~/Library/Preferences/topgrade.toml`
* **Windows** - `%APPDATA%/topgrade.toml`
* **Other Unix systems** - `~/.config/topgrade.toml`

View File

@@ -20,19 +20,19 @@ install:
- rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_VERSION% - rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_VERSION%
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- rustup component add rustfmt-preview clippy-preview - rustup component add rustfmt-preview clippy-preview
- cargo fmt --all -- --check
- cargo clippy --all-targets --all-features -- -D warnings
- rustc -Vv - rustc -Vv
- cargo -V - cargo -V
test_script: test_script:
- if [%APPVEYOR_REPO_TAG%]==[false] ( - if [%APPVEYOR_REPO_TAG%]==[false] (
cargo check --target %TARGET% && cargo fmt --all -- --check &&
cargo check --target %TARGET% --release cargo clippy --all-targets --all-features -- -D warnings &&
cargo clippy --all-targets -- -D warnings &&
cargo test
) )
before_deploy: before_deploy:
- cargo rustc --target %TARGET% --release --bin topgrade -- -C lto - cargo rustc --target %TARGET% --release --bin topgrade --all-features -- -C lto
- ps: ci\before_deploy.ps1 - ps: ci\before_deploy.ps1
deploy: deploy:
@@ -55,3 +55,10 @@ notifications:
# Building is done in the test phase, so we disable Appveyor's build phase. # Building is done in the test phase, so we disable Appveyor's build phase.
build: false build: false
branches:
only:
- staging
- trying
- master
- /^v[\d.]+$/

4
bors.toml Normal file
View File

@@ -0,0 +1,4 @@
status = [
"continuous-integration/travis-ci/push",
"continuous-integration/appveyor/branch"
]

View File

@@ -18,7 +18,7 @@ main() {
test -f Cargo.lock || cargo generate-lockfile test -f Cargo.lock || cargo generate-lockfile
# TODO Update this to build the artifacts that matter to you # TODO Update this to build the artifacts that matter to you
cross rustc --bin topgrade --target $TARGET --release -- -C lto cross rustc --bin topgrade --target $TARGET --release --all-features -- -C lto
# TODO Update this to package the right artifacts # TODO Update this to package the right artifacts
cp target/$TARGET/release/topgrade $stage/ cp target/$TARGET/release/topgrade $stage/

View File

@@ -5,11 +5,12 @@ set -ex
# TODO This is the "test phase", tweak it as you see fit # TODO This is the "test phase", tweak it as you see fit
main() { main() {
cargo fmt --all -- --check cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings cross clippy --all-targets -- -D warnings
cross check --target $TARGET cross clippy --all-targets --all-features -- -D warnings
cross check --target $TARGET --release cross check --target $TARGET --release --all-features
if [ ! -z $DISABLE_TESTS ]; then if [ ! -z $DISABLE_TESTS ]; then
cross test
return return
fi fi

View File

@@ -1,27 +1,88 @@
use super::error::{Error, ErrorKind};
use directories::BaseDirs; use directories::BaseDirs;
use failure; use failure::ResultExt;
use lazy_static::lazy_static;
use serde::Deserialize;
use shellexpand; use shellexpand;
use std::collections::BTreeMap; use std::collections::{BTreeMap, HashMap};
use std::fs; use std::fs;
use structopt::StructOpt;
use toml; use toml;
type Commands = BTreeMap<String, String>; type Commands = BTreeMap<String, String>;
lazy_static! {
// While this is used to automatically generate possible value list everywhere in the code, the
// README.md file still needs to be manually updated.
static ref STEPS_MAPPING: HashMap<&'static str, Step> = {
let mut m = HashMap::new();
m.insert("system", Step::System);
m.insert("git-repos", Step::GitRepos);
m.insert("vim", Step::Vim);
m.insert("emacs", Step::Emacs);
m.insert("gem", Step::Gem);
#[cfg(windows)]
m.insert("powershell", Step::Powershell);
m
};
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Step {
/// Don't perform system upgrade
System,
/// Don't perform updates on configured git repos
GitRepos,
/// Don't upgrade Vim packages or configuration files
Vim,
/// Don't upgrade Emacs packages or configuration files
Emacs,
/// Don't upgrade ruby gems
Gem,
#[cfg(windows)]
/// Don't update Powershell modules
Powershell,
}
impl Step {
fn possible_values() -> Vec<&'static str> {
STEPS_MAPPING.keys().cloned().collect()
}
}
impl std::str::FromStr for Step {
type Err = structopt::clap::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(STEPS_MAPPING.get(s).unwrap().clone())
}
}
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
pub struct Config { /// Configuration file
pub struct ConfigFile {
pre_commands: Option<Commands>, pre_commands: Option<Commands>,
commands: Option<Commands>, commands: Option<Commands>,
git_repos: Option<Vec<String>>, git_repos: Option<Vec<String>>,
disable: Option<Vec<Step>>,
} }
impl Config { impl ConfigFile {
pub fn read(base_dirs: &BaseDirs) -> Result<Config, failure::Error> { /// Read the configuration file.
///
/// If the configuration file does not exist the function returns the default ConfigFile.
fn read(base_dirs: &BaseDirs) -> Result<ConfigFile, Error> {
let config_path = base_dirs.config_dir().join("topgrade.toml"); let config_path = base_dirs.config_dir().join("topgrade.toml");
if !config_path.exists() { if !config_path.exists() {
return Ok(Default::default()); return Ok(Default::default());
} }
let mut result: Self = toml::from_str(&fs::read_to_string(config_path)?)?; let mut result: Self = toml::from_str(&fs::read_to_string(config_path).context(ErrorKind::Configuration)?)
.context(ErrorKind::Configuration)?;
if let Some(ref mut paths) = &mut result.git_repos { if let Some(ref mut paths) = &mut result.git_repos {
for path in paths.iter_mut() { for path in paths.iter_mut() {
@@ -31,41 +92,101 @@ impl Config {
Ok(result) Ok(result)
} }
pub fn pre_commands(&self) -> &Option<Commands> {
&self.pre_commands
}
pub fn commands(&self) -> &Option<Commands> {
&self.commands
}
pub fn git_repos(&self) -> &Option<Vec<String>> {
&self.git_repos
}
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
#[structopt(name = "Topgrade")] #[structopt(name = "Topgrade")]
pub struct Opt { /// Command line arguments
#[structopt(short = "t", long = "tmux", help = "Run inside tmux")] pub struct CommandLineArgs {
pub run_in_tmux: bool, /// Run inside tmux
#[structopt(short = "t", long = "tmux")]
run_in_tmux: bool,
#[structopt(long = "no-system", help = "Don't perform system upgrade")] /// Cleanup temporary or old files
pub no_system: bool, #[structopt(short = "c", long = "cleanup")]
cleanup: bool,
#[structopt( /// Print what would be done
long = "no-git-repos", #[structopt(short = "n", long = "dry-run")]
help = "Don't perform updates on configured git repos" dry_run: bool,
)]
pub no_git_repos: bool,
#[structopt( /// Do not ask to retry failed steps
long = "no-emacs", #[structopt(long = "no-retry")]
help = "Don't upgrade Emacs packages or configuration files" no_retry: bool,
)]
pub no_emacs: bool,
#[structopt(short = "n", long = "dry-run", help = "Print what would be done")] /// Do not perform upgrades for the given steps
pub dry_run: bool, #[structopt(long = "disable", raw(possible_values = "&Step::possible_values()"))]
disable: Vec<Step>,
}
/// 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,
}
impl Config {
/// Load the configuration.
///
/// The function parses the command line arguments and reading the configuration file.
pub fn load(base_dirs: &BaseDirs) -> Result<Self, Error> {
Ok(Self {
opt: CommandLineArgs::from_args(),
config_file: ConfigFile::read(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 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_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
.config_file
.disable
.as_ref()
.map(|d| d.contains(&step))
.unwrap_or(false)
|| self.opt.disable.contains(&step))
}
/// Tell whether we should run in tmux.
pub fn run_in_tmux(&self) -> bool {
self.opt.run_in_tmux
}
/// Tell whether we should perform cleanup steps.
#[cfg(not(windows))]
pub fn cleanup(&self) -> bool {
self.opt.cleanup
}
/// 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
}
} }

View File

@@ -1,3 +1,5 @@
//! Provides handling for process interruption.
//! There's no actual handling for Windows at the moment.
#[cfg(unix)] #[cfg(unix)]
mod unix; mod unix;
#[cfg(unix)] #[cfg(unix)]

View File

@@ -1,22 +1,31 @@
//! SIGINT handling in Unix systems.
use lazy_static::lazy_static;
use nix::sys::signal; use nix::sys::signal;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
lazy_static! { lazy_static! {
static ref RUNNING: AtomicBool = AtomicBool::new(true); /// A global variable telling whether the application has been interrupted.
static ref INTERRUPTED: AtomicBool = AtomicBool::new(false);
} }
pub fn running() -> bool { /// Tells whether the program has been interrupted
RUNNING.load(Ordering::SeqCst) pub fn interrupted() -> bool {
INTERRUPTED.load(Ordering::SeqCst)
} }
pub fn set_running(value: bool) { /// Clears the interrupted flag
RUNNING.store(value, Ordering::SeqCst) pub fn unset_interrupted() {
debug_assert!(INTERRUPTED.load(Ordering::SeqCst));
INTERRUPTED.store(false, Ordering::SeqCst)
} }
/// Handle SIGINT. Set the interruption flag.
extern "C" fn handle_sigint(_: i32) { extern "C" fn handle_sigint(_: i32) {
set_running(false); INTERRUPTED.store(true, Ordering::SeqCst)
} }
/// Set the necessary signal handlers.
/// The function panics on failure.
pub fn set_handler() { pub fn set_handler() {
let sig_action = signal::SigAction::new( let sig_action = signal::SigAction::new(
signal::SigHandler::Handler(handle_sigint), signal::SigHandler::Handler(handle_sigint),

View File

@@ -1,7 +1,9 @@
pub fn running() -> bool { //! A stub for Ctrl + C handling.
true
pub fn interrupted() -> bool {
false
} }
pub fn set_running(_value: bool) {} pub fn unset_interrupted() {}
pub fn set_handler() {} pub fn set_handler() {}

83
src/error.rs Normal file
View File

@@ -0,0 +1,83 @@
use failure::{Backtrace, Context, Fail};
use std::fmt::{self, Display};
use std::process::ExitStatus;
#[derive(Debug)]
pub struct Error {
inner: Context<ErrorKind>,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)]
pub enum ErrorKind {
#[fail(display = "Error asking the user for retry")]
Retry,
#[fail(display = "Cannot find the user base directories")]
NoBaseDirectories,
#[fail(display = "A step failed")]
StepFailed,
#[fail(display = "Error reading the configuration")]
Configuration,
#[fail(display = "A custom pre-command failed")]
PreCommand,
#[fail(display = "{}", _0)]
ProcessFailed(ExitStatus),
#[fail(display = "Unknown Linux Distribution")]
#[cfg(target_os = "linux")]
UnknownLinuxDistribution,
#[fail(display = "Detected Python is not the system Python")]
#[cfg(target_os = "linux")]
NotSystemPython,
#[fail(display = "Process execution failure")]
ProcessExecution,
#[fail(display = "Self-update failure")]
#[cfg(feature = "self-update")]
SelfUpdate,
#[fail(display = "A step should be skipped")]
SkipStep,
}
impl Fail for Error {
fn cause(&self) -> Option<&Fail> {
self.inner.cause()
}
fn backtrace(&self) -> Option<&Backtrace> {
self.inner.backtrace()
}
}
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt(&self.inner, f)
}
}
impl Error {
pub fn kind(&self) -> ErrorKind {
*self.inner.get_context()
}
}
impl From<ErrorKind> for Error {
fn from(kind: ErrorKind) -> Error {
Error {
inner: Context::new(kind),
}
}
}
impl From<Context<ErrorKind>> for Error {
fn from(inner: Context<ErrorKind>) -> Error {
Error { inner }
}
}

View File

@@ -1,27 +1,62 @@
//! Utilities for command execution
use super::error::{Error, ErrorKind};
use super::utils::Check; use super::utils::Check;
use failure; use failure::ResultExt;
use std::ffi::{OsStr, OsString}; use std::ffi::{OsStr, OsString};
use std::io;
use std::path::Path; use std::path::Path;
use std::process::{Child, Command, ExitStatus}; use std::process::{Child, Command, ExitStatus};
/// An enum telling whether Topgrade should perform dry runs or actually perform the steps.
#[derive(Clone, Copy, Debug)]
pub enum RunType {
/// Executing commands will just print the command with its argument.
Dry,
/// Executing commands will perform actual execution.
Wet,
}
impl RunType {
/// Create a new instance from a boolean telling whether to dry run.
pub fn new(dry_run: bool) -> Self {
if dry_run {
RunType::Dry
} else {
RunType::Wet
}
}
/// Create an instance of `Executor` that should run `program`.
pub fn execute<S: AsRef<OsStr>>(self, program: S) -> Executor {
match self {
RunType::Dry => Executor::Dry(DryCommand {
program: program.as_ref().into(),
..Default::default()
}),
RunType::Wet => Executor::Wet(Command::new(program)),
}
}
#[cfg(feature = "self-update")]
/// Tells whether we're performing a dry run.
pub fn dry(self) -> bool {
match self {
RunType::Dry => true,
RunType::Wet => false,
}
}
}
/// An enum providing a similar interface to `std::process::Command`.
/// If the enum is set to `Wet`, execution will be performed with `std::process::Command`.
/// If the enum is set to `Dry`, execution will just print the command with its arguments.
pub enum Executor { pub enum Executor {
Wet(Command), Wet(Command),
Dry(DryCommand), Dry(DryCommand),
} }
impl Executor { impl Executor {
pub fn new<S: AsRef<OsStr>>(program: S, dry: bool) -> Self { /// See `std::process::Command::arg`
if dry {
Executor::Dry(DryCommand {
program: program.as_ref().into(),
..Default::default()
})
} else {
Executor::Wet(Command::new(program))
}
}
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Executor { pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Executor {
match self { match self {
Executor::Wet(c) => { Executor::Wet(c) => {
@@ -35,6 +70,7 @@ impl Executor {
self self
} }
/// See `std::process::Command::args`
pub fn args<I, S>(&mut self, args: I) -> &mut Executor pub fn args<I, S>(&mut self, args: I) -> &mut Executor
where where
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
@@ -52,6 +88,7 @@ impl Executor {
self self
} }
/// See `std::process::Command::current_dir`
pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Executor { pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Executor {
match self { match self {
Executor::Wet(c) => { Executor::Wet(c) => {
@@ -63,9 +100,10 @@ impl Executor {
self self
} }
pub fn spawn(&mut self) -> Result<ExecutorChild, io::Error> { /// See `std::process::Command::spawn`
match self { pub fn spawn(&mut self) -> Result<ExecutorChild, Error> {
Executor::Wet(c) => c.spawn().map(ExecutorChild::Wet), let result = match self {
Executor::Wet(c) => c.spawn().context(ErrorKind::ProcessExecution).map(ExecutorChild::Wet)?,
Executor::Dry(c) => { Executor::Dry(c) => {
print!( print!(
"Dry running: {} {}", "Dry running: {} {}",
@@ -80,12 +118,22 @@ impl Executor {
Some(dir) => println!(" in {}", dir.to_string_lossy()), Some(dir) => println!(" in {}", dir.to_string_lossy()),
None => println!(), None => println!(),
}; };
Ok(ExecutorChild::Dry) ExecutorChild::Dry
} }
} };
Ok(result)
}
/// A convinence method for `spawn().wait().check()`.
/// Returns an error if something went wrong during the execution or if the
/// process exited with failure.
pub fn check_run(&mut self) -> Result<(), Error> {
self.spawn()?.wait()?.check()
} }
} }
/// A struct represending a command. Trying to execute it will just print its arguments.
#[derive(Default)] #[derive(Default)]
pub struct DryCommand { pub struct DryCommand {
program: OsString, program: OsString,
@@ -93,30 +141,55 @@ pub struct DryCommand {
directory: Option<OsString>, directory: Option<OsString>,
} }
/// The Result of spawn. Contains an actual `std::process::Child` if executed by a wet command.
pub enum ExecutorChild { pub enum ExecutorChild {
Wet(Child), Wet(Child),
Dry, Dry,
} }
impl ExecutorChild { impl ExecutorChild {
pub fn wait(&mut self) -> Result<ExecutorExitStatus, io::Error> { /// See `std::process::Child::wait`
match self { pub fn wait(&mut self) -> Result<ExecutorExitStatus, Error> {
ExecutorChild::Wet(c) => c.wait().map(ExecutorExitStatus::Wet), let result = match self {
ExecutorChild::Dry => Ok(ExecutorExitStatus::Dry), ExecutorChild::Wet(c) => c
} .wait()
.context(ErrorKind::ProcessExecution)
.map(ExecutorExitStatus::Wet)?,
ExecutorChild::Dry => ExecutorExitStatus::Dry,
};
Ok(result)
} }
} }
/// The Result of wait. Contains an actual `std::process::ExitStatus` if executed by a wet command.
pub enum ExecutorExitStatus { pub enum ExecutorExitStatus {
Wet(ExitStatus), Wet(ExitStatus),
Dry, Dry,
} }
impl Check for ExecutorExitStatus { impl Check for ExecutorExitStatus {
fn check(self) -> Result<(), failure::Error> { fn check(self) -> Result<(), Error> {
match self { match self {
ExecutorExitStatus::Wet(e) => e.check(), ExecutorExitStatus::Wet(e) => e.check(),
ExecutorExitStatus::Dry => Ok(()), ExecutorExitStatus::Dry => Ok(()),
} }
} }
} }
/// Extension methods for `std::process::Command`
pub trait CommandExt {
/// Run the command, wait for it to complete, check the return code and decode the output as UTF-8.
fn check_output(&mut self) -> Result<String, Error>;
}
impl CommandExt for Command {
fn check_output(&mut self) -> Result<String, Error> {
let output = self.output().context(ErrorKind::ProcessExecution)?;
let status = output.status;
if !status.success() {
Err(ErrorKind::ProcessFailed(status))?
}
Ok(String::from_utf8(output.stdout).context(ErrorKind::ProcessExecution)?)
}
}

View File

@@ -1,195 +0,0 @@
use super::executor::Executor;
use super::terminal::Terminal;
use super::utils::{self, Check, PathExt};
use directories::BaseDirs;
use failure::Error;
use std::path::PathBuf;
use std::process::Command;
const EMACS_UPGRADE: &str = include_str!("emacs.el");
#[must_use]
pub fn run_cargo_update(base_dirs: &BaseDirs, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(cargo_update) = base_dirs.home_dir().join(".cargo/bin/cargo-install-update").if_exists() {
terminal.print_separator("Cargo");
let success = || -> Result<(), Error> {
Executor::new(cargo_update, dry_run)
.args(&["install-update", "--git", "--all"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("Cargo", success));
}
None
}
#[must_use]
pub fn run_gem(base_dirs: &BaseDirs, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(gem) = utils::which("gem") {
if base_dirs.home_dir().join(".gem").exists() {
terminal.print_separator("RubyGems");
let success = || -> Result<(), Error> {
Executor::new(&gem, dry_run)
.args(&["update", "--user-install"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("RubyGems", success));
}
}
None
}
#[must_use]
pub fn run_emacs(base_dirs: &BaseDirs, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(emacs) = utils::which("emacs") {
if let Some(init_file) = base_dirs.home_dir().join(".emacs.d/init.el").if_exists() {
terminal.print_separator("Emacs");
let success = || -> Result<(), Error> {
Executor::new(&emacs, dry_run)
.args(&["--batch", "-l", init_file.to_str().unwrap(), "--eval", EMACS_UPGRADE])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("Emacs", success));
}
}
None
}
#[must_use]
#[cfg(
not(
any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)
)
)]
pub fn run_apm(terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(apm) = utils::which("apm") {
terminal.print_separator("Atom Package Manager");
let success = || -> Result<(), Error> {
Executor::new(&apm, dry_run)
.args(&["upgrade", "--confirm=false"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("apm", success));
}
None
}
#[must_use]
pub fn run_rustup(base_dirs: &BaseDirs, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(rustup) = utils::which("rustup") {
terminal.print_separator("rustup");
let success = || -> Result<(), Error> {
if rustup.is_descendant_of(base_dirs.home_dir()) {
Executor::new(&rustup, dry_run)
.args(&["self", "update"])
.spawn()?
.wait()?
.check()?;
}
Executor::new(&rustup, dry_run).arg("update").spawn()?.wait()?.check()?;
Ok(())
}().is_ok();
return Some(("rustup", success));
}
None
}
#[must_use]
pub fn run_opam_update(terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(opam) = utils::which("opam") {
terminal.print_separator("OCaml Package Manager");
let success = || -> Result<(), Error> {
Executor::new(&opam, dry_run).arg("update").spawn()?.wait()?.check()?;
Executor::new(&opam, dry_run).arg("upgrade").spawn()?.wait()?.check()?;
Ok(())
}().is_ok();
return Some(("OPAM", success));
}
None
}
#[must_use]
pub fn run_custom_command(name: &str, command: &str, terminal: &mut Terminal, dry_run: bool) -> Result<(), Error> {
terminal.print_separator(name);
Executor::new("sh", dry_run)
.arg("-c")
.arg(command)
.spawn()?
.wait()?
.check()?;
Ok(())
}
#[must_use]
pub fn run_composer_update(
base_dirs: &BaseDirs,
terminal: &mut Terminal,
dry_run: bool,
) -> Option<(&'static str, bool)> {
if let Some(composer) = utils::which("composer") {
let composer_home = || -> Result<PathBuf, Error> {
let output = Command::new(&composer)
.args(&["global", "config", "--absolute", "home"])
.output()?;
output.status.check()?;
Ok(PathBuf::from(&String::from_utf8(output.stdout)?))
}();
if let Ok(composer_home) = composer_home {
if composer_home.is_descendant_of(base_dirs.home_dir()) {
terminal.print_separator("Composer");
let success = || -> Result<(), Error> {
Executor::new(&composer, dry_run)
.args(&["global", "update"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("Composer", success));
}
}
}
None
}

View File

@@ -1,314 +0,0 @@
use super::executor::Executor;
use super::terminal::Terminal;
use super::utils::{which, Check};
use failure;
use std::fs;
use std::path::PathBuf;
use walkdir::WalkDir;
#[derive(Copy, Clone, Debug)]
pub enum Distribution {
Arch,
CentOS,
Fedora,
Debian,
Ubuntu,
}
#[derive(Debug, Fail)]
#[fail(display = "Unknown Linux Distribution")]
struct UnknownLinuxDistribution;
#[derive(Debug, Fail)]
#[fail(display = "Detected Python is not the system Python")]
struct NotSystemPython;
impl Distribution {
pub fn detect() -> Result<Self, failure::Error> {
let content = fs::read_to_string("/etc/os-release")?;
if content.contains("Arch") | content.contains("Manjaro") | content.contains("Antergos") {
return Ok(Distribution::Arch);
}
if content.contains("CentOS") {
return Ok(Distribution::CentOS);
}
if content.contains("Fedora") {
return Ok(Distribution::Fedora);
}
if content.contains("Ubuntu") {
return Ok(Distribution::Ubuntu);
}
if content.contains("Debian") {
return Ok(Distribution::Debian);
}
Err(UnknownLinuxDistribution.into())
}
#[must_use]
pub fn upgrade(
self,
sudo: &Option<PathBuf>,
terminal: &mut Terminal,
dry_run: bool,
) -> Option<(&'static str, bool)> {
terminal.print_separator("System update");
let success = match self {
Distribution::Arch => upgrade_arch_linux(&sudo, terminal, dry_run),
Distribution::CentOS => upgrade_redhat(&sudo, terminal, dry_run),
Distribution::Fedora => upgrade_fedora(&sudo, terminal, dry_run),
Distribution::Ubuntu | Distribution::Debian => upgrade_debian(&sudo, terminal, dry_run),
};
Some(("System update", success.is_ok()))
}
pub fn show_summary(self) {
if let Distribution::Arch = self {
show_pacnew();
}
}
}
pub fn show_pacnew() {
let mut iter = WalkDir::new("/etc")
.into_iter()
.filter_map(|e| e.ok())
.filter(|f| {
f.path()
.extension()
.filter(|ext| ext == &"pacnew" || ext == &"pacsave")
.is_some()
}).peekable();
if iter.peek().is_some() {
println!("\nPacman backup configuration files found:");
for entry in iter {
println!("{}", entry.path().display());
}
}
}
fn upgrade_arch_linux(sudo: &Option<PathBuf>, terminal: &mut Terminal, dry_run: bool) -> Result<(), failure::Error> {
if let Some(yay) = which("yay") {
if let Some(python) = which("python") {
if python != PathBuf::from("/usr/bin/python") {
terminal.print_warning(format!(
"Python detected at {:?}, which is probably not the system Python.
It's dangerous to run yay since Python based AUR packages will be installed in the wrong location",
python
));
return Err(NotSystemPython.into());
}
}
Executor::new(yay, dry_run).spawn()?.wait()?.check()?;
} else if let Some(sudo) = &sudo {
Executor::new(&sudo, dry_run)
.args(&["/usr/bin/pacman", "-Syu"])
.spawn()?
.wait()?
.check()?;
} else {
terminal.print_warning("No sudo or yay detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_redhat(sudo: &Option<PathBuf>, terminal: &mut Terminal, dry_run: bool) -> Result<(), failure::Error> {
if let Some(sudo) = &sudo {
Executor::new(&sudo, dry_run)
.args(&["/usr/bin/yum", "upgrade"])
.spawn()?
.wait()?
.check()?;
} else {
terminal.print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_fedora(sudo: &Option<PathBuf>, terminal: &mut Terminal, dry_run: bool) -> Result<(), failure::Error> {
if let Some(sudo) = &sudo {
Executor::new(&sudo, dry_run)
.args(&["/usr/bin/dnf", "upgrade"])
.spawn()?
.wait()?
.check()?;
} else {
terminal.print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_debian(sudo: &Option<PathBuf>, terminal: &mut Terminal, dry_run: bool) -> Result<(), failure::Error> {
if let Some(sudo) = &sudo {
Executor::new(&sudo, dry_run)
.args(&["/usr/bin/apt", "update"])
.spawn()?
.wait()?
.check()?;
Executor::new(&sudo, dry_run)
.args(&["/usr/bin/apt", "dist-upgrade"])
.spawn()?
.wait()?
.check()?;
} else {
terminal.print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
#[must_use]
pub fn run_needrestart(sudo: &Option<PathBuf>, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(sudo) = sudo {
if let Some(needrestart) = which("needrestart") {
terminal.print_separator("Check for needed restarts");
let success = || -> Result<(), failure::Error> {
Executor::new(&sudo, dry_run)
.arg(needrestart)
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("Restarts", success));
}
}
None
}
#[must_use]
pub fn run_fwupdmgr(terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(fwupdmgr) = which("fwupdmgr") {
terminal.print_separator("Firmware upgrades");
let success = || -> Result<(), failure::Error> {
Executor::new(&fwupdmgr, dry_run)
.arg("refresh")
.spawn()?
.wait()?
.check()?;
Executor::new(&fwupdmgr, dry_run)
.arg("get-updates")
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("Firmware upgrade", success));
}
None
}
#[must_use]
pub fn flatpak_user_update(terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(flatpak) = which("flatpak") {
terminal.print_separator("Flatpak User Packages");
let success = || -> Result<(), failure::Error> {
Executor::new(&flatpak, dry_run)
.args(&["update", "--user", "-y"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("Flatpak User Packages", success));
}
None
}
#[must_use]
pub fn flatpak_global_update(
sudo: &Option<PathBuf>,
terminal: &mut Terminal,
dry_run: bool,
) -> Option<(&'static str, bool)> {
if let Some(sudo) = sudo {
if let Some(flatpak) = which("flatpak") {
terminal.print_separator("Flatpak Global Packages");
let success = || -> Result<(), failure::Error> {
Executor::new(&sudo, dry_run)
.args(&[flatpak.to_str().unwrap(), "update", "-y"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("Flatpak Global Packages", success));
}
}
None
}
#[must_use]
pub fn run_snap(sudo: &Option<PathBuf>, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(sudo) = sudo {
if let Some(snap) = which("snap") {
if PathBuf::from("/var/snapd.socket").exists() {
terminal.print_separator("snap");
let success = || -> Result<(), failure::Error> {
Executor::new(&sudo, dry_run)
.args(&[snap.to_str().unwrap(), "refresh"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("snap", success));
}
}
}
None
}
#[must_use]
pub fn run_etc_update(sudo: &Option<PathBuf>, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(sudo) = sudo {
if let Some(etc_update) = which("etc-update") {
terminal.print_separator("etc-update");
let success = || -> Result<(), failure::Error> {
Executor::new(&sudo, dry_run)
.arg(&etc_update.to_str().unwrap())
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("snap", success));
}
}
None
}

View File

@@ -1,20 +0,0 @@
use super::executor::Executor;
use super::terminal::Terminal;
use super::utils::Check;
use failure;
#[must_use]
pub fn upgrade_macos(terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
terminal.print_separator("App Store");
let success = || -> Result<(), failure::Error> {
Executor::new("softwareupdate", dry_run)
.args(&["--install", "--all"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
Some(("App Store", success))
}

View File

@@ -1,114 +1,72 @@
extern crate directories;
extern crate failure;
extern crate which;
#[macro_use]
extern crate failure_derive;
extern crate toml;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate structopt;
extern crate serde;
extern crate shellexpand;
#[macro_use]
extern crate log;
extern crate console;
extern crate env_logger;
#[cfg(unix)]
extern crate nix;
#[cfg(unix)]
#[macro_use]
extern crate lazy_static;
extern crate term_size;
extern crate termcolor;
extern crate walkdir;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(unix)]
mod tmux;
#[cfg(unix)]
mod unix;
#[cfg(target_os = "windows")]
mod windows;
mod config; mod config;
mod ctrlc; mod ctrlc;
mod error;
mod executor; mod executor;
mod generic;
mod git;
mod node;
mod report; mod report;
#[cfg(feature = "self-update")]
mod self_update;
mod steps;
mod terminal; mod terminal;
mod utils; mod utils;
mod vim;
use self::config::Config; use self::config::{Config, Step};
use self::git::{Git, Repositories}; use self::error::{Error, ErrorKind};
use self::report::Report; use self::report::Report;
use self::terminal::Terminal; use self::steps::*;
use failure::Error; use self::terminal::*;
use failure::{Fail, ResultExt};
use log::debug;
use std::borrow::Cow; use std::borrow::Cow;
use std::env; use std::env;
use std::io::ErrorKind; use std::fmt::Debug;
use std::io;
#[cfg(windows)]
use std::path::PathBuf;
use std::process::exit; use std::process::exit;
use structopt::StructOpt;
#[derive(Fail, Debug)] fn execute<'a, F, M>(report: &mut Report<'a>, key: M, func: F, no_retry: bool) -> Result<(), Error>
#[fail(display = "A step failed")]
struct StepFailed;
#[derive(Fail, Debug)]
#[fail(display = "Cannot find the user base directories")]
struct NoBaseDirectories;
#[derive(Fail, Debug)]
#[fail(display = "Process Interrupted")]
pub struct Interrupted;
struct ExecutionContext {
terminal: Terminal,
}
fn execute<'a, F, M>(func: F, execution_context: &mut ExecutionContext) -> Result<Option<(M, bool)>, Error>
where where
M: Into<Cow<'a, str>>, F: Fn() -> Result<(), Error>,
F: Fn(&mut Terminal) -> Option<(M, bool)>, M: Into<Cow<'a, str>> + Debug,
{ {
while let Some((key, success)) = func(&mut execution_context.terminal) { debug!("Executing {:?}", key);
if success {
return Ok(Some((key, success)));
}
let running = ctrlc::running(); loop {
if !running { match func() {
ctrlc::set_running(true); Ok(()) => {
} report.push_result(Some((key, true)));
break;
let should_retry = execution_context.terminal.should_retry(running).map_err(|e| {
if e.kind() == ErrorKind::Interrupted {
Error::from(Interrupted)
} else {
Error::from(e)
} }
})?; Err(ref e) if e.kind() == ErrorKind::SkipStep => {
break;
}
Err(_) => {
let interrupted = ctrlc::interrupted();
if interrupted {
ctrlc::unset_interrupted();
}
if !should_retry { let should_ask = interrupted || !no_retry;
return Ok(Some((key, success))); let should_retry = should_ask && should_retry(interrupted).context(ErrorKind::Retry)?;
if !should_retry {
report.push_result(Some((key, false)));
break;
}
}
} }
} }
Ok(None) Ok(())
} }
fn run() -> Result<(), Error> { fn run() -> Result<(), Error> {
ctrlc::set_handler(); ctrlc::set_handler();
let opt = config::Opt::from_args(); let base_dirs = directories::BaseDirs::new().ok_or(ErrorKind::NoBaseDirectories)?;
let config = Config::load(&base_dirs)?;
if opt.run_in_tmux && env::var("TMUX").is_err() { if config.run_in_tmux() && env::var("TMUX").is_err() {
#[cfg(unix)] #[cfg(unix)]
{ {
tmux::run_in_tmux(); tmux::run_in_tmux();
@@ -116,23 +74,31 @@ fn run() -> Result<(), Error> {
} }
env_logger::init(); env_logger::init();
let base_dirs = directories::BaseDirs::new().ok_or(NoBaseDirectories)?;
let git = Git::new();
let mut git_repos = Repositories::new(&git);
let mut execution_context = ExecutionContext { let git = git::Git::new();
terminal: Terminal::new(), let mut git_repos = git::Repositories::new(&git);
};
let config = Config::read(&base_dirs)?;
let mut report = Report::new(); let mut report = Report::new();
#[cfg(target_os = "linux")] #[cfg(any(target_os = "freebsd", target_os = "linux"))]
let sudo = utils::which("sudo"); let sudo = utils::which("sudo");
let run_type = executor::RunType::new(config.dry_run());
#[cfg(feature = "self-update")]
{
if !run_type.dry() && env::var("TOPGRADE_NO_SELF_UPGRADE").is_err() {
if let Err(e) = self_update::self_update() {
print_warning(format!("Self update error: {}", e));
if let Some(cause) = e.cause() {
print_warning(format!("Caused by: {}", cause));
}
}
}
}
if let Some(commands) = config.pre_commands() { if let Some(commands) = config.pre_commands() {
for (name, command) in commands { for (name, command) in commands {
generic::run_custom_command(&name, &command, &mut execution_context.terminal, opt.dry_run)?; generic::run_custom_command(&name, &command, run_type).context(ErrorKind::PreCommand)?;
} }
} }
@@ -140,59 +106,81 @@ fn run() -> Result<(), Error> {
let powershell = windows::Powershell::new(); let powershell = windows::Powershell::new();
#[cfg(windows)] #[cfg(windows)]
report.push_result(execute( let should_run_powershell = powershell.profile().is_some() && config.should_run(Step::Powershell);
|terminal| powershell.update_modules(terminal, opt.dry_run),
&mut execution_context,
)?);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let distribution = linux::Distribution::detect(); let distribution = linux::Distribution::detect();
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
if !opt.no_system { if config.should_run(Step::System) {
match &distribution { match &distribution {
Ok(distribution) => { Ok(distribution) => {
report.push_result(execute( execute(
|terminal| distribution.upgrade(&sudo, terminal, opt.dry_run), &mut report,
&mut execution_context, "System update",
)?); || distribution.upgrade(&sudo, config.cleanup(), run_type),
config.no_retry(),
)?;
} }
Err(e) => { Err(e) => {
println!("Error detecting current distribution: {}", e); println!("Error detecting current distribution: {}", e);
} }
} }
report.push_result(execute( execute(
|terminal| linux::run_etc_update(&sudo, terminal, opt.dry_run), &mut report,
&mut execution_context, "etc-update",
)?); || linux::run_etc_update(sudo.as_ref(), run_type),
config.no_retry(),
)?;
} }
} }
#[cfg(windows)] #[cfg(windows)]
report.push_result(execute( execute(
|terminal| windows::run_chocolatey(terminal, opt.dry_run), &mut report,
&mut execution_context, "Chocolatey",
)?); || windows::run_chocolatey(run_type),
config.no_retry(),
)?;
#[cfg(windows)] #[cfg(windows)]
report.push_result(execute( execute(&mut report, "Scoop", || windows::run_scoop(run_type), config.no_retry())?;
|terminal| windows::run_scoop(terminal, opt.dry_run),
&mut execution_context,
)?);
#[cfg(unix)] #[cfg(unix)]
report.push_result(execute( execute(
|terminal| unix::run_homebrew(terminal, opt.dry_run), &mut report,
&mut execution_context, "brew",
)?); || unix::run_homebrew(config.cleanup(), run_type),
config.no_retry(),
)?;
#[cfg(target_os = "freebsd")]
execute(
&mut report,
"FreeBSD Packages",
|| freebsd::upgrade_packages(sudo.as_ref(), run_type),
config.no_retry(),
)?;
#[cfg(unix)]
execute(&mut report, "nix", || unix::run_nix(run_type), config.no_retry())?;
if !opt.no_emacs { if config.should_run(Step::Emacs) {
#[cfg(unix)]
git_repos.insert(base_dirs.home_dir().join(".emacs.d")); git_repos.insert(base_dirs.home_dir().join(".emacs.d"));
#[cfg(windows)]
{
git_repos.insert(base_dirs.data_dir().join(".emacs.d"));
if let Ok(home) = env::var("HOME") {
git_repos.insert(PathBuf::from(home).join(".emacs.d"));
}
}
} }
git_repos.insert(base_dirs.home_dir().join(".vim")); if config.should_run(Step::Vim) {
git_repos.insert(base_dirs.home_dir().join(".config/nvim")); git_repos.insert(base_dirs.home_dir().join(".vim"));
git_repos.insert(base_dirs.home_dir().join(".config/nvim"));
}
#[cfg(unix)] #[cfg(unix)]
{ {
@@ -201,6 +189,8 @@ fn run() -> Result<(), Error> {
git_repos.insert(base_dirs.home_dir().join(".tmux")); git_repos.insert(base_dirs.home_dir().join(".tmux"));
git_repos.insert(base_dirs.home_dir().join(".config/fish")); git_repos.insert(base_dirs.home_dir().join(".config/fish"));
git_repos.insert(base_dirs.config_dir().join("openbox")); git_repos.insert(base_dirs.config_dir().join("openbox"));
git_repos.insert(base_dirs.config_dir().join("bspwm"));
git_repos.insert(base_dirs.config_dir().join("i3"));
} }
#[cfg(windows)] #[cfg(windows)]
@@ -210,7 +200,7 @@ fn run() -> Result<(), Error> {
} }
} }
if !opt.no_git_repos { if config.should_run(Step::GitRepos) {
if let Some(custom_git_repos) = config.git_repos() { if let Some(custom_git_repos) = config.git_repos() {
for git_repo in custom_git_repos { for git_repo in custom_git_repos {
git_repos.insert(git_repo); git_repos.insert(git_repo);
@@ -218,155 +208,232 @@ fn run() -> Result<(), Error> {
} }
} }
for repo in git_repos.repositories() { for repo in git_repos.repositories() {
report.push_result(execute( execute(
|terminal| git.pull(&repo, terminal, opt.dry_run), &mut report,
&mut execution_context, format!("git: {}", utils::HumanizedPath::from(std::path::Path::new(&repo))),
)?); || git.pull(&repo, run_type),
config.no_retry(),
)?;
}
#[cfg(windows)]
{
if should_run_powershell {
execute(
&mut report,
"Powershell Modules Update",
|| powershell.update_modules(run_type),
config.no_retry(),
)?;
}
} }
#[cfg(unix)] #[cfg(unix)]
{ {
report.push_result(execute( execute(
|terminal| unix::run_zplug(&base_dirs, terminal, opt.dry_run), &mut report,
&mut execution_context, "zplug",
)?); || unix::run_zplug(&base_dirs, run_type),
report.push_result(execute( config.no_retry(),
|terminal| unix::run_fisher(&base_dirs, terminal, opt.dry_run), )?;
&mut execution_context, execute(
)?); &mut report,
report.push_result(execute( "fisher",
|terminal| tmux::run_tpm(&base_dirs, terminal, opt.dry_run), || unix::run_fisher(&base_dirs, run_type),
&mut execution_context, config.no_retry(),
)?); )?;
execute(
&mut report,
"tmux",
|| tmux::run_tpm(&base_dirs, run_type),
config.no_retry(),
)?;
} }
report.push_result(execute( execute(
|terminal| generic::run_rustup(&base_dirs, terminal, opt.dry_run), &mut report,
&mut execution_context, "rustup",
)?); || generic::run_rustup(&base_dirs, run_type),
report.push_result(execute( config.no_retry(),
|terminal| generic::run_cargo_update(&base_dirs, terminal, opt.dry_run), )?;
&mut execution_context, execute(
)?); &mut report,
"cargo",
|| generic::run_cargo_update(run_type),
config.no_retry(),
)?;
if !opt.no_emacs { if config.should_run(Step::Emacs) {
report.push_result(execute( execute(
|terminal| generic::run_emacs(&base_dirs, terminal, opt.dry_run), &mut report,
&mut execution_context, "Emacs",
)?); || generic::run_emacs(&base_dirs, run_type),
config.no_retry(),
)?;
} }
report.push_result(execute( execute(
|terminal| generic::run_opam_update(terminal, opt.dry_run), &mut report,
&mut execution_context, "opam",
)?); || generic::run_opam_update(run_type),
report.push_result(execute( config.no_retry(),
|terminal| vim::upgrade_vim(&base_dirs, terminal, opt.dry_run), )?;
&mut execution_context, execute(
)?); &mut report,
report.push_result(execute( "vcpkg",
|terminal| vim::upgrade_neovim(&base_dirs, terminal, opt.dry_run), || generic::run_vcpkg_update(run_type),
&mut execution_context, config.no_retry(),
)?); )?;
report.push_result(execute( execute(
|terminal| node::run_npm_upgrade(&base_dirs, terminal, opt.dry_run), &mut report,
&mut execution_context, "pipx",
)?); || generic::run_pipx_update(run_type),
report.push_result(execute( config.no_retry(),
|terminal| generic::run_composer_update(&base_dirs, terminal, opt.dry_run), )?;
&mut execution_context, #[cfg(unix)]
)?); execute(&mut report, "pearl", || unix::run_pearl(run_type), config.no_retry())?;
report.push_result(execute( execute(
|terminal| node::yarn_global_update(terminal, opt.dry_run), &mut report,
&mut execution_context, "jetpak",
)?); || generic::run_jetpack(run_type),
config.no_retry(),
)?;
#[cfg( if config.should_run(Step::Vim) {
not( execute(
any( &mut report,
target_os = "freebsd", "vim",
target_os = "openbsd", || vim::upgrade_vim(&base_dirs, run_type),
target_os = "netbsd", config.no_retry(),
target_os = "dragonfly" )?;
) execute(
) &mut report,
)] "Neovim",
report.push_result(execute( || vim::upgrade_neovim(&base_dirs, run_type),
|terminal| generic::run_apm(terminal, opt.dry_run), config.no_retry(),
&mut execution_context, )?;
)?); }
report.push_result(execute(
|terminal| generic::run_gem(&base_dirs, terminal, opt.dry_run), execute(
&mut execution_context, &mut report,
)?); "NPM",
|| node::run_npm_upgrade(&base_dirs, run_type),
config.no_retry(),
)?;
execute(
&mut report,
"composer",
|| generic::run_composer_update(&base_dirs, run_type),
config.no_retry(),
)?;
execute(
&mut report,
"yarn",
|| node::yarn_global_update(run_type),
config.no_retry(),
)?;
#[cfg(not(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
execute(&mut report, "apm", || generic::run_apm(run_type), config.no_retry())?;
if config.should_run(Step::Gem) {
execute(
&mut report,
"gem",
|| generic::run_gem(&base_dirs, run_type),
config.no_retry(),
)?;
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
report.push_result(execute( execute(
|terminal| linux::flatpak_user_update(terminal, opt.dry_run), &mut report,
&mut execution_context, "Flatpak",
)?); || linux::flatpak_update(run_type),
report.push_result(execute( config.no_retry(),
|terminal| linux::flatpak_global_update(&sudo, terminal, opt.dry_run), )?;
&mut execution_context, execute(
)?); &mut report,
report.push_result(execute( "snap",
|terminal| linux::run_snap(&sudo, terminal, opt.dry_run), || linux::run_snap(sudo.as_ref(), run_type),
&mut execution_context, config.no_retry(),
)?); )?;
} }
if let Some(commands) = config.commands() { if let Some(commands) = config.commands() {
for (name, command) in commands { for (name, command) in commands {
report.push_result(execute( execute(
|terminal| { &mut report,
Some(( name,
name, || generic::run_custom_command(&name, &command, run_type),
generic::run_custom_command(&name, &command, terminal, opt.dry_run).is_ok(), config.no_retry(),
)) )?;
},
&mut execution_context,
)?);
} }
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
report.push_result(execute( execute(
|terminal| linux::run_fwupdmgr(terminal, opt.dry_run), &mut report,
&mut execution_context, "Firmware upgrades",
)?); || linux::run_fwupdmgr(run_type),
report.push_result(execute( config.no_retry(),
|terminal| linux::run_needrestart(&sudo, terminal, opt.dry_run), )?;
&mut execution_context, execute(
)?); &mut report,
"Restarts",
|| linux::run_needrestart(sudo.as_ref(), run_type),
config.no_retry(),
)?;
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
if !opt.no_system { if config.should_run(Step::System) {
report.push_result(execute( execute(
|terminal| macos::upgrade_macos(terminal, opt.dry_run), &mut report,
&mut execution_context, "App Store",
)?); || macos::upgrade_macos(run_type),
config.no_retry(),
)?;
}
}
#[cfg(target_os = "freebsd")]
{
if config.should_run(Step::System) {
execute(
&mut report,
"FreeBSD Upgrade",
|| freebsd::upgrade_freebsd(sudo.as_ref(), run_type),
config.no_retry(),
)?;
} }
} }
#[cfg(windows)] #[cfg(windows)]
{ {
if !opt.no_system { if config.should_run(Step::System) {
report.push_result(execute( execute(
|terminal| powershell.windows_update(terminal, opt.dry_run), &mut report,
&mut execution_context, "Windows update",
)?); || powershell.windows_update(run_type),
config.no_retry(),
)?;
} }
} }
if !report.data().is_empty() { if !report.data().is_empty() {
execution_context.terminal.print_separator("Summary"); print_separator("Summary");
for (key, succeeded) in report.data() { for (key, succeeded) in report.data() {
execution_context.terminal.print_result(key, *succeeded); print_result(key, *succeeded);
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -375,12 +442,15 @@ fn run() -> Result<(), Error> {
distribution.show_summary(); distribution.show_summary();
} }
} }
#[cfg(target_os = "freebsd")]
freebsd::audit_packages(&sudo).ok();
} }
if report.data().iter().all(|(_, succeeded)| *succeeded) { if report.data().iter().all(|(_, succeeded)| *succeeded) {
Ok(()) Ok(())
} else { } else {
Err(StepFailed.into()) Err(ErrorKind::StepFailed)?
} }
} }
@@ -390,13 +460,21 @@ fn main() {
exit(0); exit(0);
} }
Err(error) => { Err(error) => {
match error let should_print = match error.kind() {
.downcast::<StepFailed>() ErrorKind::StepFailed => false,
.map(|_| ()) ErrorKind::Retry => error
.or_else(|error| error.downcast::<Interrupted>().map(|_| ())) .cause()
{ .and_then(|cause| cause.downcast_ref::<io::Error>())
Ok(_) => (), .filter(|io_error| io_error.kind() == io::ErrorKind::Interrupted)
Err(error) => println!("ERROR: {}", error), .is_none(),
_ => true,
};
if should_print {
println!("Error: {}", error);
if let Some(cause) = error.cause() {
println!("Caused by: {}", cause);
}
} }
exit(1); exit(1);
} }

View File

@@ -1,69 +0,0 @@
use super::executor::Executor;
use super::terminal::Terminal;
use super::utils::{which, Check, PathExt};
use directories::BaseDirs;
use failure;
use std::path::PathBuf;
use std::process::Command;
struct NPM {
command: PathBuf,
}
impl NPM {
fn new(command: PathBuf) -> Self {
Self { command }
}
fn root(&self) -> Result<PathBuf, failure::Error> {
let output = Command::new(&self.command).args(&["root", "-g"]).output()?;
output.status.check()?;
Ok(PathBuf::from(&String::from_utf8(output.stdout)?))
}
fn upgrade(&self, dry_run: bool) -> Result<(), failure::Error> {
Executor::new(&self.command, dry_run)
.args(&["update", "-g"])
.spawn()?
.wait()?
.check()?;
Ok(())
}
}
#[must_use]
pub fn run_npm_upgrade(base_dirs: &BaseDirs, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(npm) = which("npm").map(NPM::new) {
if let Ok(npm_root) = npm.root() {
if npm_root.is_descendant_of(base_dirs.home_dir()) {
terminal.print_separator("Node Package Manager");
let success = npm.upgrade(dry_run).is_ok();
return Some(("NPM", success));
}
}
}
None
}
#[must_use]
pub fn yarn_global_update(terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(yarn) = which("yarn") {
terminal.print_separator("Yarn");
let success = || -> Result<(), failure::Error> {
Executor::new(&yarn, dry_run)
.args(&["global", "upgrade", "-s"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("yarn", success));
}
None
}

54
src/self_update.rs Normal file
View File

@@ -0,0 +1,54 @@
use super::error::{Error, ErrorKind};
use super::terminal::*;
use failure::ResultExt;
use self_update_crate;
use self_update_crate::backends::github::{GitHubUpdateStatus, Update};
#[cfg(unix)]
use std::env;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
#[cfg(unix)]
use std::process::Command;
pub fn self_update() -> Result<(), Error> {
print_separator("Self update");
#[cfg(unix)]
let current_exe = env::current_exe();
let target = self_update_crate::get_target().context(ErrorKind::SelfUpdate)?;
let result = Update::configure()
.and_then(|mut u| {
u.repo_owner("r-darwish")
.repo_name("topgrade")
.target(&target)
.bin_name(if cfg!(windows) { "topgrade.exe" } else { "topgrade" })
.show_output(false)
.show_download_progress(true)
.current_version(self_update_crate::cargo_crate_version!())
.no_confirm(true)
.build()
})
.and_then(|u| u.update_extended())
.context(ErrorKind::SelfUpdate)?;
if let GitHubUpdateStatus::Updated(release) = &result {
println!("\nTopgrade upgraded to {}:\n", release.version());
println!("{}", release.body);
} else {
println!("Topgrade is up-to-date");
}
#[cfg(unix)]
{
if result.updated() {
print_warning("Respawning...");
let err = Command::new(current_exe.context(ErrorKind::SelfUpdate)?)
.args(env::args().skip(1))
.env("TOPGRADE_NO_SELF_UPGRADE", "")
.exec();
Err(err).context(ErrorKind::SelfUpdate)?
}
}
Ok(())
}

132
src/steps/generic.rs Normal file
View File

@@ -0,0 +1,132 @@
use crate::error::{Error, ErrorKind};
use crate::executor::{CommandExt, RunType};
use crate::terminal::print_separator;
use crate::utils::{self, PathExt};
use directories::BaseDirs;
use failure::ResultExt;
use std::path::PathBuf;
use std::process::Command;
const EMACS_UPGRADE: &str = include_str!("emacs.el");
pub fn run_cargo_update(run_type: RunType) -> Result<(), Error> {
let cargo_update = utils::require("cargo-install-update")?;
print_separator("Cargo");
run_type
.execute(cargo_update)
.args(&["install-update", "--git", "--all"])
.check_run()
}
pub fn run_gem(base_dirs: &BaseDirs, run_type: RunType) -> Result<(), Error> {
let gem = utils::require("gem")?;
base_dirs.home_dir().join(".gem").require()?;
print_separator("RubyGems");
run_type.execute(&gem).args(&["update", "--user-install"]).check_run()
}
pub fn run_emacs(base_dirs: &BaseDirs, run_type: RunType) -> Result<(), Error> {
let emacs = utils::require("emacs")?;
let init_file = base_dirs.home_dir().join(".emacs.d/init.el").require()?;
print_separator("Emacs");
run_type
.execute(&emacs)
.args(&["--batch", "-l", init_file.to_str().unwrap(), "--eval", EMACS_UPGRADE])
.check_run()
}
#[cfg(not(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
pub fn run_apm(run_type: RunType) -> Result<(), Error> {
let apm = utils::require("apm")?;
print_separator("Atom Package Manager");
run_type.execute(&apm).args(&["upgrade", "--confirm=false"]).check_run()
}
pub fn run_rustup(base_dirs: &BaseDirs, run_type: RunType) -> Result<(), Error> {
let rustup = utils::require("rustup")?;
print_separator("rustup");
if rustup
.canonicalize()
.context(ErrorKind::StepFailed)?
.is_descendant_of(base_dirs.home_dir())
{
run_type.execute(&rustup).args(&["self", "update"]).check_run()?;
}
run_type.execute(&rustup).arg("update").check_run()
}
pub fn run_jetpack(run_type: RunType) -> Result<(), Error> {
let jetpack = utils::require("jetpack")?;
print_separator("Jetpack");
run_type.execute(&jetpack).args(&["global", "update"]).check_run()
}
pub fn run_opam_update(run_type: RunType) -> Result<(), Error> {
let opam = utils::require("opam")?;
print_separator("OCaml Package Manager");
run_type.execute(&opam).arg("update").check_run()?;
run_type.execute(&opam).arg("upgrade").check_run()
}
pub fn run_vcpkg_update(run_type: RunType) -> Result<(), Error> {
let vcpkg = utils::require("vcpkg")?;
print_separator("vcpkg");
run_type.execute(&vcpkg).args(&["upgrade", "--no-dry-run"]).check_run()
}
pub fn run_pipx_update(run_type: RunType) -> Result<(), Error> {
let pipx = utils::require("pipx")?;
print_separator("pipx");
run_type.execute(&pipx).arg("upgrade-all").check_run()
}
pub fn run_custom_command(name: &str, command: &str, run_type: RunType) -> Result<(), Error> {
print_separator(name);
run_type.execute("sh").arg("-c").arg(command).check_run()
}
pub fn run_composer_update(base_dirs: &BaseDirs, run_type: RunType) -> Result<(), Error> {
let composer = utils::require("composer")?;
let composer_home = Command::new(&composer)
.args(&["global", "config", "--absolute", "home"])
.check_output()
.map_err(|_| Error::from(ErrorKind::SkipStep))
.map(PathBuf::from)
.and_then(|p| p.require())?;
if !composer_home.is_descendant_of(base_dirs.home_dir()) {
Err(ErrorKind::SkipStep)?;
}
print_separator("Composer");
run_type.execute(&composer).args(&["global", "update"]).check_run()?;
if let Some(valet) = utils::which("valet") {
run_type.execute(&valet).arg("install").check_run()?;
}
Ok(())
}

View File

@@ -1,16 +1,19 @@
use super::executor::Executor; use crate::error::Error;
use super::terminal::Terminal; use crate::executor::{CommandExt, RunType};
use super::utils::{which, Check}; use crate::terminal::print_separator;
use failure::Error; use crate::utils::{which, HumanizedPath};
use log::{debug, error};
use std::collections::HashSet; use std::collections::HashSet;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
#[derive(Debug)]
pub struct Git { pub struct Git {
git: Option<PathBuf>, git: Option<PathBuf>,
} }
#[derive(Debug)]
pub struct Repositories<'a> { pub struct Repositories<'a> {
git: &'a Git, git: &'a Git,
repositories: HashSet<String>, repositories: HashSet<String>,
@@ -37,15 +40,10 @@ impl Git {
let output = Command::new(&git) let output = Command::new(&git)
.args(&["rev-parse", "--show-toplevel"]) .args(&["rev-parse", "--show-toplevel"])
.current_dir(path) .current_dir(path)
.output(); .check_output()
.ok()
if let Ok(output) = output { .map(|output| output.trim().to_string());
if !output.status.success() { return output;
return None;
}
return Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
} }
} }
Err(e) => match e.kind() { Err(e) => match e.kind() {
@@ -57,32 +55,18 @@ impl Git {
None None
} }
pub fn pull<P: AsRef<Path>>(&self, path: P, terminal: &mut Terminal, dry_run: bool) -> Option<(String, bool)> { pub fn pull<P: AsRef<Path>>(&self, path: P, run_type: RunType) -> Result<(), Error> {
let path = path.as_ref(); let path = path.as_ref();
terminal.print_separator(format!("Pulling {}", path.display())); print_separator(format!("Pulling {}", HumanizedPath::from(path)));
let git = self.git.as_ref().unwrap(); let git = self.git.as_ref().unwrap();
let success = || -> Result<(), Error> { run_type
Executor::new(git, dry_run) .execute(git)
.args(&["pull", "--rebase", "--autostash"]) .args(&["pull", "--rebase", "--autostash"])
.current_dir(&path) .current_dir(&path)
.spawn()? .check_run()
.wait()?
.check()?;
Executor::new(git, dry_run)
.args(&["submodule", "update", "--init", "--recursive"])
.current_dir(&path)
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
Some((format!("git: {}", path.display()), success))
} }
} }

9
src/steps/mod.rs Normal file
View File

@@ -0,0 +1,9 @@
pub mod generic;
pub mod git;
pub mod node;
pub mod os;
#[cfg(unix)]
pub mod tmux;
pub mod vim;
pub use self::os::*;

48
src/steps/node.rs Normal file
View File

@@ -0,0 +1,48 @@
use crate::error::{Error, ErrorKind};
use crate::executor::{CommandExt, RunType};
use crate::terminal::print_separator;
use crate::utils::{require, PathExt};
use directories::BaseDirs;
use std::path::PathBuf;
use std::process::Command;
struct NPM {
command: PathBuf,
}
impl NPM {
fn new(command: PathBuf) -> Self {
Self { command }
}
fn root(&self) -> Result<PathBuf, Error> {
Command::new(&self.command)
.args(&["root", "-g"])
.check_output()
.map(PathBuf::from)
}
fn upgrade(&self, run_type: RunType) -> Result<(), Error> {
run_type.execute(&self.command).args(&["update", "-g"]).check_run()?;
Ok(())
}
}
pub fn run_npm_upgrade(base_dirs: &BaseDirs, run_type: RunType) -> Result<(), Error> {
let npm = require("npm").map(NPM::new)?;
let npm_root = npm.root()?;
if !npm_root.is_descendant_of(base_dirs.home_dir()) {
Err(ErrorKind::SkipStep)?;
}
print_separator("Node Package Manager");
npm.upgrade(run_type)
}
pub fn yarn_global_update(run_type: RunType) -> Result<(), Error> {
let yarn = require("yarn")?;
print_separator("Yarn");
run_type.execute(&yarn).args(&["global", "upgrade", "-s"]).check_run()
}

35
src/steps/os/freebsd.rs Normal file
View File

@@ -0,0 +1,35 @@
use crate::error::{Error, ErrorKind};
use crate::executor::RunType;
use crate::terminal::print_separator;
use crate::utils::require_option;
use failure::ResultExt;
use std::path::PathBuf;
use std::process::Command;
pub fn upgrade_freebsd(sudo: Option<&PathBuf>, run_type: RunType) -> Result<(), Error> {
let sudo = require_option(sudo)?;
print_separator("FreeBSD Update");
run_type
.execute(sudo)
.args(&["/usr/sbin/freebsd-update", "fetch", "install"])
.check_run()
}
pub fn upgrade_packages(sudo: Option<&PathBuf>, run_type: RunType) -> Result<(), Error> {
let sudo = require_option(sudo)?;
print_separator("FreeBSD Packages");
run_type.execute(sudo).args(&["/usr/sbin/pkg", "upgrade"]).check_run()
}
pub fn audit_packages(sudo: &Option<PathBuf>) -> Result<(), Error> {
if let Some(sudo) = sudo {
println!();
Command::new(sudo)
.args(&["/usr/sbin/pkg", "audit", "-Fr"])
.spawn()
.context(ErrorKind::ProcessExecution)?
.wait()
.context(ErrorKind::ProcessExecution)?;
}
Ok(())
}

295
src/steps/os/linux.rs Normal file
View File

@@ -0,0 +1,295 @@
use crate::error::{Error, ErrorKind};
use crate::executor::RunType;
use crate::terminal::{print_separator, print_warning};
use crate::utils::{require, require_option, which};
use failure::ResultExt;
use std::fs;
use std::path::PathBuf;
use walkdir::WalkDir;
#[derive(Copy, Clone, Debug)]
pub enum Distribution {
Arch,
CentOS,
Fedora,
Debian,
Ubuntu,
Gentoo,
OpenSuse,
Void,
}
impl Distribution {
pub fn detect() -> Result<Self, Error> {
let content = fs::read_to_string("/etc/os-release").context(ErrorKind::UnknownLinuxDistribution)?;
if content.contains("Arch") | content.contains("Manjaro") | content.contains("Antergos") {
return Ok(Distribution::Arch);
}
if content.contains("CentOS") || content.contains("Oracle Linux") {
return Ok(Distribution::CentOS);
}
if content.contains("Fedora") {
return Ok(Distribution::Fedora);
}
if content.contains("Ubuntu") {
return Ok(Distribution::Ubuntu);
}
if content.contains("Debian") {
return Ok(Distribution::Debian);
}
if content.contains("openSUSE") {
return Ok(Distribution::OpenSuse);
}
if content.contains("void") {
return Ok(Distribution::Void);
}
if PathBuf::from("/etc/gentoo-release").exists() {
return Ok(Distribution::Gentoo);
}
Err(ErrorKind::UnknownLinuxDistribution)?
}
#[must_use]
pub fn upgrade(self, sudo: &Option<PathBuf>, cleanup: bool, run_type: RunType) -> Result<(), Error> {
print_separator("System update");
match self {
Distribution::Arch => upgrade_arch_linux(&sudo, cleanup, run_type),
Distribution::CentOS => upgrade_redhat(&sudo, run_type),
Distribution::Fedora => upgrade_fedora(&sudo, run_type),
Distribution::Ubuntu | Distribution::Debian => upgrade_debian(&sudo, cleanup, run_type),
Distribution::Gentoo => upgrade_gentoo(&sudo, run_type),
Distribution::OpenSuse => upgrade_opensuse(&sudo, run_type),
Distribution::Void => upgrade_void(&sudo, run_type),
}
}
pub fn show_summary(self) {
if let Distribution::Arch = self {
show_pacnew();
}
}
}
pub fn show_pacnew() {
let mut iter = WalkDir::new("/etc")
.into_iter()
.filter_map(|e| e.ok())
.filter(|f| {
f.path()
.extension()
.filter(|ext| ext == &"pacnew" || ext == &"pacsave")
.is_some()
})
.peekable();
if iter.peek().is_some() {
println!("\nPacman backup configuration files found:");
for entry in iter {
println!("{}", entry.path().display());
}
}
}
fn upgrade_arch_linux(sudo: &Option<PathBuf>, cleanup: bool, run_type: RunType) -> Result<(), Error> {
if let Some(yay) = which("yay") {
if let Some(python) = which("python") {
if python != PathBuf::from("/usr/bin/python") {
print_warning(format!(
"Python detected at {:?}, which is probably not the system Python.
It's dangerous to run yay since Python based AUR packages will be installed in the wrong location",
python
));
return Err(ErrorKind::NotSystemPython)?;
}
}
run_type.execute(yay).check_run()?;
} else if let Some(sudo) = &sudo {
run_type.execute(&sudo).args(&["/usr/bin/pacman", "-Syu"]).check_run()?;
} else {
print_warning("No sudo or yay detected. Skipping system upgrade");
}
if cleanup {
if let Some(sudo) = &sudo {
run_type.execute(&sudo).args(&["/usr/bin/pacman", "-Scc"]).check_run()?;
}
}
Ok(())
}
fn upgrade_redhat(sudo: &Option<PathBuf>, run_type: RunType) -> Result<(), Error> {
if let Some(sudo) = &sudo {
run_type.execute(&sudo).args(&["/usr/bin/yum", "upgrade"]).check_run()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_opensuse(sudo: &Option<PathBuf>, run_type: RunType) -> Result<(), Error> {
if let Some(sudo) = &sudo {
run_type
.execute(&sudo)
.args(&["/usr/bin/zypper", "refresh"])
.check_run()?;
run_type
.execute(&sudo)
.args(&["/usr/bin/zypper", "dist-upgrade"])
.check_run()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_void(sudo: &Option<PathBuf>, run_type: RunType) -> Result<(), Error> {
if let Some(sudo) = &sudo {
run_type
.execute(&sudo)
.args(&["/usr/bin/xbps-install", "-Su"])
.check_run()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_fedora(sudo: &Option<PathBuf>, run_type: RunType) -> Result<(), Error> {
if let Some(sudo) = &sudo {
run_type.execute(&sudo).args(&["/usr/bin/dnf", "upgrade"]).check_run()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_gentoo(sudo: &Option<PathBuf>, run_type: RunType) -> Result<(), Error> {
if let Some(sudo) = &sudo {
if let Some(layman) = which("layman") {
run_type.execute(&sudo).arg(layman).args(&["-s", "ALL"]).check_run()?;
}
println!("Syncing portage");
run_type
.execute(&sudo)
.arg("/usr/bin/emerge")
.args(&["-q", "--sync"])
.check_run()?;
if let Some(eix_update) = which("eix-update") {
run_type.execute(&sudo).arg(eix_update).check_run()?;
}
run_type
.execute(&sudo)
.arg("/usr/bin/emerge")
.args(&["-uDNa", "world"])
.check_run()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_debian(sudo: &Option<PathBuf>, cleanup: bool, run_type: RunType) -> Result<(), Error> {
if let Some(sudo) = &sudo {
run_type.execute(&sudo).args(&["/usr/bin/apt", "update"]).check_run()?;
run_type
.execute(&sudo)
.args(&["/usr/bin/apt", "dist-upgrade"])
.check_run()?;
if cleanup {
run_type.execute(&sudo).args(&["/usr/bin/apt", "clean"]).check_run()?;
run_type
.execute(&sudo)
.args(&["/usr/bin/apt", "autoremove"])
.check_run()?;
}
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
pub fn run_needrestart(sudo: Option<&PathBuf>, run_type: RunType) -> Result<(), Error> {
let sudo = require_option(sudo)?;
let needrestart = require("needrestart")?;
print_separator("Check for needed restarts");
run_type.execute(&sudo).arg(needrestart).check_run()?;
Ok(())
}
#[must_use]
pub fn run_fwupdmgr(run_type: RunType) -> Result<(), Error> {
let fwupdmgr = require("fwupdmgr")?;
print_separator("Firmware upgrades");
run_type.execute(&fwupdmgr).arg("refresh").check_run()?;
run_type.execute(&fwupdmgr).arg("get-updates").check_run()
}
#[must_use]
pub fn flatpak_update(run_type: RunType) -> Result<(), Error> {
let flatpak = require("flatpak")?;
print_separator("Flatpak User Packages");
run_type
.execute(&flatpak)
.args(&["update", "--user", "-y"])
.check_run()?;
run_type
.execute(&flatpak)
.args(&["update", "--system", "-y"])
.check_run()
}
#[must_use]
pub fn run_snap(sudo: Option<&PathBuf>, run_type: RunType) -> Result<(), Error> {
let sudo = require_option(sudo)?;
let snap = require("snap")?;
if !PathBuf::from("/var/snapd.socket").exists() {
Err(ErrorKind::SkipStep)?;
}
print_separator("snap");
run_type
.execute(sudo)
.args(&[snap.to_str().unwrap(), "refresh"])
.check_run()
}
#[must_use]
pub fn run_etc_update(sudo: Option<&PathBuf>, run_type: RunType) -> Result<(), Error> {
let sudo = require_option(sudo)?;
let etc_update = require("etc_update")?;
print_separator("etc-update");
run_type.execute(sudo).arg(&etc_update.to_str().unwrap()).check_run()
}

13
src/steps/os/macos.rs Normal file
View File

@@ -0,0 +1,13 @@
use crate::error::Error;
use crate::executor::RunType;
use crate::terminal::print_separator;
#[must_use]
pub fn upgrade_macos(run_type: RunType) -> Result<(), Error> {
print_separator("App Store");
run_type
.execute("softwareupdate")
.args(&["--install", "--all"])
.check_run()
}

10
src/steps/os/mod.rs Normal file
View File

@@ -0,0 +1,10 @@
#[cfg(target_os = "freebsd")]
pub mod freebsd;
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(unix)]
pub mod unix;
#[cfg(target_os = "windows")]
pub mod windows;

83
src/steps/os/unix.rs Normal file
View File

@@ -0,0 +1,83 @@
use crate::error::Error;
use crate::executor::{CommandExt, RunType};
use crate::terminal::print_separator;
use crate::utils::{require, PathExt};
use directories::BaseDirs;
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn run_zplug(base_dirs: &BaseDirs, run_type: RunType) -> Result<(), Error> {
let zsh = require("zsh")?;
env::var("ZPLUG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| base_dirs.home_dir().join("zplug"))
.require()?;
let zshrc = env::var("ZDOTDIR")
.map(|p| Path::new(&p).join(".zshrc"))
.unwrap_or_else(|_| base_dirs.home_dir().join(".zshrc"))
.require()?;
print_separator("zplug");
let cmd = format!("source {} && zplug update", zshrc.display());
run_type.execute(zsh).args(&["-c", cmd.as_str()]).check_run()
}
pub fn run_fisher(base_dirs: &BaseDirs, run_type: RunType) -> Result<(), Error> {
let fish = require("fish")?;
base_dirs
.home_dir()
.join(".config/fish/functions/fisher.fish")
.require()?;
run_type
.execute(&fish)
.args(&["-c", "fisher self-update"])
.check_run()?;
run_type.execute(&fish).args(&["-c", "fisher"]).check_run()
}
#[must_use]
pub fn run_homebrew(cleanup: bool, run_type: RunType) -> Result<(), Error> {
let brew = require("brew")?;
print_separator("Brew");
run_type.execute(&brew).arg("update").check_run()?;
run_type.execute(&brew).arg("upgrade").check_run()?;
let cask_upgrade_exists = Command::new(&brew)
.args(&["--repository", "buo/cask-upgrade"])
.check_output()
.map(|p| Path::new(p.trim()).exists())?;
if cask_upgrade_exists {
run_type.execute(&brew).args(&["cu", "-ay"]).check_run()?;
} else {
run_type.execute(&brew).args(&["cask", "upgrade"]).check_run()?;
}
if cleanup {
run_type.execute(&brew).arg("cleanup").check_run()?;
}
Ok(())
}
#[must_use]
pub fn run_nix(run_type: RunType) -> Result<(), Error> {
let nix = require("nix")?;
let nix_env = require("nix_env")?;
print_separator("Nix");
run_type.execute(&nix).arg("upgrade-nix").check_run()?;
run_type.execute(&nix_env).arg("--upgrade").check_run()
}
pub fn run_pearl(run_type: RunType) -> Result<(), Error> {
let pearl = require("pearl")?;
print_separator("pearl");
run_type.execute(&pearl).arg("update").check_run()
}

82
src/steps/os/windows.rs Normal file
View File

@@ -0,0 +1,82 @@
use crate::error::{Error, ErrorKind};
use crate::executor::{CommandExt, RunType};
use crate::terminal::{is_dumb, print_separator};
use crate::utils::{require, require_option, which};
use std::path::PathBuf;
use std::process::Command;
pub fn run_chocolatey(run_type: RunType) -> Result<(), Error> {
let choco = require("choco")?;
print_separator("Chocolatey");
run_type.execute(&choco).args(&["upgrade", "all"]).check_run()
}
pub fn run_scoop(run_type: RunType) -> Result<(), Error> {
let scoop = require("scoop")?;
print_separator("Scoop");
run_type.execute(&scoop).args(&["update"]).check_run()?;
run_type.execute(&scoop).args(&["update", "*"]).check_run()
}
pub struct Powershell {
path: Option<PathBuf>,
profile: Option<PathBuf>,
}
impl Powershell {
/// Returns a powershell instance.
///
/// If the powershell binary is not found, or the current terminal is dumb
/// then the instance of this struct will skip all the powershell steps.
pub fn new() -> Self {
let path = which("powershell").filter(|_| !is_dumb());
let profile = path.as_ref().and_then(|path| {
Command::new(path)
.args(&["-Command", "echo $profile"])
.check_output()
.map(|output| PathBuf::from(output.trim()))
.ok()
});
Powershell { path, profile }
}
pub fn has_command(powershell: &PathBuf, command: &str) -> bool {
|| -> Result<(), Error> {
Command::new(&powershell)
.args(&["-Command", &format!("Get-Command {}", command)])
.check_output()?;
Ok(())
}()
.is_ok()
}
pub fn profile(&self) -> Option<&PathBuf> {
self.profile.as_ref()
}
pub fn update_modules(&self, run_type: RunType) -> Result<(), Error> {
let powershell = require_option(self.path.as_ref())?;
print_separator("Powershell Modules Update");
run_type.execute(&powershell).args(&["Update-Module", "-v"]).check_run()
}
pub fn windows_update(&self, run_type: RunType) -> Result<(), Error> {
let powershell = require_option(self.path.as_ref())?;
if !Self::has_command(&powershell, "Install-WindowsUpdate") {
Err(ErrorKind::SkipStep)?;
}
print_separator("Windows Update");
run_type
.execute(&powershell)
.args(&["-Command", "Install-WindowsUpdate -MicrosoftUpdate -AcceptAll -Verbose"])
.check_run()
}
}

View File

@@ -1,32 +1,24 @@
use super::executor::Executor; use crate::error::{Error, ErrorKind};
use super::terminal::Terminal; use crate::executor::RunType;
use super::utils::which; use crate::terminal::print_separator;
use super::utils::{Check, PathExt}; use crate::utils::{which, Check, PathExt};
use directories::BaseDirs; use directories::BaseDirs;
use failure::Error; use failure::ResultExt;
use std::env; use std::env;
use std::io; use std::io;
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
pub fn run_tpm(base_dirs: &BaseDirs, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> { pub fn run_tpm(base_dirs: &BaseDirs, run_type: RunType) -> Result<(), Error> {
if let Some(tpm) = base_dirs let tpm = base_dirs
.home_dir() .home_dir()
.join(".tmux/plugins/tpm/bin/update_plugins") .join(".tmux/plugins/tpm/bin/update_plugins")
.if_exists() .require()?;
{
terminal.print_separator("tmux plugins");
let success = || -> Result<(), Error> { print_separator("tmux plugins");
Executor::new(&tpm, dry_run).arg("all").spawn()?.wait()?.check()?;
Ok(())
}().is_ok();
return Some(("tmux", success)); run_type.execute(&tpm).arg("all").check_run()
}
None
} }
fn has_session(tmux: &Path, session_name: &str) -> Result<bool, io::Error> { fn has_session(tmux: &Path, session_name: &str) -> Result<bool, io::Error> {
@@ -40,8 +32,10 @@ fn has_session(tmux: &Path, session_name: &str) -> Result<bool, io::Error> {
fn run_in_session(tmux: &Path, command: &str) -> Result<(), Error> { fn run_in_session(tmux: &Path, command: &str) -> Result<(), Error> {
Command::new(tmux) Command::new(tmux)
.args(&["new-window", "-a", "-t", "topgrade:1", command]) .args(&["new-window", "-a", "-t", "topgrade:1", command])
.spawn()? .spawn()
.wait()? .context(ErrorKind::ProcessExecution)?
.wait()
.context(ErrorKind::ProcessExecution)?
.check()?; .check()?;
Ok(()) Ok(())
@@ -70,7 +64,8 @@ pub fn run_in_tmux() -> ! {
"set", "set",
"remain-on-exit", "remain-on-exit",
"on", "on",
]).exec(); ])
.exec();
panic!("{:?}", err); panic!("{:?}", err);
} }

View File

@@ -1,8 +1,8 @@
use super::executor::Executor; use crate::error::Error;
use super::terminal::Terminal; use crate::executor::RunType;
use super::utils::{which, Check, PathExt}; use crate::terminal::print_separator;
use crate::utils::{require, require_option, PathExt};
use directories::BaseDirs; use directories::BaseDirs;
use failure;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
@@ -54,13 +54,9 @@ fn nvimrc(base_dirs: &BaseDirs) -> Option<PathBuf> {
} }
#[must_use] #[must_use]
fn upgrade( fn upgrade(vim: &PathBuf, vimrc: &PathBuf, plugin_framework: PluginFramework, run_type: RunType) -> Result<(), Error> {
vim: &PathBuf, run_type
vimrc: &PathBuf, .execute(&vim)
plugin_framework: PluginFramework,
dry_run: bool,
) -> Result<(), failure::Error> {
Executor::new(&vim, dry_run)
.args(&[ .args(&[
"-N", "-N",
"-u", "-u",
@@ -72,9 +68,8 @@ fn upgrade(
"-e", "-e",
"-s", "-s",
"-V1", "-V1",
]).spawn()? ])
.wait()? .check_run()?;
.check()?;
println!(); println!();
@@ -82,31 +77,21 @@ fn upgrade(
} }
#[must_use] #[must_use]
pub fn upgrade_vim(base_dirs: &BaseDirs, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> { pub fn upgrade_vim(base_dirs: &BaseDirs, run_type: RunType) -> Result<(), Error> {
if let Some(vim) = which("vim") { let vim = require("vim")?;
if let Some(vimrc) = vimrc(&base_dirs) { let vimrc = require_option(vimrc(&base_dirs))?;
if let Some(plugin_framework) = PluginFramework::detect(&vimrc) { let plugin_framework = require_option(PluginFramework::detect(&vimrc))?;
terminal.print_separator(&format!("Vim ({:?})", plugin_framework));
let success = upgrade(&vim, &vimrc, plugin_framework, dry_run).is_ok();
return Some(("vim", success));
}
}
}
None print_separator(&format!("Vim ({:?})", plugin_framework));
upgrade(&vim, &vimrc, plugin_framework, run_type)
} }
#[must_use] #[must_use]
pub fn upgrade_neovim(base_dirs: &BaseDirs, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> { pub fn upgrade_neovim(base_dirs: &BaseDirs, run_type: RunType) -> Result<(), Error> {
if let Some(nvim) = which("nvim") { let nvim = require("nvim")?;
if let Some(nvimrc) = nvimrc(&base_dirs) { let nvimrc = require_option(nvimrc(&base_dirs))?;
if let Some(plugin_framework) = PluginFramework::detect(&nvimrc) { let plugin_framework = require_option(PluginFramework::detect(&nvimrc))?;
terminal.print_separator(&format!("Neovim ({:?})", plugin_framework));
let success = upgrade(&nvim, &nvimrc, plugin_framework, dry_run).is_ok();
return Some(("Neovim", success));
}
}
}
None print_separator(&format!("Neovim ({:?})", plugin_framework));
upgrade(&nvim, &nvimrc, plugin_framework, run_type)
} }

View File

@@ -1,95 +1,140 @@
use console::Term; use console::{style, Term};
use lazy_static::lazy_static;
use std::cmp::{max, min}; use std::cmp::{max, min};
use std::io::{self, Write}; use std::io::{self, Write};
use term_size; use std::sync::Mutex;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
pub struct Terminal { lazy_static! {
width: Option<usize>, static ref TERMINAL: Mutex<Terminal> = Mutex::new(Terminal::new());
stdout: StandardStream, }
struct Terminal {
width: Option<u16>,
term: Term,
} }
impl Terminal { impl Terminal {
pub fn new() -> Self { fn new() -> Self {
let term = Term::stdout();
Self { Self {
width: term_size::dimensions().map(|(w, _)| w), width: term.size_checked().map(|(_, w)| w),
stdout: StandardStream::stdout(ColorChoice::Auto), term,
} }
} }
pub fn print_separator<P: AsRef<str>>(&mut self, message: P) { fn print_separator<P: AsRef<str>>(&mut self, message: P) {
let message = message.as_ref(); let message = message.as_ref();
match self.width { match self.width {
Some(width) => { Some(width) => {
let _ = self self.term
.stdout .write_fmt(format_args!(
.set_color(ColorSpec::new().set_fg(Some(Color::White)).set_bold(true)); "{}\n",
let _ = writeln!( style(format_args!(
&mut self.stdout, "\n―― {} {:―^border$}",
"\n―― {} {:―^border$}", message,
message, "",
"", border = max(
border = max(2, min(80, width as usize) - 3 - message.len()) 2,
); min(80, width as usize)
let _ = self.stdout.reset(); .checked_sub(3)
let _ = self.stdout.flush(); .and_then(|e| e.checked_sub(message.len()))
.unwrap_or(0)
)
))
.bold()
))
.ok();
} }
None => { None => {
let _ = writeln!(&mut self.stdout, "―― {} ――", message); self.term.write_fmt(format_args!("―― {} ――\n", message)).ok();
} }
} }
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn print_warning<P: AsRef<str>>(&mut self, message: P) { fn print_warning<P: AsRef<str>>(&mut self, message: P) {
let message = message.as_ref(); let message = message.as_ref();
self.term
let _ = self .write_fmt(format_args!("{}\n", style(message).yellow().bold()))
.stdout .ok();
.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true));
let _ = writeln!(&mut self.stdout, "{}", message);
let _ = self.stdout.reset();
let _ = self.stdout.flush();
} }
pub fn print_result<P: AsRef<str>>(&mut self, key: P, succeeded: bool) { fn print_result<P: AsRef<str>>(&mut self, key: P, succeeded: bool) {
let key = key.as_ref(); let key = key.as_ref();
let _ = write!(&mut self.stdout, "{}: ", key);
let _ = self.stdout.set_color( self.term
ColorSpec::new() .write_fmt(format_args!(
.set_fg(Some(if succeeded { Color::Green } else { Color::Red })) "{}: {}\n",
.set_bold(true), key,
); if succeeded {
style("OK").bold().green()
let _ = writeln!(&mut self.stdout, "{}", if succeeded { "OK" } else { "FAILED" }); } else {
style("FAILED").bold().red()
let _ = self.stdout.reset(); }
let _ = self.stdout.flush(); ))
.ok();
} }
pub fn should_retry(&mut self, running: bool) -> Result<bool, io::Error> { fn should_retry(&mut self, interrupted: bool) -> Result<bool, io::Error> {
if self.width.is_none() { if self.width.is_none() {
return Ok(false); return Ok(false);
} }
println!(); self.term
loop { .write_fmt(format_args!(
let _ = self "\n{}",
.stdout style(format!(
.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true)); "Retry? [y/N] {}",
let _ = write!(&mut self.stdout, "Retry? [y/N] "); if interrupted {
if !running { "(Press Ctrl+C again to stop Topgrade) "
write!(&mut self.stdout, "(Press Ctrl+C again to stop Topgrade) "); } else {
} ""
let _ = self.stdout.reset(); }
let _ = self.stdout.flush(); ))
.yellow()
.bold()
))
.ok();
match Term::stdout().read_char()? { let answer = loop {
'y' | 'Y' => return Ok(true), match self.term.read_char()? {
'n' | 'N' | '\r' | '\n' => return Ok(false), 'y' | 'Y' => break Ok(true),
'n' | 'N' | '\r' | '\n' => break Ok(false),
_ => (), _ => (),
} }
} };
self.term.write_str("\n").ok();
answer
} }
} }
impl Default for Terminal {
fn default() -> Self {
Self::new()
}
}
pub fn should_retry(interrupted: bool) -> Result<bool, io::Error> {
TERMINAL.lock().unwrap().should_retry(interrupted)
}
pub fn print_separator<P: AsRef<str>>(message: P) {
TERMINAL.lock().unwrap().print_separator(message)
}
#[allow(dead_code)]
pub fn print_warning<P: AsRef<str>>(message: P) {
TERMINAL.lock().unwrap().print_warning(message)
}
pub fn print_result<P: AsRef<str>>(key: P, succeeded: bool) {
TERMINAL.lock().unwrap().print_result(key, succeeded)
}
#[cfg(windows)]
/// Tells whether the terminal is dumb.
pub fn is_dumb() -> bool {
TERMINAL.lock().unwrap().width.is_none()
}

View File

@@ -1,69 +0,0 @@
use super::executor::Executor;
use super::terminal::Terminal;
use super::utils::{which, Check};
use directories::BaseDirs;
use failure::Error;
pub fn run_zplug(base_dirs: &BaseDirs, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(zsh) = which("zsh") {
if base_dirs.home_dir().join(".zplug").exists() {
terminal.print_separator("zplug");
let success = || -> Result<(), Error> {
Executor::new(zsh, dry_run)
.args(&["-c", "source ~/.zshrc && zplug update"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("zplug", success));
}
}
None
}
pub fn run_fisher(base_dirs: &BaseDirs, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(fish) = which("fish") {
if base_dirs.home_dir().join(".config/fish/functions/fisher.fish").exists() {
terminal.print_separator("fisher");
let success = || -> Result<(), Error> {
Executor::new(&fish, dry_run)
.args(&["-c", "fisher self-update"])
.spawn()?
.wait()?
.check()?;
Executor::new(&fish, dry_run)
.args(&["-c", "fisher"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("fisher", success));
}
}
None
}
#[must_use]
pub fn run_homebrew(terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(brew) = which("brew") {
terminal.print_separator("Brew");
let inner = || -> Result<(), Error> {
Executor::new(&brew, dry_run).arg("update").spawn()?.wait()?.check()?;
Executor::new(&brew, dry_run).arg("upgrade").spawn()?.wait()?.check()?;
Ok(())
};
return Some(("Brew", inner().is_ok()));
}
None
}

View File

@@ -1,13 +1,10 @@
use failure::Error; use super::error::{Error, ErrorKind};
use log::{debug, error};
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fmt::Debug; use std::fmt::{self, Debug};
use std::path::{Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::process::{ExitStatus, Output}; use std::process::{ExitStatus, Output};
use which as which_mod; use which_crate;
#[derive(Fail, Debug)]
#[fail(display = "Process failed")]
pub struct ProcessFailed;
pub trait Check { pub trait Check {
fn check(self) -> Result<(), Error>; fn check(self) -> Result<(), Error>;
@@ -18,7 +15,7 @@ impl Check for ExitStatus {
if self.success() { if self.success() {
Ok(()) Ok(())
} else { } else {
Err(Error::from(ProcessFailed {})) Err(ErrorKind::ProcessFailed(self))?
} }
} }
} }
@@ -35,6 +32,9 @@ where
{ {
fn if_exists(self) -> Option<Self>; fn if_exists(self) -> Option<Self>;
fn is_descendant_of(&self, ancestor: &Path) -> bool; fn is_descendant_of(&self, ancestor: &Path) -> bool;
/// Returns the path if it exists or ErrorKind::SkipStep otherwise
fn require(self) -> Result<Self, Error>;
} }
impl PathExt for PathBuf { impl PathExt for PathBuf {
@@ -49,17 +49,25 @@ impl PathExt for PathBuf {
fn is_descendant_of(&self, ancestor: &Path) -> bool { fn is_descendant_of(&self, ancestor: &Path) -> bool {
self.iter().zip(ancestor.iter()).all(|(a, b)| a == b) self.iter().zip(ancestor.iter()).all(|(a, b)| a == b)
} }
fn require(self) -> Result<Self, Error> {
if self.exists() {
Ok(self)
} else {
Err(ErrorKind::SkipStep)?
}
}
} }
pub fn which<T: AsRef<OsStr> + Debug>(binary_name: T) -> Option<PathBuf> { pub fn which<T: AsRef<OsStr> + Debug>(binary_name: T) -> Option<PathBuf> {
match which_mod::which(&binary_name) { match which_crate::which(&binary_name) {
Ok(path) => { Ok(path) => {
debug!("Detected {:?} as {:?}", &path, &binary_name); debug!("Detected {:?} as {:?}", &path, &binary_name);
Some(path) Some(path)
} }
Err(e) => { Err(e) => {
match e.kind() { match e.kind() {
which_mod::ErrorKind::CannotFindBinaryPath => { which_crate::ErrorKind::CannotFindBinaryPath => {
debug!("Cannot find {:?}", &binary_name); debug!("Cannot find {:?}", &binary_name);
} }
_ => { _ => {
@@ -71,3 +79,113 @@ pub fn which<T: AsRef<OsStr> + Debug>(binary_name: T) -> Option<PathBuf> {
} }
} }
} }
/// `std::fmt::Display` implementation for `std::path::Path`.
///
/// This struct differs from `std::path::Display` in that in Windows it takes care of printing backslashes
/// instead of slashes and don't print the `\\?` prefix in long paths.
pub struct HumanizedPath<'a> {
path: &'a Path,
}
impl<'a> From<&'a Path> for HumanizedPath<'a> {
fn from(path: &'a Path) -> Self {
Self { path }
}
}
impl<'a> fmt::Display for HumanizedPath<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if cfg!(windows) {
let mut iterator = self.path.components().peekable();
while let Some(component) = iterator.next() {
let mut print_seperator = iterator.peek().is_some();
match &component {
Component::Normal(c) if *c == "?" => {
print_seperator = false;
}
Component::RootDir | Component::CurDir => {
print_seperator = false;
}
Component::ParentDir => {
write!(f, "..")?;
}
Component::Prefix(p) => {
write!(f, "{}", p.as_os_str().to_string_lossy())?;
print_seperator = true;
}
Component::Normal(c) => {
write!(f, "{}", c.to_string_lossy())?;
}
};
if print_seperator {
write!(f, "{}", std::path::MAIN_SEPARATOR)?;
}
}
} else {
write!(f, "{}", self.path.display())?;
}
Ok(())
}
}
#[cfg(test)]
#[cfg(windows)]
mod tests {
use super::*;
fn humanize<P: AsRef<Path>>(path: P) -> String {
format!("{}", HumanizedPath::from(path.as_ref()))
}
#[test]
fn test_just_drive() {
assert_eq!("C:\\", humanize("C:\\"));
}
#[test]
fn test_path() {
assert_eq!("C:\\hi", humanize("C:\\hi"));
}
#[test]
fn test_unc() {
assert_eq!("\\\\server\\share\\", humanize("\\\\server\\share"));
}
#[test]
fn test_long_path() {
assert_eq!("C:\\hi", humanize("//?/C:/hi"));
}
}
pub fn require<T: AsRef<OsStr> + Debug>(binary_name: T) -> Result<PathBuf, Error> {
match which_crate::which(&binary_name) {
Ok(path) => {
debug!("Detected {:?} as {:?}", &path, &binary_name);
Ok(path)
}
Err(e) => match e.kind() {
which_crate::ErrorKind::CannotFindBinaryPath => {
debug!("Cannot find {:?}", &binary_name);
Err(ErrorKind::SkipStep)?
}
_ => {
panic!("Detecting {:?} failed: {}", &binary_name, e);
}
},
}
}
#[allow(dead_code)]
pub fn require_option<T>(option: Option<T>) -> Result<T, Error> {
if let Some(value) = option {
Ok(value)
} else {
Err(ErrorKind::SkipStep)?
}
}

View File

@@ -1,133 +0,0 @@
use super::executor::Executor;
use super::terminal::Terminal;
use super::utils::{self, which, Check};
use failure;
use std::path::PathBuf;
use std::process::Command;
#[must_use]
pub fn run_chocolatey(terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(choco) = utils::which("choco") {
terminal.print_separator("Chocolatey");
let success = || -> Result<(), failure::Error> {
Executor::new(&choco, dry_run)
.args(&["upgrade", "all"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("Chocolatey", success));
}
None
}
#[must_use]
pub fn run_scoop(terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(scoop) = utils::which("scoop") {
terminal.print_separator("Scoop");
let success = || -> Result<(), failure::Error> {
Executor::new(&scoop, dry_run)
.args(&["update"])
.spawn()?
.wait()?
.check()?;
Executor::new(&scoop, dry_run)
.args(&["update", "*"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("Scoop", success));
}
None
}
pub struct Powershell {
path: Option<PathBuf>,
}
impl Powershell {
pub fn new() -> Self {
Powershell {
path: which("powershell"),
}
}
pub fn has_command(powershell: &PathBuf, command: &str) -> bool {
|| -> Result<(), failure::Error> {
Command::new(&powershell)
.args(&["-Command", &format!("Get-Command {}", command)])
.output()?
.check()?;
Ok(())
}().is_ok()
}
pub fn profile(&self) -> Option<PathBuf> {
if let Some(powershell) = &self.path {
let result = || -> Result<PathBuf, failure::Error> {
let output = Command::new(powershell).args(&["-Command", "echo $profile"]).output()?;
output.status.check()?;
Ok(PathBuf::from(
String::from_utf8_lossy(&output.stdout).trim().to_string(),
))
}();
match result {
Err(e) => error!("Error getting Powershell profile: {}", e),
Ok(path) => return Some(path),
}
}
None
}
#[must_use]
pub fn update_modules(&self, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(powershell) = &self.path {
terminal.print_separator("Powershell Modules Update");
let success = || -> Result<(), failure::Error> {
Executor::new(&powershell, dry_run)
.arg("Update-Module")
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("Powershell Modules Update", success));
}
None
}
#[must_use]
pub fn windows_update(&self, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(powershell) = &self.path {
if Self::has_command(&powershell, "Install-WindowsUpdate") {
terminal.print_separator("Windows Update");
let success = || -> Result<(), failure::Error> {
Executor::new(&powershell, dry_run)
.args(&["-Command", "Install-WindowsUpdate -MicrosoftUpdate -AcceptAll -Verbose"])
.spawn()?
.wait()?
.check()?;
Ok(())
}().is_ok();
return Some(("Windows Update", success));
}
}
None
}
}