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=x86_64-unknown-linux-gnu
- env: TARGET=x86_64-unknown-linux-musl
- env: TARGET=x86_64-unknown-freebsd
# OSX
- env: TARGET=x86_64-apple-darwin
@@ -72,3 +73,10 @@ before_cache:
notifications:
email:
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]
name = "topgrade"
description = "Upgrade all the things"
license-file = "LICENCE"
license-file = "LICENSE"
repository = "https://github.com/r-darwish/topgrade"
version = "0.16.0"
version = "1.9.0"
authors = ["Roey Darwish Dror <roey.ghost@gmail.com>"]
exclude = ["doc/screenshot.gif"]
edition = "2018"
[dependencies]
directories = "1.0.2"
failure = "0.1.2"
failure_derive = "0.1.2"
serde = "1.0.79"
serde_derive = "1.0.79"
toml = "0.4.8"
which = "2.0.0"
failure = "0.1.5"
failure_derive = "0.1.5"
serde = { version = "1.0.88", features = ["derive"] }
toml = "0.4.10"
which_crate = { version = "2.0.1", package = "which" }
shellexpand = "1.0.0"
structopt = "0.2.10"
log = "0.4.5"
env_logger = "0.5.13"
term_size = "0.3.1"
termcolor = "1.0.4"
walkdir = "2.2.5"
console = "0.6.2"
structopt = "0.2.14"
log = "0.4.6"
env_logger = "0.6.0"
walkdir = "2.2.7"
console = "0.7.5"
self_update_crate = { version = "0.5.1", optional = true, package = "self_update" }
lazy_static = "1.2.0"
[target.'cfg(unix)'.dependencies]
nix = "0.11"
lazy_static = "1.1.0"
nix = "0.13.0"
[profile.release]
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
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
Arch Linux users can use the [AUR](https://aur.archlinux.org/packages/topgrade/) package.
macOS users can install topgrade via Homebrew.
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
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
Just run `topgrade`. It will run the following steps:
* *Linux*: Run the system package manager:
* *Arch*: Run [yay](https://github.com/Jguer/yay) or fall back to pacman
* *CentOS/RHEL*: Run `yum upgrade`
* *Fedora* - Run `dnf upgrade`
* *Debian/Ubuntu*: Run `apt update && apt dist-upgrade`
* *Linux*: Run [etc-update](https://dev.gentoo.org/~zmedico/portage/doc/man/etc-update.1.html):
* *Unix*: Run `brew update && brew upgrade`. This should handle both Homebrew and Linuxbrew
* *Windows*: Upgrade Powershell modules
* *Windows*: Upgrade all [Chocolatey](https://chocolatey.org/) packages
* Try to self-upgrade if compiled with this feature. On Unix systems Topgrade will also respawn
itself if it was upgraded
* **Linux**: Run the system package manager:
* **Arch**: Run [yay](https://github.com/Jguer/yay) or fall back to pacman
* **CentOS/RHEL**: Run `yum upgrade`
* **Fedora**: Run `dnf upgrade`
* **Debian/Ubuntu**: Run `apt update && apt dist-upgrade`
* **Gentoo**: Run `layman -s ALL && emerge --sync -q && eix-update && emerge -uDNa world`
* **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:
* ~/.emacs.d (Should work whether you use [Spacemacs](http://spacemacs.org/) or a custom configuration)
* ~/.zshrc
@@ -45,15 +71,21 @@ Just run `topgrade`. It will run the following steps:
* ~/.config/nvim
* ~/.vim
* ~/.config/openbox
* ~/.config/bspwm
* ~/.config/i3
* Powershell Profile
* Custom defined paths
* *Unix*: Run [zplug](https://github.com/zplug/zplug) update
* *Unix*: Run [fisher](https://github.com/jorgebucaran/fisher)
* *Unix*: Upgrade tmux plugins with [TPM](https://github.com/tmux-plugins/tpm)
* **Unix**: Run [zplug](https://github.com/zplug/zplug) update
* **Unix**: Run [fisher](https://github.com/jorgebucaran/fisher)
* **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.
* 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 [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:
* [NeoBundle](https://github.com/Shougo/neobundle.vim)
* [Vundle](https://github.com/VundleVim/Vundle.vim)
@@ -61,29 +93,36 @@ Just run `topgrade`. It will run the following steps:
* Node
* 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 `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
* Run `gem upgrade --user-install` if `~/.gem` exists
* *Linux*: Update Flatpak packages
* *Linux*: Update snap packages
* *Linux*: Run [fwupdmgr](https://github.com/hughsie/fwupd) to show firmware upgrade. (View
* **Linux**: Update Flatpak packages
* **Linux**: Update snap packages
* **Linux**: Run [fwupdmgr](https://github.com/hughsie/fwupd) to show firmware upgrade. (View
only. No upgrades will actually be performed)
* Run custom defined commands
* Final stage
* *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/))
* *macOS*: Upgrade App Store applications
* **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/))
* **macOS**: Upgrade App Store applications
* **FreeBSD**: Run `freebsd-upgrade`
## Flags
* `-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.
* `-c/--cleanup` - Topgrade will instruct package managers to remove old or unused files
* `-n/--dry-run` - Print what should be run.
* `--no-system` - Skip the system upgrade phase.
* `--no-git-repos` - Don't pull custom git repositories.
* `--no-emacs` - Don't upgrade Emacs packages or configuration files.
* `--disable [STEPS]` - Disable one or more steps:
* `system` - Skip the system upgrade phase.
* `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
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
@@ -91,6 +130,9 @@ git_repos = [
"~/dev/topgrade",
]
# Same options as the command line flag
disable = ["system", "emacs"]
[pre_commands]
"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
* `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.
### 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%
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- rustup component add rustfmt-preview clippy-preview
- cargo fmt --all -- --check
- cargo clippy --all-targets --all-features -- -D warnings
- rustc -Vv
- cargo -V
test_script:
- if [%APPVEYOR_REPO_TAG%]==[false] (
cargo check --target %TARGET% &&
cargo check --target %TARGET% --release
cargo fmt --all -- --check &&
cargo clippy --all-targets --all-features -- -D warnings &&
cargo clippy --all-targets -- -D warnings &&
cargo test
)
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
deploy:
@@ -55,3 +55,10 @@ notifications:
# Building is done in the test phase, so we disable Appveyor's build phase.
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
# 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
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
main() {
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cross check --target $TARGET
cross check --target $TARGET --release
cross clippy --all-targets -- -D warnings
cross clippy --all-targets --all-features -- -D warnings
cross check --target $TARGET --release --all-features
if [ ! -z $DISABLE_TESTS ]; then
cross test
return
fi

View File

@@ -1,27 +1,88 @@
use super::error::{Error, ErrorKind};
use directories::BaseDirs;
use failure;
use failure::ResultExt;
use lazy_static::lazy_static;
use serde::Deserialize;
use shellexpand;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
use std::fs;
use structopt::StructOpt;
use toml;
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)]
pub struct Config {
/// Configuration file
pub struct ConfigFile {
pre_commands: Option<Commands>,
commands: Option<Commands>,
git_repos: Option<Vec<String>>,
disable: Option<Vec<Step>>,
}
impl Config {
pub fn read(base_dirs: &BaseDirs) -> Result<Config, failure::Error> {
impl ConfigFile {
/// 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");
if !config_path.exists() {
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 {
for path in paths.iter_mut() {
@@ -31,41 +92,101 @@ impl Config {
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)]
#[structopt(name = "Topgrade")]
pub struct Opt {
#[structopt(short = "t", long = "tmux", help = "Run inside tmux")]
pub run_in_tmux: bool,
/// Command line arguments
pub struct CommandLineArgs {
/// Run inside tmux
#[structopt(short = "t", long = "tmux")]
run_in_tmux: bool,
#[structopt(long = "no-system", help = "Don't perform system upgrade")]
pub no_system: bool,
/// Cleanup temporary or old files
#[structopt(short = "c", long = "cleanup")]
cleanup: bool,
#[structopt(
long = "no-git-repos",
help = "Don't perform updates on configured git repos"
)]
pub no_git_repos: bool,
/// Print what would be done
#[structopt(short = "n", long = "dry-run")]
dry_run: bool,
#[structopt(
long = "no-emacs",
help = "Don't upgrade Emacs packages or configuration files"
)]
pub no_emacs: bool,
/// Do not ask to retry failed steps
#[structopt(long = "no-retry")]
no_retry: bool,
#[structopt(short = "n", long = "dry-run", help = "Print what would be done")]
pub dry_run: bool,
/// Do not perform upgrades for the given steps
#[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)]
mod unix;
#[cfg(unix)]

View File

@@ -1,22 +1,31 @@
//! SIGINT handling in Unix systems.
use lazy_static::lazy_static;
use nix::sys::signal;
use std::sync::atomic::{AtomicBool, Ordering};
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 {
RUNNING.load(Ordering::SeqCst)
/// Tells whether the program has been interrupted
pub fn interrupted() -> bool {
INTERRUPTED.load(Ordering::SeqCst)
}
pub fn set_running(value: bool) {
RUNNING.store(value, Ordering::SeqCst)
/// Clears the interrupted flag
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) {
set_running(false);
INTERRUPTED.store(true, Ordering::SeqCst)
}
/// Set the necessary signal handlers.
/// The function panics on failure.
pub fn set_handler() {
let sig_action = signal::SigAction::new(
signal::SigHandler::Handler(handle_sigint),

View File

@@ -1,7 +1,9 @@
pub fn running() -> bool {
true
//! A stub for Ctrl + C handling.
pub fn interrupted() -> bool {
false
}
pub fn set_running(_value: bool) {}
pub fn unset_interrupted() {}
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 failure;
use failure::ResultExt;
use std::ffi::{OsStr, OsString};
use std::io;
use std::path::Path;
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 {
Wet(Command),
Dry(DryCommand),
}
impl Executor {
pub fn new<S: AsRef<OsStr>>(program: S, dry: bool) -> Self {
if dry {
Executor::Dry(DryCommand {
program: program.as_ref().into(),
..Default::default()
})
} else {
Executor::Wet(Command::new(program))
}
}
/// See `std::process::Command::arg`
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Executor {
match self {
Executor::Wet(c) => {
@@ -35,6 +70,7 @@ impl Executor {
self
}
/// See `std::process::Command::args`
pub fn args<I, S>(&mut self, args: I) -> &mut Executor
where
I: IntoIterator<Item = S>,
@@ -52,6 +88,7 @@ impl Executor {
self
}
/// See `std::process::Command::current_dir`
pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Executor {
match self {
Executor::Wet(c) => {
@@ -63,9 +100,10 @@ impl Executor {
self
}
pub fn spawn(&mut self) -> Result<ExecutorChild, io::Error> {
match self {
Executor::Wet(c) => c.spawn().map(ExecutorChild::Wet),
/// See `std::process::Command::spawn`
pub fn spawn(&mut self) -> Result<ExecutorChild, Error> {
let result = match self {
Executor::Wet(c) => c.spawn().context(ErrorKind::ProcessExecution).map(ExecutorChild::Wet)?,
Executor::Dry(c) => {
print!(
"Dry running: {} {}",
@@ -80,12 +118,22 @@ impl Executor {
Some(dir) => println!(" in {}", dir.to_string_lossy()),
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)]
pub struct DryCommand {
program: OsString,
@@ -93,30 +141,55 @@ pub struct DryCommand {
directory: Option<OsString>,
}
/// The Result of spawn. Contains an actual `std::process::Child` if executed by a wet command.
pub enum ExecutorChild {
Wet(Child),
Dry,
}
impl ExecutorChild {
pub fn wait(&mut self) -> Result<ExecutorExitStatus, io::Error> {
match self {
ExecutorChild::Wet(c) => c.wait().map(ExecutorExitStatus::Wet),
ExecutorChild::Dry => Ok(ExecutorExitStatus::Dry),
}
/// See `std::process::Child::wait`
pub fn wait(&mut self) -> Result<ExecutorExitStatus, Error> {
let result = match self {
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 {
Wet(ExitStatus),
Dry,
}
impl Check for ExecutorExitStatus {
fn check(self) -> Result<(), failure::Error> {
fn check(self) -> Result<(), Error> {
match self {
ExecutorExitStatus::Wet(e) => e.check(),
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 ctrlc;
mod error;
mod executor;
mod generic;
mod git;
mod node;
mod report;
#[cfg(feature = "self-update")]
mod self_update;
mod steps;
mod terminal;
mod utils;
mod vim;
use self::config::Config;
use self::git::{Git, Repositories};
use self::config::{Config, Step};
use self::error::{Error, ErrorKind};
use self::report::Report;
use self::terminal::Terminal;
use failure::Error;
use self::steps::*;
use self::terminal::*;
use failure::{Fail, ResultExt};
use log::debug;
use std::borrow::Cow;
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 structopt::StructOpt;
#[derive(Fail, Debug)]
#[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>
fn execute<'a, F, M>(report: &mut Report<'a>, key: M, func: F, no_retry: bool) -> Result<(), Error>
where
M: Into<Cow<'a, str>>,
F: Fn(&mut Terminal) -> Option<(M, bool)>,
F: Fn() -> Result<(), Error>,
M: Into<Cow<'a, str>> + Debug,
{
while let Some((key, success)) = func(&mut execution_context.terminal) {
if success {
return Ok(Some((key, success)));
}
debug!("Executing {:?}", key);
let running = ctrlc::running();
if !running {
ctrlc::set_running(true);
}
let should_retry = execution_context.terminal.should_retry(running).map_err(|e| {
if e.kind() == ErrorKind::Interrupted {
Error::from(Interrupted)
} else {
Error::from(e)
loop {
match func() {
Ok(()) => {
report.push_result(Some((key, true)));
break;
}
})?;
Err(ref e) if e.kind() == ErrorKind::SkipStep => {
break;
}
Err(_) => {
let interrupted = ctrlc::interrupted();
if interrupted {
ctrlc::unset_interrupted();
}
if !should_retry {
return Ok(Some((key, success)));
let should_ask = interrupted || !no_retry;
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> {
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)]
{
tmux::run_in_tmux();
@@ -116,23 +74,31 @@ fn run() -> Result<(), Error> {
}
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 {
terminal: Terminal::new(),
};
let git = git::Git::new();
let mut git_repos = git::Repositories::new(&git);
let config = Config::read(&base_dirs)?;
let mut report = Report::new();
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "freebsd", target_os = "linux"))]
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() {
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();
#[cfg(windows)]
report.push_result(execute(
|terminal| powershell.update_modules(terminal, opt.dry_run),
&mut execution_context,
)?);
let should_run_powershell = powershell.profile().is_some() && config.should_run(Step::Powershell);
#[cfg(target_os = "linux")]
let distribution = linux::Distribution::detect();
#[cfg(target_os = "linux")]
{
if !opt.no_system {
if config.should_run(Step::System) {
match &distribution {
Ok(distribution) => {
report.push_result(execute(
|terminal| distribution.upgrade(&sudo, terminal, opt.dry_run),
&mut execution_context,
)?);
execute(
&mut report,
"System update",
|| distribution.upgrade(&sudo, config.cleanup(), run_type),
config.no_retry(),
)?;
}
Err(e) => {
println!("Error detecting current distribution: {}", e);
}
}
report.push_result(execute(
|terminal| linux::run_etc_update(&sudo, terminal, opt.dry_run),
&mut execution_context,
)?);
execute(
&mut report,
"etc-update",
|| linux::run_etc_update(sudo.as_ref(), run_type),
config.no_retry(),
)?;
}
}
#[cfg(windows)]
report.push_result(execute(
|terminal| windows::run_chocolatey(terminal, opt.dry_run),
&mut execution_context,
)?);
execute(
&mut report,
"Chocolatey",
|| windows::run_chocolatey(run_type),
config.no_retry(),
)?;
#[cfg(windows)]
report.push_result(execute(
|terminal| windows::run_scoop(terminal, opt.dry_run),
&mut execution_context,
)?);
execute(&mut report, "Scoop", || windows::run_scoop(run_type), config.no_retry())?;
#[cfg(unix)]
report.push_result(execute(
|terminal| unix::run_homebrew(terminal, opt.dry_run),
&mut execution_context,
)?);
execute(
&mut report,
"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"));
#[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"));
git_repos.insert(base_dirs.home_dir().join(".config/nvim"));
if config.should_run(Step::Vim) {
git_repos.insert(base_dirs.home_dir().join(".vim"));
git_repos.insert(base_dirs.home_dir().join(".config/nvim"));
}
#[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(".config/fish"));
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)]
@@ -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() {
for git_repo in custom_git_repos {
git_repos.insert(git_repo);
@@ -218,155 +208,232 @@ fn run() -> Result<(), Error> {
}
}
for repo in git_repos.repositories() {
report.push_result(execute(
|terminal| git.pull(&repo, terminal, opt.dry_run),
&mut execution_context,
)?);
execute(
&mut report,
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)]
{
report.push_result(execute(
|terminal| unix::run_zplug(&base_dirs, terminal, opt.dry_run),
&mut execution_context,
)?);
report.push_result(execute(
|terminal| unix::run_fisher(&base_dirs, terminal, opt.dry_run),
&mut execution_context,
)?);
report.push_result(execute(
|terminal| tmux::run_tpm(&base_dirs, terminal, opt.dry_run),
&mut execution_context,
)?);
execute(
&mut report,
"zplug",
|| unix::run_zplug(&base_dirs, run_type),
config.no_retry(),
)?;
execute(
&mut report,
"fisher",
|| unix::run_fisher(&base_dirs, run_type),
config.no_retry(),
)?;
execute(
&mut report,
"tmux",
|| tmux::run_tpm(&base_dirs, run_type),
config.no_retry(),
)?;
}
report.push_result(execute(
|terminal| generic::run_rustup(&base_dirs, terminal, opt.dry_run),
&mut execution_context,
)?);
report.push_result(execute(
|terminal| generic::run_cargo_update(&base_dirs, terminal, opt.dry_run),
&mut execution_context,
)?);
execute(
&mut report,
"rustup",
|| generic::run_rustup(&base_dirs, run_type),
config.no_retry(),
)?;
execute(
&mut report,
"cargo",
|| generic::run_cargo_update(run_type),
config.no_retry(),
)?;
if !opt.no_emacs {
report.push_result(execute(
|terminal| generic::run_emacs(&base_dirs, terminal, opt.dry_run),
&mut execution_context,
)?);
if config.should_run(Step::Emacs) {
execute(
&mut report,
"Emacs",
|| generic::run_emacs(&base_dirs, run_type),
config.no_retry(),
)?;
}
report.push_result(execute(
|terminal| generic::run_opam_update(terminal, opt.dry_run),
&mut execution_context,
)?);
report.push_result(execute(
|terminal| vim::upgrade_vim(&base_dirs, terminal, opt.dry_run),
&mut execution_context,
)?);
report.push_result(execute(
|terminal| vim::upgrade_neovim(&base_dirs, terminal, opt.dry_run),
&mut execution_context,
)?);
report.push_result(execute(
|terminal| node::run_npm_upgrade(&base_dirs, terminal, opt.dry_run),
&mut execution_context,
)?);
report.push_result(execute(
|terminal| generic::run_composer_update(&base_dirs, terminal, opt.dry_run),
&mut execution_context,
)?);
report.push_result(execute(
|terminal| node::yarn_global_update(terminal, opt.dry_run),
&mut execution_context,
)?);
execute(
&mut report,
"opam",
|| generic::run_opam_update(run_type),
config.no_retry(),
)?;
execute(
&mut report,
"vcpkg",
|| generic::run_vcpkg_update(run_type),
config.no_retry(),
)?;
execute(
&mut report,
"pipx",
|| generic::run_pipx_update(run_type),
config.no_retry(),
)?;
#[cfg(unix)]
execute(&mut report, "pearl", || unix::run_pearl(run_type), config.no_retry())?;
execute(
&mut report,
"jetpak",
|| generic::run_jetpack(run_type),
config.no_retry(),
)?;
#[cfg(
not(
any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)
)
)]
report.push_result(execute(
|terminal| generic::run_apm(terminal, opt.dry_run),
&mut execution_context,
)?);
report.push_result(execute(
|terminal| generic::run_gem(&base_dirs, terminal, opt.dry_run),
&mut execution_context,
)?);
if config.should_run(Step::Vim) {
execute(
&mut report,
"vim",
|| vim::upgrade_vim(&base_dirs, run_type),
config.no_retry(),
)?;
execute(
&mut report,
"Neovim",
|| vim::upgrade_neovim(&base_dirs, run_type),
config.no_retry(),
)?;
}
execute(
&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")]
{
report.push_result(execute(
|terminal| linux::flatpak_user_update(terminal, opt.dry_run),
&mut execution_context,
)?);
report.push_result(execute(
|terminal| linux::flatpak_global_update(&sudo, terminal, opt.dry_run),
&mut execution_context,
)?);
report.push_result(execute(
|terminal| linux::run_snap(&sudo, terminal, opt.dry_run),
&mut execution_context,
)?);
execute(
&mut report,
"Flatpak",
|| linux::flatpak_update(run_type),
config.no_retry(),
)?;
execute(
&mut report,
"snap",
|| linux::run_snap(sudo.as_ref(), run_type),
config.no_retry(),
)?;
}
if let Some(commands) = config.commands() {
for (name, command) in commands {
report.push_result(execute(
|terminal| {
Some((
name,
generic::run_custom_command(&name, &command, terminal, opt.dry_run).is_ok(),
))
},
&mut execution_context,
)?);
execute(
&mut report,
name,
|| generic::run_custom_command(&name, &command, run_type),
config.no_retry(),
)?;
}
}
#[cfg(target_os = "linux")]
{
report.push_result(execute(
|terminal| linux::run_fwupdmgr(terminal, opt.dry_run),
&mut execution_context,
)?);
report.push_result(execute(
|terminal| linux::run_needrestart(&sudo, terminal, opt.dry_run),
&mut execution_context,
)?);
execute(
&mut report,
"Firmware upgrades",
|| linux::run_fwupdmgr(run_type),
config.no_retry(),
)?;
execute(
&mut report,
"Restarts",
|| linux::run_needrestart(sudo.as_ref(), run_type),
config.no_retry(),
)?;
}
#[cfg(target_os = "macos")]
{
if !opt.no_system {
report.push_result(execute(
|terminal| macos::upgrade_macos(terminal, opt.dry_run),
&mut execution_context,
)?);
if config.should_run(Step::System) {
execute(
&mut report,
"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)]
{
if !opt.no_system {
report.push_result(execute(
|terminal| powershell.windows_update(terminal, opt.dry_run),
&mut execution_context,
)?);
if config.should_run(Step::System) {
execute(
&mut report,
"Windows update",
|| powershell.windows_update(run_type),
config.no_retry(),
)?;
}
}
if !report.data().is_empty() {
execution_context.terminal.print_separator("Summary");
print_separator("Summary");
for (key, succeeded) in report.data() {
execution_context.terminal.print_result(key, *succeeded);
print_result(key, *succeeded);
}
#[cfg(target_os = "linux")]
@@ -375,12 +442,15 @@ fn run() -> Result<(), Error> {
distribution.show_summary();
}
}
#[cfg(target_os = "freebsd")]
freebsd::audit_packages(&sudo).ok();
}
if report.data().iter().all(|(_, succeeded)| *succeeded) {
Ok(())
} else {
Err(StepFailed.into())
Err(ErrorKind::StepFailed)?
}
}
@@ -390,13 +460,21 @@ fn main() {
exit(0);
}
Err(error) => {
match error
.downcast::<StepFailed>()
.map(|_| ())
.or_else(|error| error.downcast::<Interrupted>().map(|_| ()))
{
Ok(_) => (),
Err(error) => println!("ERROR: {}", error),
let should_print = match error.kind() {
ErrorKind::StepFailed => false,
ErrorKind::Retry => error
.cause()
.and_then(|cause| cause.downcast_ref::<io::Error>())
.filter(|io_error| io_error.kind() == io::ErrorKind::Interrupted)
.is_none(),
_ => true,
};
if should_print {
println!("Error: {}", error);
if let Some(cause) = error.cause() {
println!("Caused by: {}", cause);
}
}
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 super::terminal::Terminal;
use super::utils::{which, Check};
use failure::Error;
use crate::error::Error;
use crate::executor::{CommandExt, RunType};
use crate::terminal::print_separator;
use crate::utils::{which, HumanizedPath};
use log::{debug, error};
use std::collections::HashSet;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug)]
pub struct Git {
git: Option<PathBuf>,
}
#[derive(Debug)]
pub struct Repositories<'a> {
git: &'a Git,
repositories: HashSet<String>,
@@ -37,15 +40,10 @@ impl Git {
let output = Command::new(&git)
.args(&["rev-parse", "--show-toplevel"])
.current_dir(path)
.output();
if let Ok(output) = output {
if !output.status.success() {
return None;
}
return Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
.check_output()
.ok()
.map(|output| output.trim().to_string());
return output;
}
}
Err(e) => match e.kind() {
@@ -57,32 +55,18 @@ impl Git {
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();
terminal.print_separator(format!("Pulling {}", path.display()));
print_separator(format!("Pulling {}", HumanizedPath::from(path)));
let git = self.git.as_ref().unwrap();
let success = || -> Result<(), Error> {
Executor::new(git, dry_run)
.args(&["pull", "--rebase", "--autostash"])
.current_dir(&path)
.spawn()?
.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))
run_type
.execute(git)
.args(&["pull", "--rebase", "--autostash"])
.current_dir(&path)
.check_run()
}
}

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

View File

@@ -1,8 +1,8 @@
use super::executor::Executor;
use super::terminal::Terminal;
use super::utils::{which, Check, PathExt};
use crate::error::Error;
use crate::executor::RunType;
use crate::terminal::print_separator;
use crate::utils::{require, require_option, PathExt};
use directories::BaseDirs;
use failure;
use std::fs;
use std::path::PathBuf;
@@ -54,13 +54,9 @@ fn nvimrc(base_dirs: &BaseDirs) -> Option<PathBuf> {
}
#[must_use]
fn upgrade(
vim: &PathBuf,
vimrc: &PathBuf,
plugin_framework: PluginFramework,
dry_run: bool,
) -> Result<(), failure::Error> {
Executor::new(&vim, dry_run)
fn upgrade(vim: &PathBuf, vimrc: &PathBuf, plugin_framework: PluginFramework, run_type: RunType) -> Result<(), Error> {
run_type
.execute(&vim)
.args(&[
"-N",
"-u",
@@ -72,9 +68,8 @@ fn upgrade(
"-e",
"-s",
"-V1",
]).spawn()?
.wait()?
.check()?;
])
.check_run()?;
println!();
@@ -82,31 +77,21 @@ fn upgrade(
}
#[must_use]
pub fn upgrade_vim(base_dirs: &BaseDirs, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(vim) = which("vim") {
if let Some(vimrc) = vimrc(&base_dirs) {
if let Some(plugin_framework) = 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));
}
}
}
pub fn upgrade_vim(base_dirs: &BaseDirs, run_type: RunType) -> Result<(), Error> {
let vim = require("vim")?;
let vimrc = require_option(vimrc(&base_dirs))?;
let plugin_framework = require_option(PluginFramework::detect(&vimrc))?;
None
print_separator(&format!("Vim ({:?})", plugin_framework));
upgrade(&vim, &vimrc, plugin_framework, run_type)
}
#[must_use]
pub fn upgrade_neovim(base_dirs: &BaseDirs, terminal: &mut Terminal, dry_run: bool) -> Option<(&'static str, bool)> {
if let Some(nvim) = which("nvim") {
if let Some(nvimrc) = nvimrc(&base_dirs) {
if let Some(plugin_framework) = 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));
}
}
}
pub fn upgrade_neovim(base_dirs: &BaseDirs, run_type: RunType) -> Result<(), Error> {
let nvim = require("nvim")?;
let nvimrc = require_option(nvimrc(&base_dirs))?;
let plugin_framework = require_option(PluginFramework::detect(&nvimrc))?;
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::io::{self, Write};
use term_size;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
use std::sync::Mutex;
pub struct Terminal {
width: Option<usize>,
stdout: StandardStream,
lazy_static! {
static ref TERMINAL: Mutex<Terminal> = Mutex::new(Terminal::new());
}
struct Terminal {
width: Option<u16>,
term: Term,
}
impl Terminal {
pub fn new() -> Self {
fn new() -> Self {
let term = Term::stdout();
Self {
width: term_size::dimensions().map(|(w, _)| w),
stdout: StandardStream::stdout(ColorChoice::Auto),
width: term.size_checked().map(|(_, w)| w),
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();
match self.width {
Some(width) => {
let _ = self
.stdout
.set_color(ColorSpec::new().set_fg(Some(Color::White)).set_bold(true));
let _ = writeln!(
&mut self.stdout,
"\n―― {} {:―^border$}",
message,
"",
border = max(2, min(80, width as usize) - 3 - message.len())
);
let _ = self.stdout.reset();
let _ = self.stdout.flush();
self.term
.write_fmt(format_args!(
"{}\n",
style(format_args!(
"\n―― {} {:―^border$}",
message,
"",
border = max(
2,
min(80, width as usize)
.checked_sub(3)
.and_then(|e| e.checked_sub(message.len()))
.unwrap_or(0)
)
))
.bold()
))
.ok();
}
None => {
let _ = writeln!(&mut self.stdout, "―― {} ――", message);
self.term.write_fmt(format_args!("―― {} ――\n", message)).ok();
}
}
}
#[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 _ = self
.stdout
.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();
self.term
.write_fmt(format_args!("{}\n", style(message).yellow().bold()))
.ok();
}
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 _ = write!(&mut self.stdout, "{}: ", key);
let _ = self.stdout.set_color(
ColorSpec::new()
.set_fg(Some(if succeeded { Color::Green } else { Color::Red }))
.set_bold(true),
);
let _ = writeln!(&mut self.stdout, "{}", if succeeded { "OK" } else { "FAILED" });
let _ = self.stdout.reset();
let _ = self.stdout.flush();
self.term
.write_fmt(format_args!(
"{}: {}\n",
key,
if succeeded {
style("OK").bold().green()
} else {
style("FAILED").bold().red()
}
))
.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() {
return Ok(false);
}
println!();
loop {
let _ = self
.stdout
.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true));
let _ = write!(&mut self.stdout, "Retry? [y/N] ");
if !running {
write!(&mut self.stdout, "(Press Ctrl+C again to stop Topgrade) ");
}
let _ = self.stdout.reset();
let _ = self.stdout.flush();
self.term
.write_fmt(format_args!(
"\n{}",
style(format!(
"Retry? [y/N] {}",
if interrupted {
"(Press Ctrl+C again to stop Topgrade) "
} else {
""
}
))
.yellow()
.bold()
))
.ok();
match Term::stdout().read_char()? {
'y' | 'Y' => return Ok(true),
'n' | 'N' | '\r' | '\n' => return Ok(false),
let answer = loop {
match self.term.read_char()? {
'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::fmt::Debug;
use std::path::{Path, PathBuf};
use std::fmt::{self, Debug};
use std::path::{Component, Path, PathBuf};
use std::process::{ExitStatus, Output};
use which as which_mod;
#[derive(Fail, Debug)]
#[fail(display = "Process failed")]
pub struct ProcessFailed;
use which_crate;
pub trait Check {
fn check(self) -> Result<(), Error>;
@@ -18,7 +15,7 @@ impl Check for ExitStatus {
if self.success() {
Ok(())
} else {
Err(Error::from(ProcessFailed {}))
Err(ErrorKind::ProcessFailed(self))?
}
}
}
@@ -35,6 +32,9 @@ where
{
fn if_exists(self) -> Option<Self>;
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 {
@@ -49,17 +49,25 @@ impl PathExt for PathBuf {
fn is_descendant_of(&self, ancestor: &Path) -> bool {
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> {
match which_mod::which(&binary_name) {
match which_crate::which(&binary_name) {
Ok(path) => {
debug!("Detected {:?} as {:?}", &path, &binary_name);
Some(path)
}
Err(e) => {
match e.kind() {
which_mod::ErrorKind::CannotFindBinaryPath => {
which_crate::ErrorKind::CannotFindBinaryPath => {
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
}
}