Compare commits

...

76 Commits

Author SHA1 Message Date
Thomas Schönauer
80a95cb404 Clippy (#331) 2023-01-29 19:31:37 +00:00
Thomas Schönauer
ab630cfbc6 v10.2.5 release (#330)
* Don't show desktop notification on error (if `skip_notify = true`) (#275)

* Use ─ (U+2500) to draw borders (#282)

* Adds Pclinuxos support (#283)

* Add Devkitpro Pacman support (#291)

* Added support for Neovim package manager lazy.nvim (#293)

* Added support for lazy.nvim

From https://github.com/folke/lazy.nvim
Authored-by: Jacob Lane Ledbetter <jledbetter460@gmail.com>

* Make garuda-update update AUR packages by default (#296)

* fix(#298): Don't throw error if no Helm repository found (#305)

* Skip .NET when `dotnet tool list` is not successful (#302)

* feat(pacstall): add `-y` flag variant (#312)

* Add openSUSE MicroOS support (#315)

* Adds notify-send timeout of 10s (#318)

* Don't run yum when rpm-ostree is available (#313)

* don't run yum when rpm-ostree is available

* Clippy fix

* rpm-ostree: set default value to true

* Fixes if loop error

* Fixes gem update --system requires sudo now (#317)

* Fixes gem update --system requires sudo now

* rubygem: Adds arg -EH to sudo

* Use fixed nala path instead of which(nala) (#314)

* Adds notify-send bug warning when topgrade is run (#324)

* Adds notify-send bug warning when topgrade is run

* fix typo + clippy

* notify-send warning respects skip_notify flag

* nix: Adds additional arguments support (#325)

* Adds pip-review and pipupgrade support (#316)

* Adds pip-review and pipupgrade support

* Python: fixes pip_review and pipupgrade

* v10.2.5 patch (#329)

* WSL: Adds new wsl --update flags (#327)

* wsl: Updates available flags

* Clippy fix

* Add WslUpdate runner

* wsl: Code Typo

* wsl: Code Typos

* wsl: Code Typos

* wsl: Code Typo

* Adds AM Package Manager (#328)

* Adds AM Package Manager

* Clippy fixes

* Cargo fmt

* Moves am to linux only in main file

---------

Co-authored-by: Guilherme Silva <626206+guihkx@users.noreply.github.com>
Co-authored-by: Gabriel Augendre <gabriel@augendre.info>
Co-authored-by: Cat Core <34719527+arthurbambou@users.noreply.github.com>
Co-authored-by: Hugo Haas <hugoh@hugoh.net>
Co-authored-by: Baptiste <32563450+BapRx@users.noreply.github.com>
Co-authored-by: bbx0 <39773919+bbx0@users.noreply.github.com>
Co-authored-by: Sourajyoti Basak <wiz28@protonmail.com>
2023-01-29 19:19:27 +00:00
edi
c13e14080c Add Lazy, a Neovim plugin manager (#326)
* fix upgrade order of (n)vim plugins

* treesitter should use the synchronous cmd

* add lazy pkg manager for neovim

* fix lazy cmd

---------

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2023-01-29 18:49:56 +00:00
edi
4abbee99cc fix upgrade order of (n)vim plugins (#322)
* fix upgrade order of (n)vim plugins

* treesitter should use the synchronous cmd

---------

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2023-01-27 21:41:48 +00:00
Giovanni Merlino
45d935eda3 Fix missing separator for Pkgin (pkgsrc) (#307)
* Fix missing separator for Pkgin (pkgsrc)

* Fix whitespace (giving issues with cargo fmt?)

---------

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2023-01-27 21:34:27 +00:00
pwygab
b4c5efde50 Fix small typo in warning when notify-send fails (#319)
Fix small typo
2023-01-27 20:52:20 +00:00
Roey Darwish Dror
cba9dc1c2c Fix windows build (#303) 2023-01-09 08:50:21 +01:00
dependabot[bot]
938647123c Bump tokio from 1.8.5 to 1.18.4 (#301) 2023-01-06 23:13:01 +00:00
Jacob Lane Ledbetter
9f24f6474e Add dnf config to config example (#292) 2022-12-30 22:52:04 +00:00
Thomas Schönauer
814e39644c fixes dotnet update + version bump 10.2.4 (#274) 2022-12-18 15:37:10 +00:00
Thomas Schönauer
eb51be0732 10.2.3 release (#272) 2022-12-18 14:20:00 +00:00
Thomas Schönauer
56a717dcc6 Fixes dotnet if dotnet tool is not available (#229)
* Fixes dotnet if dotnet tool si not available

* dotnet: multi-lang support

* Fixes dotnet for non-english terminals
2022-12-18 14:12:36 +00:00
Thomas Schönauer
ee353ccb66 adds fish_update_completions to fish step (#270) 2022-12-18 14:11:36 +00:00
Thomas Schönauer
33cea0e5b6 10.2.3 release (#271)
10.2.3
2022-12-18 14:07:31 +00:00
Thomas Schönauer
78a491a976 Fixes rubygems on .deb distros (#268)
Fixes rubygem on systems with the .deb ruby package
2022-12-15 20:27:27 +00:00
Thomas Schönauer
9553be04e4 Fixes vcpkg when installed as root (#266) 2022-12-15 11:39:25 +00:00
Thomas Schönauer
81928f55a2 Fix notify not available (#257) 2022-12-15 11:34:18 +00:00
Mark Nefedov
4e56bf07f3 Add options to disable Yarn and Pnpm individually. (#260)
* Add options to disable Yarn and Pnpm individualy.

* cargo fmt

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-12-14 21:42:48 +00:00
sandal
9f424f03c3 Added a step for fetching and building treesitter grammars for helix (#263) 2022-12-14 08:12:39 +00:00
Thomas Kosiewski
1e14b3bf28 Add helm (#255) (#256)
* Add `helm` (#255)

Signed-off-by: Thomas Kosiewski <thoma471@googlemail.com>

* Sorted steps alphabetically

Signed-off-by: Thomas Kosiewski <thoma471@googlemail.com>

Signed-off-by: Thomas Kosiewski <thoma471@googlemail.com>
2022-12-08 21:47:57 +00:00
Thomas Schönauer
51e7a31f48 10.2.2 (#253) 2022-12-06 19:48:46 +00:00
Thomas Schönauer
9847fd9d4d Merge branch 'master' into dev 2022-12-06 19:23:45 +00:00
Thomas Schönauer
24ef44291f 10.2.2 version bump (#252) 2022-12-06 19:22:06 +00:00
Julien ITARD
ebb0c5a6d8 Fix RubyGems step (#251)
* Bump tokio from 1.5.1 to 1.8.4 (#245)

Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.5.1 to 1.8.4.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.5.1...tokio-1.8.4)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix RubyGems step

* Fix style

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-06 19:13:07 +00:00
Armel Soro
1dee462175 Fix misleading log message when detecting rbenv (#249) 2022-12-05 17:42:26 +00:00
Armel Soro
dc82b8b766 Fix issue with RubyGems update command (#248) 2022-12-05 13:16:03 +00:00
dependabot[bot]
73888e7669 Bump tokio from 1.5.1 to 1.8.4 (#245)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.5.1 to 1.8.4.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.5.1...tokio-1.8.4)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-30 21:56:52 +00:00
dependabot[bot]
46a010cc8f Bump tokio from 1.5.1 to 1.8.4 (#147)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.5.1 to 1.8.4.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.5.1...tokio-1.8.4)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-11-30 21:29:39 +00:00
Thomas Schönauer
96e4de4594 10.2.1 release (#244) 2022-11-30 21:08:15 +00:00
Thomas Schönauer
70616b2ec5 Version bump (#237) 2022-11-29 15:17:20 +00:00
Thomas Schönauer
22ab07d88e Fixes paru/yay -Pw return code error (#228) 2022-11-29 06:49:26 +00:00
Guilherme Silva
70045fca29 CI: Force color support for Rustfmt (#230)
Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-11-27 00:21:29 +01:00
Marcin Puc
f6ec6c76db Add shell completion and manpage generation (#233) 2022-11-26 19:42:35 +00:00
Jason Stelzer
37b900c56a Add garuda-update (#227) 2022-11-26 19:38:18 +00:00
Guilherme Silva
e26ec4d9e0 Fix clippy warning for DragonFly BSD (#231) 2022-11-26 17:52:14 +00:00
Rebecca Turner
b31778bdd8 Add Sudo type (#221)
* Create `Sudo` type and `SudoKind` enum

* Fix build

* reformat

* Fix choco on windows

* Fix linux

* Fix linux more

* more fix stuff hehe hoho hahaha

* more fix stuff hehe hoho hahaha

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-11-25 22:19:32 +00:00
Thomas Schönauer
526c4c9a58 Fixes typo 2022-11-25 18:44:36 +00:00
Julien ITARD
3c1dda0c39 Add RubyGems update (#217) 2022-11-24 19:21:03 +00:00
Ruben Molina
25c5057171 Add support for juliaup (#208)
* Add support for juliaup

* Update config.rs

* Change the position for Juliaup Step.

* Update generic.rs
2022-11-24 19:17:58 +00:00
Rebecca Turner
e456155562 Add pre_sudo option (#219)
* Add `pre_sudo` option
2022-11-24 19:15:43 +00:00
Guilherme Silva
f2c7e4848e CI: Install only necessary components (#218)
* CI: Install only necessary components
2022-11-24 19:15:13 +00:00
Guilherme Silva
b4407963ad Fix compilation on DragonFly BSD (#210) 2022-11-23 15:24:58 +00:00
Thomas Schönauer
85a4691229 10.2.0 dependency + version bump (#209) 2022-11-23 15:24:58 +00:00
Rebecca Turner
8e9c3cc56a Add gup (#203) 2022-11-23 15:24:58 +00:00
Thomas Schönauer
7cdaefe3b0 --yes flag support on freebsd (#198)
* Adds freebsd yes flag support

* Fixes freebsd update functions

* Missing semicolon

* Adds Step dependency to freebsd.rs

* Change freebsd to status_checked
2022-11-23 15:24:58 +00:00
Thomas Schönauer
aedb25cde6 Npm rework (#199) 2022-11-23 15:24:58 +00:00
Rebecca Turner
582bc737cb Fix a bug with status_checked_with_codes (#202) 2022-11-23 15:24:58 +00:00
Thomas Schönauer
c4091584c3 Fixes NPM failure under sudo (#197)
Fixes npm under sudo
2022-11-23 15:24:58 +00:00
Guilherme Silva
22ed1ef50a CI: Add NetBSD target (#180)
* Remove the `sys-info` crate

It offers much more features than we currently use.

Additionally, it was preventing me to cross-compile for NetBSD.

Since we were just using the `hostname()` function from the crate,
I went ahead and stole it.

* Add NetBSD target

* Fix FreeBSD clippy warnings
2022-11-23 15:24:58 +00:00
Rebecca Turner
41e2321b93 Use tracing (#174) 2022-11-23 15:24:58 +00:00
Rebecca Turner
d8add139e1 Add Treesitter support to Vim upgrade (#184)
* Add `:TSUpdate` to Vim upgrade

* Reformat `freebsd.rs`

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-11-23 15:24:58 +00:00
Thomas Schönauer
04a8d960a9 Revert " Fix unattended package upgrades on FreeBSD" (#188)
Revert " Fix unattended package upgrades on FreeBSD (#181)"

This reverts commit a0ad83a58b.
2022-11-23 15:24:58 +00:00
Yonas Yanfa
2c6a8f73fa Fix unattended package upgrades on FreeBSD (#181)
* Fix unattended package upgrades on FreeBSD

* Fix unattended package upgrades on FreeBSD

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-11-23 15:24:58 +00:00
Rebecca Turner
71883d7164 Fix tmux panic (#165)
Fix `tmux` sessions

This will create a new session named `topgrade`, `topgrade-1`,
`topgrade-2`, using the first nonexistent session name it finds. That
session will have a window in it named `topgrade` in which `topgrade` is
run. If `topgrade --tmux` is being run from within tmux, it won't attach
to the new tmux session. If the user is not currently in tmux, it will
attach to the newly-created session.

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-11-23 15:24:58 +00:00
Rebecca Turner
d4fe748814 Run fisher if fish -c 'type -t fisher' is OK (#172) 2022-11-23 15:24:58 +00:00
LeSnake04
369d923532 Added configuration for cargo-deb and cargo-generate-rpm (#178) 2022-11-23 15:24:58 +00:00
Guilherme Silva
6be4a4a48d CI improvements + Android and FreeBSD targets (#177)
* Bump minimum Rust version to 1.60

As required by the `time` crate (`notify-rust` > `mac-notification-sys` > `time`).

* Simplify CI

Changes:

- Bump `actions/checkout` to v3, fixing a bunch of warnings.
- Replace unmaintained `actions-rs/cargo` by `dtolnay/rust-toolchain`.
- Run Rustfmt only once.
- Add support for cached dependencies (via `Swatinem/rust-cache`).

* Add Android target

Use the awesome `cross` tool for cross-compiling!

* Add FreeBSD target
2022-11-23 15:24:58 +00:00
Guilherme Silva
7442ddd386 Further clippy fixes (#176) 2022-11-23 15:24:58 +00:00
Guilherme Silva
761ffac127 Fix compilation on FreeBSD (#175) 2022-11-23 15:24:58 +00:00
Guilherme Silva
5ca1dc3703 Fix compilation on Android (#166)
Co-authored-by: guihkx <guihkx@users.noreply.github.com>
2022-11-23 15:24:58 +00:00
Rebecca Turner
a18c6e815c Fix lack of separators for pnpm/npm/yarn (#170) 2022-11-23 15:24:58 +00:00
Rebecca Turner
022afab1ca Print errors when steps fail (#171) 2022-11-23 15:24:58 +00:00
Rebecca Turner
2cbb7db66d Use color_eyre (#173) 2022-11-23 15:24:58 +00:00
Rebecca Turner
e84173be8f Add CommandExt trait (#146)
* Color CI output

* Improve `CommandExt`

* Add comments explaining `#[allow]`s

* Remove useless `dead_code` annotation

* Improve error messages

* Print errors when running a shell errors

* fixup! Remove useless `dead_code` annotation
2022-11-23 15:24:58 +00:00
Thomas Schönauer
bd34a3bcd4 Revert "10.2.0 release " (#215)
Revert "10.2.0 release  (#213)"

This reverts commit 13076fcef6.
2022-11-23 16:23:00 +01:00
Thomas Schönauer
13076fcef6 10.2.0 release (#213) 2022-11-23 15:18:09 +00:00
0xMRTT
6f48017761 Create SECURITY.md (#142) 2022-11-16 19:40:17 +01:00
Marcin Puc
7fb07879bb Add repology badge (#191) 2022-11-16 19:39:36 +01:00
Pascal Jufer
ecbaf52156 Enhance the README (#186) 2022-11-15 08:41:35 +01:00
Thomas Schönauer
6a6a84b0c5 Update update_homebrew.yml 2022-11-06 15:28:57 +00:00
Thomas Schönauer
3486200b2c Update update_homebrew.yml 2022-11-06 15:08:35 +00:00
Thomas Schönauer
f6b3a8fdca Update update_homebrew.yml 2022-11-06 15:07:55 +00:00
Thomas Schönauer
058a6fd9c9 Update update_homebrew.yml 2022-11-06 15:01:20 +00:00
Thomas Schönauer
e1783e3af8 Create update_homebrew.yml 2022-11-06 14:58:47 +00:00
Thomas Schönauer
e161d3cd3c 10.1.2 (#162)
* Closes #150 please disable distrobox by default (#151)

* Check if distrobox exists before running step

* Improve help prompt value names (#153)

* 159 self update error message with standalone versions (#161)

* Rename back to topgrade

* Bugfix Version bump

* Changes reference to topgrade-rs in self-update

* Fixes distrobox errors (#160)

* Rename back to topgrade

* Bugfix Version bump

* Check if distrobox exists before running step

* Fixed sitrobox and version bump

* Version bump to 10.1.2

Co-authored-by: Marcin Puc <tranzystorek.io@protonmail.com>
2022-11-06 13:54:38 +00:00
Marcin Puc
318f935b43 Add info about Void Linux to README (#154) 2022-11-05 14:43:09 +00:00
51 changed files with 2528 additions and 1289 deletions

View File

@@ -4,66 +4,88 @@ on:
branches:
- main
name: CI
name: Check and Lint
env:
RUST_VER: '1.60.0'
CROSS_VER: '0.2.4'
CARGO_NET_RETRY: 3
jobs:
check:
name: Check
strategy:
matrix:
platform: [ ubuntu-latest, macos-latest, windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: check
fmt:
name: Rustfmt
strategy:
matrix:
platform: [ ubuntu-latest, macos-latest, windows-latest ]
runs-on: ${{ matrix.platform }}
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Checkout code
uses: actions/checkout@v3
clippy:
name: Clippy
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: '${{ env.RUST_VER }}'
components: rustfmt
- name: Run cargo fmt
env:
TERM: xterm-256color
run: |
cargo fmt --all -- --check
main:
needs: fmt
name: ${{ matrix.target_name }} (check, clippy)
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-latest, macos-latest, windows-latest ]
runs-on: ${{ matrix.platform }}
include:
- target: x86_64-linux-android
target_name: Android
use_cross: true
os: ubuntu-20.04
- target: x86_64-unknown-freebsd
target_name: FreeBSD
use_cross: true
os: ubuntu-20.04
- target: x86_64-unknown-linux-gnu
target_name: Linux
os: ubuntu-20.04
- target: x86_64-apple-darwin
target_name: macOS
os: macos-11
- target: x86_64-unknown-netbsd
target_name: NetBSD
use_cross: true
os: ubuntu-20.04
- target: x86_64-pc-windows-msvc
target_name: Windows
os: windows-2019
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
toolchain: '${{ env.RUST_VER }}'
components: clippy
override: true
- uses: actions-rs/cargo@v1.0.1
- name: Setup Rust Cache
uses: Swatinem/rust-cache@v2
with:
command: clippy
args: --all-targets --locked -- -D warnings
name: Clippy Output
- uses: actions-rs/cargo@v1.0.1
with:
command: clippy
args: --all-targets --locked --all-features -- -D warnings
name: Clippy (All features) Output
prefix-key: ${{ matrix.target }}
- name: Setup cross
if: matrix.use_cross == true
run: curl -fL --retry 3 https://github.com/cross-rs/cross/releases/download/v${{ env.CROSS_VER }}/cross-x86_64-unknown-linux-musl.tar.gz | tar vxz -C /usr/local/bin
- name: Run cargo check
run: ${{ matrix.use_cross == true && 'cross' || 'cargo' }} check --locked --target ${{ matrix.target }}
- name: Run cargo clippy
run: ${{ matrix.use_cross == true && 'cross' || 'cargo' }} clippy --locked --target ${{ matrix.target }} --all-features -- -D warnings

38
.github/workflows/update_homebrew.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Publish to Homebrew
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
workflow_dispatch:
push:
tags:
- "v*"
jobs:
homebrew-publish:
runs-on: ubuntu-latest
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Cache Homebrew Bundler RubyGems
id: cache
uses: actions/cache@v1
with:
path: ${{ steps.set-up-homebrew.outputs.gems-path }}
key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}
restore-keys: ${{ runner.os }}-rubygems-
- name: Install Homebrew Bundler RubyGems
if: steps.cache.outputs.cache-hit != 'true'
run: brew install-bundler-gems
- name: Bump formulae
uses: Homebrew/actions/bump-formulae@master
with:
# Custom GitHub access token with only the 'public_repo' scope enabled
token: ${{secrets.HOMEBREW_ACCESS_TOKEN}}
# Bump only these formulae if outdated
formulae: |
topgrade

735
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,10 @@ name = "topgrade"
description = "Upgrade all the things"
categories = ["os"]
keywords = ["upgrade", "update"]
license-file = "LICENSE"
license = "GPL-3.0"
# license-file = "LICENSE"
repository = "https://github.com/topgrade-rs/topgrade"
version = "10.1.0"
version = "10.2.5"
authors = ["Roey Darwish Dror <roey.ghost@gmail.com>", "Thomas Schönauer <t.schoenauer@hgs-wt.at>"]
exclude = ["doc/screenshot.gif"]
edition = "2021"
@@ -27,29 +28,40 @@ toml = "0.5"
which_crate = { version = "~4.1", package = "which" }
shellexpand = "~2.1"
clap = { version = "~3.1", features = ["cargo", "derive"] }
log = "~0.4"
clap_complete = "~3.1"
clap_mangen = "~0.1"
walkdir = "~2.3"
console = "~0.15"
lazy_static = "~1.4"
chrono = "~0.4"
pretty_env_logger = "~0.4"
glob = "~0.3"
strum = { version = "~0.24", features = ["derive"] }
thiserror = "~1.0"
anyhow = "~1.0"
tempfile = "~3.2"
cfg-if = "~1.0"
tokio = { version = "~1.5", features = ["process", "rt-multi-thread"] }
tokio = { version = "~1.18", features = ["process", "rt-multi-thread"] }
futures = "~0.3"
regex = "~1.5"
sys-info = "~0.9"
semver = "~1.0"
shell-words = "~1.1"
color-eyre = "~0.6"
tracing = { version = "~0.1", features = ["attributes", "log"] }
tracing-subscriber = { version = "~0.3", features = ["env-filter", "time"] }
[target.'cfg(target_os = "macos")'.dependencies]
notify-rust = "~4.5"
[package.metadata.generate-rpm]
assets = [{source = "target/release/topgrade", dest="/usr/bin/topgrade"}]
[package.metadata.generate-rpm.requires]
git = "*"
[package.metadata.deb]
depends = "$auto,git"
[target.'cfg(unix)'.dependencies]
libc = "~0.2"
nix = "~0.24"
rust-ini = "~0.18"
self_update_crate = { version = "~0.30", default-features = false, optional = true, package = "self_update", features = ["archive-tar", "compression-flate2", "rustls"] }

4
Cross.toml Normal file
View File

@@ -0,0 +1,4 @@
# Workaround for: https://github.com/cross-rs/cross/issues/1100
# TODO: Remove this file altogether once a new version of cross (after v0.2.4) is released.
[target.x86_64-unknown-freebsd.env]
passthrough = ["AR_x86_64_unknown_freebsd=x86_64-unknown-freebsd12-ar"]

View File

@@ -1,12 +1,12 @@
<div align="center">
<img alt="Topgrade" src="doc/topgrade.png" width="850px">
<h1>
<img alt="Topgrade" src="doc/topgrade_transparent.png" width="850px">
</h1>
<!--
<a href="https://github.com/topgrade-rs/topgrade/releases"><img alt="GitHub Release" src="https://img.shields.io/github/release/r-darwish/topgrade.svg"></a>
<a href="https://github.com/topgrade-rs/topgrade/releases"><img alt="GitHub Release" src="https://img.shields.io/github/release/topgrade-rs/topgrade.svg"></a>
<a href="https://crates.io/crates/topgrade"><img alt="crates.io" src="https://img.shields.io/crates/v/topgrade.svg"></a>
<a href="https://aur.archlinux.org/packages/topgrade"><img alt="AUR" src="https://img.shields.io/aur/version/topgrade.svg"></a>
<a href="https://formulae.brew.sh/formula/topgrade"><img alt="Homebrew" src="https://img.shields.io/homebrew/v/topgrade.svg"></a>
-->
<img alt="Demo" src="doc/screenshot.gif" width="550px">
</div>
@@ -22,48 +22,55 @@ To remedy this, **Topgrade** detects which tools you use and runs the appropriat
## Installation
- Arch Linux: [AUR](https://aur.archlinux.org/packages/topgrade) package.
- NixOS: _topgrade_ package in `nixpkgs`.
- macOS: [Homebrew](https://formulae.brew.sh/formula/topgrade) or [MacPorts](https://ports.macports.org/port/topgrade/).
[![Packaging status](https://repology.org/badge/vertical-allrepos/topgrade.svg)](https://repology.org/project/topgrade/versions)
Other systems users can either use `cargo install` or use the compiled binaries from the release page.
- Arch Linux: [AUR](https://aur.archlinux.org/packages/topgrade)
- NixOS: [Nixpkgs](https://search.nixos.org/packages?show=topgrade)
- Void Linux: [XBPS](https://voidlinux.org/packages/?arch=x86_64&q=topgrade)
- macOS: [Homebrew](https://formulae.brew.sh/formula/topgrade) or [MacPorts](https://ports.macports.org/port/topgrade/)
Other systems users can either use `cargo install` or the compiled binaries from the release page.
The compiled binaries contain a self-upgrading feature.
Topgrade requires Rust 1.51 or above.
## Documentation
> **Warning**
> Work in Progress
You can visit the documentation at [topgrade-rs.github.io](https://topgrade-rs.github.io/) .
Topgrade requires Rust 1.60 or above.
## Usage
Just run `topgrade`.
See [the documentation](https://topgrade-rs.github.io/) for the list of things Topgrade supports.
Visit the documentation at [topgrade-rs.github.io](https://topgrade-rs.github.io/) for more information.
> **Warning**
> Work in Progress
## Customization
See `config.example.toml` for an example configuration file.
### Configuration path
### Configuration Path
The configuration should be placed in the following paths depending by the operating system:
The configuration should be placed in the following paths depending on the operating system:
- **Windows** - `%APPDATA%/topgrade.toml`
- **macOS** and **other Unix systems** - `${XDG_CONFIG_HOME:-~/.config}/topgrade.toml`
## Remote Execution
You can specify a key called `remote_topgrades` in the configuration file.
This key should contain a list of hostnames that have Topgrade installed on them.
Topgrade will use `ssh` to run `topgrade` on remote hosts before acting locally.
To limit the execution only to specific hosts use the `--remote-host-limit` parameter.
## Contribution
### Problems or missing features?
Open a new Issue describing your problem and if possible with a possible solution.
Open a new issue describing your problem and if possible provide a solution.
### Missing a feature or found an unsupported tool/distro?
Just let us now what you are missing by opening an issue.
For tools please open an Issue describing the tool, which platforms it supports and if possible, give us an example of its usage.
For tools, please open an issue describing the tool, which platforms it supports and if possible, give us an example of its usage.
### Want to contribute to the code?
@@ -73,16 +80,9 @@ Just fork the repository and start coding.
- Check if your code passes `cargo fmt` and `cargo clippy`.
- Check if your code is self explanatory, if not it should be documented by comments.
- Make a Pull Request to the dev branch for new features or to the bug-fixes branch for bug fixes.
- Make a pull request to the `dev` branch for new features or to the `bug-fixes` branch for bug fixes.
## Remote execution
You can specify a key called `remote_topgrades` in the configuration file.
This key should contain a list of hostnames that have Topgrade installed on them.
Topgrade will use `ssh` to run `topgrade` on remote hosts before acting locally.
To limit the execution only to specific hosts use the `--remote-host-limit` parameter.
## ToDo
## Roadmap
- [ ] Add a proper testing framework to the code base.
- [ ] Add unit tests for package managers.

11
SECURITY.md Normal file
View File

@@ -0,0 +1,11 @@
# Security Policy
## Supported Versions
We only support the latest major version and each subversion.
| Version | Supported |
| -------- | ------------------ |
| 10.0.x | :white_check_mark: |
| < 10.0 | :x: |

5
clippy.toml Normal file
View File

@@ -0,0 +1,5 @@
disallowed-methods = [
{ path = "std::process::Command::output", reason = "Use `output_checked[_with][_utf8]`" },
{ path = "std::process::Command::spawn", reason = "Use `spawn_checked`" },
{ path = "std::process::Command::status", reason = "Use `status_checked`" },
]

View File

@@ -13,6 +13,10 @@
# Do not ask to retry failed steps (default: false)
#no_retry = true
# Run `sudo -v` to cache credentials at the start of the run; this avoids a
# blocking password prompt in the middle of a possibly-unattended run.
#pre_sudo = false
# Run inside tmux
#run_in_tmux = true
@@ -40,6 +44,9 @@
# Skip sending a notification at the end of a run
#skip_notify = true
# Skip the preamble displayed when topgrade is run
#display_preamble = false
[git]
#max_concurrency = 5
# Additional git repositories to pull
@@ -70,12 +77,15 @@
#autoremove = true
[linux]
# Arch Package Manager to use. Allowed values: autodetect, trizen, aura, paru, yay, pikaur, pacman, pamac.
# Arch Package Manager to use. Allowed values: autodetect, aura, garuda_update, pacman, pamac, paru, pikaur, trizen, yay.
#arch_package_manager = "pacman"
# Arguments to pass yay (or paru) when updating packages
#yay_arguments = "--nodevel"
# Arguments to pass dnf when updating packages
#dnf_arguments = "--refresh"
#aura_aur_arguments = "-kx"
#aura_pacman_arguments = ""
#garuda_update_arguments = ""
#show_arch_news = true
#trizen_arguments = "--devel"
#pikaur_arguments = ""
@@ -85,11 +95,18 @@
#emerge_update_flags = "-uDNa --with-bdeps=y world"
#redhat_distro_sync = false
#rpm_ostree = false
#nix_arguments = "--flake"
[python]
#enable_pip_review = true ###disabled by default
#enable_pipupgrade = true ###disabled by default
[windows]
# Manually select Windows updates
#accept_all_updates = false
#open_remotes_in_new_terminal = true
#wsl_update_pre_release = true
#wsl_update_use_web_download = true
# Causes Topgrade to rename itself during the run to allow package managers
# to upgrade it. Use this only if you installed Topgrade by using a package

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

244
src/command.rs Normal file
View File

@@ -0,0 +1,244 @@
//! Utilities for running commands and providing user-friendly error messages.
use std::fmt::Display;
use std::process::Child;
use std::process::{Command, ExitStatus, Output};
use color_eyre::eyre;
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use crate::error::TopgradeError;
/// Like [`Output`], but UTF-8 decoded.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Utf8Output {
pub status: ExitStatus,
pub stdout: String,
pub stderr: String,
}
impl TryFrom<Output> for Utf8Output {
type Error = eyre::Error;
fn try_from(Output { status, stdout, stderr }: Output) -> Result<Self, Self::Error> {
let stdout = String::from_utf8(stdout).map_err(|err| {
eyre!(
"Stdout contained invalid UTF-8: {}",
String::from_utf8_lossy(err.as_bytes())
)
})?;
let stderr = String::from_utf8(stderr).map_err(|err| {
eyre!(
"Stderr contained invalid UTF-8: {}",
String::from_utf8_lossy(err.as_bytes())
)
})?;
Ok(Utf8Output { status, stdout, stderr })
}
}
impl TryFrom<&Output> for Utf8Output {
type Error = eyre::Error;
fn try_from(Output { status, stdout, stderr }: &Output) -> Result<Self, Self::Error> {
let stdout = String::from_utf8(stdout.to_vec()).map_err(|err| {
eyre!(
"Stdout contained invalid UTF-8: {}",
String::from_utf8_lossy(err.as_bytes())
)
})?;
let stderr = String::from_utf8(stderr.to_vec()).map_err(|err| {
eyre!(
"Stderr contained invalid UTF-8: {}",
String::from_utf8_lossy(err.as_bytes())
)
})?;
let status = *status;
Ok(Utf8Output { status, stdout, stderr })
}
}
impl Display for Utf8Output {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.stdout)
}
}
/// Extension trait for [`Command`], adding helpers to gather output while checking the exit
/// status.
///
/// These also give us significantly better error messages, which include:
///
/// 1. The command and arguments that were executed, escaped with familiar `sh` syntax.
/// 2. The exit status of the command or the signal that killed it.
/// 3. If we were capturing the output of the command, rather than forwarding it to the user's
/// stdout/stderr, the error message includes the command's stdout and stderr output.
///
/// Additionally, executing commands with these methods will log the command at debug-level,
/// useful when gathering error reports.
pub trait CommandExt {
type Child;
/// Like [`Command::output`], but checks the exit status and provides nice error messages.
///
/// Returns an `Err` if the command failed to execute or returned a non-zero exit code.
#[track_caller]
fn output_checked(&mut self) -> eyre::Result<Output> {
self.output_checked_with(|output: &Output| if output.status.success() { Ok(()) } else { Err(()) })
}
/// Like [`output_checked`], but also decodes Stdout and Stderr as UTF-8.
///
/// Returns an `Err` if the command failed to execute, returned a non-zero exit code, or if the
/// output contains invalid UTF-8.
#[track_caller]
fn output_checked_utf8(&mut self) -> eyre::Result<Utf8Output> {
let output = self.output_checked()?;
output.try_into()
}
/// Like [`output_checked`] but a closure determines if the command failed instead of
/// [`ExitStatus::success`].
///
/// Returns an `Err` if the command failed to execute or if `succeeded` returns an `Err`.
/// (This lets the caller substitute their own notion of "success" instead of assuming
/// non-zero exit codes indicate success.)
#[track_caller]
fn output_checked_with(&mut self, succeeded: impl Fn(&Output) -> Result<(), ()>) -> eyre::Result<Output>;
/// Like [`output_checked_with`], but also decodes Stdout and Stderr as UTF-8.
///
/// Returns an `Err` if the command failed to execute, if `succeeded` returns an `Err`, or if
/// the output contains invalid UTF-8.
#[track_caller]
fn output_checked_with_utf8(
&mut self,
succeeded: impl Fn(&Utf8Output) -> Result<(), ()>,
) -> eyre::Result<Utf8Output> {
// This decodes the Stdout and Stderr as UTF-8 twice...
let output =
self.output_checked_with(|output| output.try_into().map_err(|_| ()).and_then(|o| succeeded(&o)))?;
output.try_into()
}
/// Like [`Command::status`], but gives a nice error message if the status is unsuccessful
/// rather than returning the [`ExitStatus`].
///
/// Returns `Ok` if the command executes successfully, returns `Err` if the command fails to
/// execute or returns a non-zero exit code.
#[track_caller]
fn status_checked(&mut self) -> eyre::Result<()> {
self.status_checked_with(|status| if status.success() { Ok(()) } else { Err(()) })
}
/// Like [`status_checked`], but gives a nice error message if the status is unsuccessful
/// rather than returning the [`ExitStatus`].
///
/// Returns `Ok` if the command executes successfully, returns `Err` if the command fails to
/// execute or if `succeeded` returns an `Err`.
/// (This lets the caller substitute their own notion of "success" instead of assuming
/// non-zero exit codes indicate success.)
#[track_caller]
fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> eyre::Result<()>;
/// Like [`Command::spawn`], but gives a nice error message if the command fails to
/// execute.
#[track_caller]
fn spawn_checked(&mut self) -> eyre::Result<Self::Child>;
}
impl CommandExt for Command {
type Child = Child;
fn output_checked_with(&mut self, succeeded: impl Fn(&Output) -> Result<(), ()>) -> eyre::Result<Output> {
let command = log(self);
// This is where we implement `output_checked`, which is what we prefer to use instead of
// `output`, so we allow `Command::output` here.
#[allow(clippy::disallowed_methods)]
let output = self
.output()
.with_context(|| format!("Failed to execute `{command}`"))?;
if succeeded(&output).is_ok() {
Ok(output)
} else {
let mut message = format!("Command failed: `{command}`");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let stdout_trimmed = stdout.trim();
if !stdout_trimmed.is_empty() {
message.push_str(&format!("\n\nStdout:\n{stdout_trimmed}"));
}
let stderr_trimmed = stderr.trim();
if !stderr_trimmed.is_empty() {
message.push_str(&format!("\n\nStderr:\n{stderr_trimmed}"));
}
let (program, _) = get_program_and_args(self);
let err = TopgradeError::ProcessFailedWithOutput(program, output.status, stderr.into_owned());
let ret = Err(err).with_context(|| message);
tracing::debug!("Command failed: {ret:?}");
ret
}
}
fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> eyre::Result<()> {
let command = log(self);
let message = format!("Failed to execute `{command}`");
// This is where we implement `status_checked`, which is what we prefer to use instead of
// `status`, so we allow `Command::status` here.
#[allow(clippy::disallowed_methods)]
let status = self.status().with_context(|| message.clone())?;
if succeeded(status).is_ok() {
Ok(())
} else {
let (program, _) = get_program_and_args(self);
let err = TopgradeError::ProcessFailed(program, status);
let ret = Err(err).with_context(|| format!("Command failed: `{command}`"));
tracing::debug!("Command failed: {ret:?}");
ret
}
}
fn spawn_checked(&mut self) -> eyre::Result<Self::Child> {
let command = log(self);
let message = format!("Failed to execute `{command}`");
// This is where we implement `spawn_checked`, which is what we prefer to use instead of
// `spawn`, so we allow `Command::spawn` here.
#[allow(clippy::disallowed_methods)]
{
self.spawn().with_context(|| message.clone())
}
}
}
fn get_program_and_args(cmd: &Command) -> (String, String) {
// We're not doing anything weird with commands that are invalid UTF-8 so this is fine.
let program = cmd.get_program().to_string_lossy().into_owned();
let args = shell_words::join(cmd.get_args().map(|arg| arg.to_string_lossy()));
(program, args)
}
fn format_program_and_args(cmd: &Command) -> String {
let (program, args) = get_program_and_args(cmd);
if args.is_empty() {
program
} else {
format!("{program} {args}")
}
}
fn log(cmd: &Command) -> String {
let command = format_program_and_args(cmd);
tracing::debug!("Executing command `{command}`");
command
}

View File

@@ -1,21 +1,25 @@
#![allow(dead_code)]
use anyhow::Context;
use anyhow::Result;
use clap::{ArgEnum, Parser};
use directories::BaseDirs;
use log::debug;
use regex::Regex;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::fs::write;
use std::path::PathBuf;
use std::process::Command;
use std::{env, fs};
use clap::{ArgEnum, Parser};
use clap_complete::Shell;
use color_eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use directories::BaseDirs;
use regex::Regex;
use serde::Deserialize;
use strum::{EnumIter, EnumString, EnumVariantNames, IntoEnumIterator};
use sys_info::hostname;
use tracing::debug;
use which_crate::which;
use super::utils::editor;
use crate::command::CommandExt;
use super::utils::{editor, hostname};
pub static EXAMPLE_CONFIG: &str = include_str!("../config.example.toml");
@@ -67,12 +71,13 @@ type Commands = BTreeMap<String, String>;
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum Step {
AM,
Asdf,
Atom,
Bin,
BrewCask,
BrewFormula,
Bun,
Bin,
Cargo,
Chezmoi,
Chocolatey,
@@ -83,8 +88,9 @@ pub enum Step {
Containers,
CustomCommands,
DebGet,
Distrobox,
Deno,
Distrobox,
DkpPacman,
Dotnet,
Emacs,
Firmware,
@@ -96,14 +102,17 @@ pub enum Step {
Ghcup,
GithubCliExtensions,
GitRepos,
GnomeShellExtensions,
Go,
Guix,
Haxelib,
GnomeShellExtensions,
Helm,
HomeManager,
Jetpack,
Julia,
Juliaup,
Kakoune,
Helix,
Krew,
Macports,
Mas,
@@ -115,10 +124,13 @@ pub enum Step {
Pacdef,
Pacstall,
Pearl,
Pipx,
Pip3,
PipReview,
Pipupgrade,
Pipx,
Pkg,
Pkgin,
Pnpm,
Powershell,
Protonup,
Raco,
@@ -126,6 +138,7 @@ pub enum Step {
Remotes,
Restarts,
Rtcl,
RubyGems,
Rustup,
Scoop,
Sdkman,
@@ -145,7 +158,9 @@ pub enum Step {
Vim,
Winget,
Wsl,
WslUpdate,
Yadm,
Yarn,
}
#[derive(Deserialize, Default, Debug)]
@@ -172,6 +187,15 @@ pub struct Windows {
self_rename: Option<bool>,
open_remotes_in_new_terminal: Option<bool>,
enable_winget: Option<bool>,
wsl_update_pre_release: Option<bool>,
wsl_update_use_web_download: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct Python {
enable_pip_review: Option<bool>,
enable_pipupgrade: Option<bool>,
}
#[derive(Deserialize, Default, Debug)]
@@ -221,13 +245,14 @@ pub struct Brew {
#[serde(rename_all = "snake_case")]
pub enum ArchPackageManager {
Autodetect,
Trizen,
Paru,
Yay,
Pacman,
Pikaur,
Pamac,
Aura,
GarudaUpdate,
Pacman,
Pamac,
Paru,
Pikaur,
Trizen,
Yay,
}
#[derive(Deserialize, Default, Debug)]
@@ -238,10 +263,12 @@ pub struct Linux {
aura_pacman_arguments: Option<String>,
arch_package_manager: Option<ArchPackageManager>,
show_arch_news: Option<bool>,
garuda_update_arguments: Option<String>,
trizen_arguments: Option<String>,
pikaur_arguments: Option<String>,
pamac_arguments: Option<String>,
dnf_arguments: Option<String>,
nix_arguments: Option<String>,
apt_arguments: Option<String>,
enable_tlmgr: Option<bool>,
redhat_distro_sync: Option<bool>,
@@ -266,6 +293,7 @@ pub struct Vim {
#[serde(deny_unknown_fields)]
/// Configuration file
pub struct ConfigFile {
pre_sudo: Option<bool>,
pre_commands: Option<Commands>,
post_commands: Option<Commands>,
commands: Option<Commands>,
@@ -280,10 +308,12 @@ pub struct ConfigFile {
tmux_arguments: Option<String>,
set_title: Option<bool>,
display_time: Option<bool>,
display_preamble: Option<bool>,
assume_yes: Option<bool>,
yay_arguments: Option<String>,
aura_aur_arguments: Option<String>,
aura_pacman_arguments: Option<String>,
python: Option<Python>,
no_retry: Option<bool>,
run_in_tmux: Option<bool>,
cleanup: Option<bool>,
@@ -348,12 +378,12 @@ impl ConfigFile {
};
let contents = fs::read_to_string(&config_path).map_err(|e| {
log::error!("Unable to read {}", config_path.display());
tracing::error!("Unable to read {}", config_path.display());
e
})?;
let mut result: Self = toml::from_str(&contents).map_err(|e| {
log::error!("Failed to deserialize {}", config_path.display());
tracing::error!("Failed to deserialize {}", config_path.display());
e
})?;
@@ -389,9 +419,8 @@ impl ConfigFile {
Command::new(command)
.args(args)
.arg(config_path)
.spawn()
.and_then(|mut p| p.wait())?;
Ok(())
.status_checked()
.context("Failed to open configuration file editor")
}
}
@@ -424,22 +453,22 @@ pub struct CommandLineArgs {
no_retry: bool,
/// Do not perform upgrades for the given steps
#[clap(long = "disable", arg_enum, multiple_values = true)]
#[clap(long = "disable", value_name = "STEP", arg_enum, multiple_values = true)]
disable: Vec<Step>,
/// Perform only the specified steps (experimental)
#[clap(long = "only", arg_enum, multiple_values = true)]
#[clap(long = "only", value_name = "STEP", arg_enum, multiple_values = true)]
only: Vec<Step>,
/// Run only specific custom commands
#[clap(long = "custom-commands")]
#[clap(long = "custom-commands", value_name = "NAME", multiple_values = true)]
custom_commands: Vec<String>,
/// Set environment variables
#[clap(long = "env", multiple_values = true)]
#[clap(long = "env", value_name = "NAME=VALUE", multiple_values = true)]
env: Vec<String>,
/// Output logs
/// Output debug logs. Alias for `--log-filter debug`.
#[clap(short = 'v', long = "verbose")]
pub verbose: bool,
@@ -452,7 +481,14 @@ pub struct CommandLineArgs {
skip_notify: bool,
/// Say yes to package manager's prompt
#[clap(short = 'y', long = "yes", arg_enum, multiple_values = true, min_values = 0)]
#[clap(
short = 'y',
long = "yes",
value_name = "STEP",
arg_enum,
multiple_values = true,
min_values = 0
)]
yes: Option<Vec<Step>>,
/// Don't pull the predefined git repos
@@ -460,16 +496,30 @@ pub struct CommandLineArgs {
disable_predefined_git_repos: bool,
/// Alternative configuration file
#[clap(long = "config")]
#[clap(long = "config", value_name = "PATH")]
config: Option<PathBuf>,
/// A regular expression for restricting remote host execution
#[clap(long = "remote-host-limit")]
#[clap(long = "remote-host-limit", value_name = "REGEX")]
remote_host_limit: Option<Regex>,
/// Show the reason for skipped steps
#[clap(long = "show-skipped")]
show_skipped: bool,
/// Tracing filter directives.
///
/// See: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/struct.EnvFilter.html
#[clap(long, default_value = "info")]
pub log_filter: String,
/// Print completion script for the given shell and exit
#[clap(long, arg_enum, hide = true)]
pub gen_completion: Option<Shell>,
/// Print roff manpage and exit
#[clap(long, hide = true)]
pub gen_manpage: bool,
}
impl CommandLineArgs {
@@ -484,6 +534,14 @@ impl CommandLineArgs {
pub fn env_variables(&self) -> &Vec<String> {
&self.env
}
pub fn tracing_filter_directives(&self) -> String {
if self.verbose {
"debug".into()
} else {
self.log_filter.clone()
}
}
}
/// Represents the application configuration
@@ -507,11 +565,11 @@ impl Config {
ConfigFile::read(base_dirs, opt.config.clone()).unwrap_or_else(|e| {
// Inform the user about errors when loading the configuration,
// but fallback to the default config to at least attempt to do something
log::error!("failed to load configuration: {}", e);
tracing::error!("failed to load configuration: {}", e);
ConfigFile::default()
})
} else {
log::debug!("Configuration directory {} does not exist", config_directory.display());
tracing::debug!("Configuration directory {} does not exist", config_directory.display());
ConfigFile::default()
};
@@ -626,7 +684,7 @@ impl Config {
}
/// Extra Tmux arguments
pub fn tmux_arguments(&self) -> anyhow::Result<Vec<String>> {
pub fn tmux_arguments(&self) -> eyre::Result<Vec<String>> {
let args = &self.config_file.tmux_arguments.as_deref().unwrap_or_default();
shell_words::split(args)
// The only time the parse failed is in case of a missing close quote.
@@ -699,6 +757,24 @@ impl Config {
.unwrap_or(false)
}
// Should wsl --update should use the --pre-release flag
pub fn wsl_update_pre_release(&self) -> bool {
self.config_file
.windows
.as_ref()
.and_then(|w| w.wsl_update_pre_release)
.unwrap_or(false)
}
// Should wsl --update use the --web-download flag
pub fn wsl_update_use_web_download(&self) -> bool {
self.config_file
.windows
.as_ref()
.and_then(|w| w.wsl_update_use_web_download)
.unwrap_or(false)
}
/// Whether Brew cask should be greedy
pub fn brew_cask_greedy(&self) -> bool {
self.config_file
@@ -740,6 +816,15 @@ impl Config {
self.config_file.notify_each_step.unwrap_or(false)
}
/// Extra garuda-update arguments
pub fn garuda_update_arguments(&self) -> &str {
self.config_file
.linux
.as_ref()
.and_then(|s| s.garuda_update_arguments.as_deref())
.unwrap_or("")
}
/// Extra trizen arguments
pub fn trizen_arguments(&self) -> &str {
self.config_file
@@ -827,6 +912,14 @@ impl Config {
.and_then(|linux| linux.dnf_arguments.as_deref())
}
/// Extra nix arguments
pub fn nix_arguments(&self) -> Option<&str> {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.nix_arguments.as_deref())
}
/// Distrobox use root
pub fn distrobox_root(&self) -> bool {
self.config_file
@@ -891,7 +984,7 @@ impl Config {
.linux
.as_ref()
.and_then(|linux| linux.rpm_ostree)
.unwrap_or(false)
.unwrap_or(true)
}
/// Should we ignore failures for this step
@@ -924,6 +1017,12 @@ impl Config {
.unwrap_or(false)
}
/// If `true`, `sudo` should be called after `pre_commands` in order to elevate at the
/// start of the session (and not in the middle).
pub fn pre_sudo(&self) -> bool {
self.config_file.pre_sudo.unwrap_or(false)
}
#[cfg(target_os = "linux")]
pub fn npm_use_sudo(&self) -> bool {
self.config_file
@@ -989,10 +1088,31 @@ impl Config {
.unwrap_or(false);
}
pub fn enable_pipupgrade(&self) -> bool {
return self
.config_file
.python
.as_ref()
.and_then(|python| python.enable_pipupgrade)
.unwrap_or(false);
}
pub fn enable_pip_review(&self) -> bool {
return self
.config_file
.python
.as_ref()
.and_then(|python| python.enable_pip_review)
.unwrap_or(false);
}
pub fn display_time(&self) -> bool {
self.config_file.display_time.unwrap_or(true)
}
pub fn display_preamble(&self) -> bool {
self.config_file.display_preamble.unwrap_or(true)
}
pub fn should_run_custom_command(&self, name: &str) -> bool {
if self.opt.custom_commands.is_empty() {
return true;

View File

@@ -16,6 +16,6 @@ extern "system" fn handler(ctrl_type: DWORD) -> BOOL {
pub fn set_handler() {
if 0 == unsafe { SetConsoleCtrlHandler(Some(handler), TRUE) } {
log::error!("Cannot set a control C handler")
tracing::error!("Cannot set a control C handler")
}
}

View File

@@ -4,11 +4,11 @@ use thiserror::Error;
#[derive(Error, Debug, PartialEq, Eq)]
pub enum TopgradeError {
#[error("{0}")]
ProcessFailed(ExitStatus),
#[error("`{0}` failed: {1}")]
ProcessFailed(String, ExitStatus),
#[error("{0}: {1}")]
ProcessFailedWithOutput(ExitStatus, String),
#[error("`{0}` failed: {1}")]
ProcessFailedWithOutput(String, ExitStatus, String),
#[error("Sudo is required for this step")]
#[allow(dead_code)]

View File

@@ -1,51 +1,47 @@
#![allow(dead_code)]
use crate::executor::RunType;
use crate::git::Git;
use crate::sudo::Sudo;
use crate::utils::require_option;
use crate::{config::Config, executor::Executor};
use anyhow::Result;
use color_eyre::eyre::Result;
use directories::BaseDirs;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::sync::Mutex;
pub struct ExecutionContext<'a> {
run_type: RunType,
sudo: &'a Option<PathBuf>,
sudo: Option<Sudo>,
git: &'a Git,
config: &'a Config,
base_dirs: &'a BaseDirs,
/// Name of a tmux session to execute commands in, if any.
/// This is used in `./steps/remote/ssh.rs`, where we want to run `topgrade` in a new
/// tmux window for each remote.
tmux_session: Mutex<Option<String>>,
}
impl<'a> ExecutionContext<'a> {
pub fn new(
run_type: RunType,
sudo: &'a Option<PathBuf>,
sudo: Option<Sudo>,
git: &'a Git,
config: &'a Config,
base_dirs: &'a BaseDirs,
) -> ExecutionContext<'a> {
ExecutionContext {
) -> Self {
Self {
run_type,
sudo,
git,
config,
base_dirs,
tmux_session: Mutex::new(None),
}
}
pub fn execute_elevated(&self, command: &Path, interactive: bool) -> Result<Executor> {
let sudo = require_option(self.sudo.clone(), "Sudo is required for this operation".into())?;
let mut cmd = self.run_type.execute(&sudo);
if sudo.ends_with("sudo") {
cmd.arg("--preserve-env=DIFFPROG");
}
if interactive {
cmd.arg("-i");
}
cmd.arg(command);
Ok(cmd)
Ok(sudo.execute_elevated(self, command, interactive))
}
pub fn run_type(&self) -> RunType {
@@ -56,8 +52,8 @@ impl<'a> ExecutionContext<'a> {
self.git
}
pub fn sudo(&self) -> &Option<PathBuf> {
self.sudo
pub fn sudo(&self) -> &Option<Sudo> {
&self.sudo
}
pub fn config(&self) -> &Config {
@@ -67,4 +63,12 @@ impl<'a> ExecutionContext<'a> {
pub fn base_dirs(&self) -> &BaseDirs {
self.base_dirs
}
pub fn set_tmux_session(&self, session_name: String) {
self.tmux_session.lock().unwrap().replace(session_name);
}
pub fn get_tmux_session(&self) -> Option<String> {
self.tmux_session.lock().unwrap().clone()
}
}

View File

@@ -1,11 +1,14 @@
//! Utilities for command execution
use crate::error::{DryRun, TopgradeError};
use crate::utils::{Check, CheckWithCodes};
use anyhow::Result;
use log::{debug, trace};
use std::ffi::{OsStr, OsString};
use std::path::Path;
use std::process::{Child, Command, ExitStatus};
use std::process::{Child, Command, ExitStatus, Output};
use color_eyre::eyre;
use color_eyre::eyre::Result;
use tracing::debug;
use crate::command::CommandExt;
use crate::error::DryRun;
/// An enum telling whether Topgrade should perform dry runs or actually perform the steps.
#[derive(Clone, Copy, Debug)]
@@ -56,6 +59,16 @@ pub enum Executor {
}
impl Executor {
/// Get the name of the program being run.
///
/// Will give weird results for non-UTF-8 programs; see `to_string_lossy()`.
pub fn get_program(&self) -> String {
match self {
Executor::Wet(c) => c.get_program().to_string_lossy().into_owned(),
Executor::Dry(c) => c.program.to_string_lossy().into_owned(),
}
}
/// See `std::process::Command::arg`
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Executor {
match self {
@@ -139,7 +152,7 @@ impl Executor {
let result = match self {
Executor::Wet(c) => {
debug!("Running {:?}", c);
c.spawn().map(ExecutorChild::Wet)?
c.spawn_checked().map(ExecutorChild::Wet)?
}
Executor::Dry(c) => {
c.dry_run();
@@ -153,7 +166,7 @@ impl Executor {
/// See `std::process::Command::output`
pub fn output(&mut self) -> Result<ExecutorOutput> {
match self {
Executor::Wet(c) => Ok(ExecutorOutput::Wet(c.output()?)),
Executor::Wet(c) => Ok(ExecutorOutput::Wet(c.output_checked()?)),
Executor::Dry(c) => {
c.dry_run();
Ok(ExecutorOutput::Dry)
@@ -161,23 +174,28 @@ impl Executor {
}
}
/// 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<()> {
self.spawn()?.wait()?.check()
}
/// An extension of `check_run` that allows you to set a sequence of codes
/// An extension of `status_checked` that allows you to set a sequence of codes
/// that can indicate success of a script
#[allow(dead_code)]
pub fn check_run_with_codes(&mut self, codes: &[i32]) -> Result<()> {
self.spawn()?.wait()?.check_with_codes(codes)
pub fn status_checked_with_codes(&mut self, codes: &[i32]) -> Result<()> {
match self {
Executor::Wet(c) => c.status_checked_with(|status| {
if status.success() || status.code().as_ref().map(|c| codes.contains(c)).unwrap_or(false) {
Ok(())
} else {
Err(())
}
}),
Executor::Dry(c) => {
c.dry_run();
Ok(())
}
}
}
}
pub enum ExecutorOutput {
Wet(std::process::Output),
Wet(Output),
Dry,
}
@@ -214,78 +232,33 @@ pub enum ExecutorChild {
Dry,
}
impl ExecutorChild {
/// See `std::process::Child::wait`
pub fn wait(&mut self) -> Result<ExecutorExitStatus> {
let result = match self {
ExecutorChild::Wet(c) => c.wait().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 CheckWithCodes for ExecutorExitStatus {
fn check_with_codes(self, codes: &[i32]) -> Result<()> {
match self {
ExecutorExitStatus::Wet(e) => e.check_with_codes(codes),
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>;
fn string_output(&mut self) -> Result<String>;
}
impl CommandExt for Command {
fn check_output(&mut self) -> Result<String> {
let output = self.output()?;
trace!("Output of {:?}: {:?}", self, output);
let status = output.status;
if !status.success() {
let stderr = String::from_utf8(output.stderr).unwrap_or_default();
return Err(TopgradeError::ProcessFailedWithOutput(status, stderr).into());
}
Ok(String::from_utf8(output.stdout)?)
}
fn string_output(&mut self) -> Result<String> {
let output = self.output()?;
trace!("Output of {:?}: {:?}", self, output);
Ok(String::from_utf8(output.stdout)?)
}
}
impl CommandExt for Executor {
fn check_output(&mut self) -> Result<String> {
let output = match self.output()? {
ExecutorOutput::Wet(output) => output,
ExecutorOutput::Dry => return Err(DryRun().into()),
};
let status = output.status;
if !status.success() {
let stderr = String::from_utf8(output.stderr).unwrap_or_default();
return Err(TopgradeError::ProcessFailedWithOutput(status, stderr).into());
type Child = ExecutorChild;
// TODO: It might be nice to make `output_checked_with` return something that has a
// variant for wet/dry runs.
fn output_checked_with(&mut self, succeeded: impl Fn(&Output) -> Result<(), ()>) -> eyre::Result<Output> {
match self {
Executor::Wet(c) => c.output_checked_with(succeeded),
Executor::Dry(c) => {
c.dry_run();
Err(DryRun().into())
}
}
Ok(String::from_utf8(output.stdout)?)
}
fn string_output(&mut self) -> Result<String> {
let output = match self.output()? {
ExecutorOutput::Wet(output) => output,
ExecutorOutput::Dry => return Err(DryRun().into()),
};
Ok(String::from_utf8(output.stdout)?)
fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> eyre::Result<()> {
match self {
Executor::Wet(c) => c.status_checked_with(succeeded),
Executor::Dry(c) => {
c.dry_run();
Ok(())
}
}
}
fn spawn_checked(&mut self) -> eyre::Result<Self::Child> {
self.spawn()
}
}

View File

@@ -3,13 +3,14 @@
use std::env;
use std::io;
use std::process::exit;
use std::time::Duration;
use anyhow::{anyhow, Result};
use clap::CommandFactory;
use clap::{crate_version, Parser};
use color_eyre::eyre::Context;
use color_eyre::eyre::{eyre, Result};
use console::Key;
use log::debug;
use log::LevelFilter;
use pretty_env_logger::formatted_timed_builder;
use tracing::debug;
use self::config::{CommandLineArgs, Config, Step};
use self::error::StepFailed;
@@ -18,6 +19,7 @@ use self::error::Upgraded;
use self::steps::{remote::*, *};
use self::terminal::*;
mod command;
mod config;
mod ctrlc;
mod error;
@@ -30,16 +32,32 @@ mod self_renamer;
#[cfg(feature = "self-update")]
mod self_update;
mod steps;
mod sudo;
mod terminal;
mod utils;
fn run() -> Result<()> {
color_eyre::install()?;
ctrlc::set_handler();
let base_dirs = directories::BaseDirs::new().ok_or_else(|| anyhow!("No base directories"))?;
let base_dirs = directories::BaseDirs::new().ok_or_else(|| eyre!("No base directories"))?;
let opt = CommandLineArgs::parse();
if let Some(shell) = opt.gen_completion {
let cmd = &mut CommandLineArgs::command();
clap_complete::generate(shell, cmd, clap::crate_name!(), &mut std::io::stdout());
return Ok(());
}
if opt.gen_manpage {
let man = clap_mangen::Man::new(CommandLineArgs::command());
man.render(&mut std::io::stdout())?;
return Ok(());
}
install_tracing(&opt.tracing_filter_directives())?;
for env in opt.env_variables() {
let mut splitted = env.split('=');
let var = splitted.next().unwrap();
@@ -47,14 +65,6 @@ fn run() -> Result<()> {
env::set_var(var, value);
}
let mut builder = formatted_timed_builder();
if opt.verbose {
builder.filter(Some("topgrade"), LevelFilter::Trace);
}
builder.init();
if opt.edit_config() {
Config::edit(&base_dirs)?;
return Ok(());
@@ -76,20 +86,28 @@ fn run() -> Result<()> {
debug!("Binary path: {:?}", std::env::current_exe());
debug!("Self Update: {:?}", cfg!(feature = "self-update"));
if config.display_preamble() || !config.skip_notify() {
print_warning("Due to a design issue with notify-send it could be that topgrade hangs when it's finished.
If this is the case on your system add the --skip-notify flag to the topgrade command or set skip_notify = true in the config file.
If you don't want this message to appear any longer set display_preamble = false in the config file.
For more information about this issue see https://askubuntu.com/questions/110969/notify-send-ignores-timeout and https://github.com/topgrade-rs/topgrade/issues/288.");
}
if config.run_in_tmux() && env::var("TOPGRADE_INSIDE_TMUX").is_err() {
#[cfg(unix)]
{
tmux::run_in_tmux(config.tmux_arguments()?);
tmux::run_in_tmux(config.tmux_arguments()?)?;
return Ok(());
}
}
let git = git::Git::new();
let mut git_repos = git::Repositories::new(&git);
let sudo = utils::sudo();
let sudo = sudo::Sudo::detect();
let run_type = executor::RunType::new(config.dry_run());
let ctx = execution_context::ExecutionContext::new(run_type, &sudo, &git, &config, &base_dirs);
let ctx = execution_context::ExecutionContext::new(run_type, sudo, &git, &config, &base_dirs);
let mut runner = runner::Runner::new(&ctx);
@@ -105,7 +123,7 @@ fn run() -> Result<()> {
return result;
}
}
print_warning(format!("Self update error: {}", e));
print_warning(format!("Self update error: {e}"));
}
}
}
@@ -123,15 +141,24 @@ fn run() -> Result<()> {
}
}
if config.pre_sudo() {
if let Some(sudo) = ctx.sudo() {
sudo.elevate(&ctx)?;
}
}
let powershell = powershell::Powershell::new();
let should_run_powershell = powershell.profile().is_some() && config.should_run(Step::Powershell);
#[cfg(windows)]
runner.execute(Step::Wsl, "WSL", || windows::run_wsl_topgrade(&ctx))?;
#[cfg(windows)]
runner.execute(Step::WslUpdate, "WSL", || windows::update_wsl(&ctx))?;
if let Some(topgrades) = config.remote_topgrades() {
for remote_topgrade in topgrades.iter().filter(|t| config.should_execute_remote(t)) {
runner.execute(Step::Remotes, format!("Remote ({})", remote_topgrade), || {
runner.execute(Step::Remotes, format!("Remote ({remote_topgrade})"), || {
remote::ssh::ssh_step(&ctx, remote_topgrade)
})?;
}
@@ -147,7 +174,7 @@ fn run() -> Result<()> {
runner.execute(Step::System, "System update", || distribution.upgrade(&ctx))?;
}
Err(e) => {
println!("Error detecting current distribution: {}", e);
println!("Error detecting current distribution: {e}");
}
}
runner.execute(Step::ConfigUpdate, "config-update", || linux::run_config_update(&ctx))?;
@@ -201,17 +228,17 @@ fn run() -> Result<()> {
#[cfg(target_os = "dragonfly")]
runner.execute(Step::Pkg, "DragonFly BSD Packages", || {
dragonfly::upgrade_packages(sudo.as_ref(), run_type)
dragonfly::upgrade_packages(ctx.sudo().as_ref(), run_type)
})?;
#[cfg(target_os = "freebsd")]
runner.execute(Step::Pkg, "FreeBSD Packages", || {
freebsd::upgrade_packages(sudo.as_ref(), run_type)
freebsd::upgrade_packages(&ctx, ctx.sudo().as_ref(), run_type)
})?;
#[cfg(target_os = "openbsd")]
runner.execute(Step::Pkg, "OpenBSD Packages", || {
openbsd::upgrade_packages(sudo.as_ref(), run_type)
openbsd::upgrade_packages(ctx.sudo().as_ref(), run_type)
})?;
#[cfg(target_os = "android")]
@@ -296,7 +323,7 @@ fn run() -> Result<()> {
runner.execute(Step::Shell, "zi", || zsh::run_zi(&base_dirs, run_type))?;
runner.execute(Step::Shell, "zim", || zsh::run_zim(&base_dirs, run_type))?;
runner.execute(Step::Shell, "oh-my-zsh", || zsh::run_oh_my_zsh(&ctx))?;
runner.execute(Step::Shell, "fisher", || unix::run_fisher(&base_dirs, run_type))?;
runner.execute(Step::Shell, "fisher", || unix::run_fisher(run_type))?;
runner.execute(Step::Shell, "bash-it", || unix::run_bashit(&ctx))?;
runner.execute(Step::Shell, "oh-my-fish", || unix::run_oh_my_fish(&ctx))?;
runner.execute(Step::Shell, "fish-plug", || unix::run_fish_plug(&ctx))?;
@@ -323,17 +350,21 @@ fn run() -> Result<()> {
runner.execute(Step::Atom, "apm", || generic::run_apm(run_type))?;
runner.execute(Step::Fossil, "fossil", || generic::run_fossil(run_type))?;
runner.execute(Step::Rustup, "rustup", || generic::run_rustup(&base_dirs, run_type))?;
runner.execute(Step::Juliaup, "juliaup", || generic::run_juliaup(&base_dirs, run_type))?;
runner.execute(Step::Dotnet, ".NET", || generic::run_dotnet_upgrade(&ctx))?;
runner.execute(Step::Choosenim, "choosenim", || generic::run_choosenim(&ctx))?;
runner.execute(Step::Cargo, "cargo", || generic::run_cargo_update(&ctx))?;
runner.execute(Step::Flutter, "Flutter", || generic::run_flutter_upgrade(run_type))?;
runner.execute(Step::Go, "Go", || generic::run_go(run_type))?;
runner.execute(Step::Go, "go-global-update", || go::run_go_global_update(run_type))?;
runner.execute(Step::Go, "gup", || go::run_go_gup(run_type))?;
runner.execute(Step::Emacs, "Emacs", || emacs.upgrade(&ctx))?;
runner.execute(Step::Opam, "opam", || generic::run_opam_update(&ctx))?;
runner.execute(Step::Vcpkg, "vcpkg", || generic::run_vcpkg_update(run_type))?;
runner.execute(Step::Vcpkg, "vcpkg", || generic::run_vcpkg_update(&ctx))?;
runner.execute(Step::Pipx, "pipx", || generic::run_pipx_update(run_type))?;
runner.execute(Step::Conda, "conda", || generic::run_conda_update(&ctx))?;
runner.execute(Step::Pip3, "pip3", || generic::run_pip3_update(run_type))?;
runner.execute(Step::PipReview, "pip-review", || generic::run_pip_review_update(&ctx))?;
runner.execute(Step::Pipupgrade, "pipupgrade", || generic::run_pipupgrade_update(&ctx))?;
runner.execute(Step::Ghcup, "ghcup", || generic::run_ghcup_update(run_type))?;
runner.execute(Step::Stack, "stack", || generic::run_stack_update(run_type))?;
runner.execute(Step::Tlmgr, "tlmgr", || generic::run_tlmgr_update(&ctx))?;
@@ -349,14 +380,17 @@ fn run() -> Result<()> {
runner.execute(Step::Vim, "The Ultimate vimrc", || vim::upgrade_ultimate_vimrc(&ctx))?;
runner.execute(Step::Vim, "voom", || vim::run_voom(&base_dirs, run_type))?;
runner.execute(Step::Kakoune, "Kakoune", || kakoune::upgrade_kak_plug(&ctx))?;
runner.execute(Step::Helix, "helix", || generic::run_helix_grammars(&ctx))?;
runner.execute(Step::Node, "npm", || node::run_npm_upgrade(&ctx))?;
runner.execute(Step::Node, "yarn", || node::run_yarn_upgrade(&ctx))?;
runner.execute(Step::Node, "pnpm", || node::run_pnpm_upgrade(&ctx))?;
runner.execute(Step::Yarn, "yarn", || node::run_yarn_upgrade(&ctx))?;
runner.execute(Step::Pnpm, "pnpm", || node::run_pnpm_upgrade(&ctx))?;
runner.execute(Step::Containers, "Containers", || containers::run_containers(&ctx))?;
runner.execute(Step::Deno, "deno", || node::deno_upgrade(&ctx))?;
runner.execute(Step::Composer, "composer", || generic::run_composer_update(&ctx))?;
runner.execute(Step::Krew, "krew", || generic::run_krew_upgrade(run_type))?;
runner.execute(Step::Helm, "helm", || generic::run_helm_repo_update(run_type))?;
runner.execute(Step::Gem, "gem", || generic::run_gem(&base_dirs, run_type))?;
runner.execute(Step::RubyGems, "rubygems", || generic::run_rubygems(&ctx))?;
runner.execute(Step::Julia, "julia", || generic::update_julia_packages(&ctx))?;
runner.execute(Step::Haxelib, "haxelib", || generic::run_haxelib_update(&ctx))?;
runner.execute(Step::Sheldon, "sheldon", || generic::run_sheldon(&ctx))?;
@@ -374,14 +408,16 @@ fn run() -> Result<()> {
#[cfg(target_os = "linux")]
{
runner.execute(Step::AM, "am", || linux::update_am(&ctx))?;
runner.execute(Step::DebGet, "deb-get", || linux::run_deb_get(&ctx))?;
runner.execute(Step::Toolbx, "toolbx", || toolbx::run_toolbx(&ctx))?;
runner.execute(Step::Flatpak, "Flatpak", || linux::flatpak_update(&ctx))?;
runner.execute(Step::Snap, "snap", || linux::run_snap(sudo.as_ref(), run_type))?;
runner.execute(Step::Snap, "snap", || linux::run_snap(ctx.sudo().as_ref(), run_type))?;
runner.execute(Step::Pacstall, "pacstall", || linux::run_pacstall(&ctx))?;
runner.execute(Step::Pacdef, "pacdef", || linux::run_pacdef(&ctx))?;
runner.execute(Step::Protonup, "protonup", || linux::run_protonup_update(&ctx))?;
runner.execute(Step::Distrobox, "distrobox", || linux::run_distrobox_update(&ctx))?;
runner.execute(Step::DkpPacman, "dkp-pacman", || linux::run_dkp_pacman_update(&ctx))?;
}
if let Some(commands) = config.commands() {
@@ -397,11 +433,11 @@ fn run() -> Result<()> {
#[cfg(target_os = "linux")]
{
runner.execute(Step::System, "pihole", || {
linux::run_pihole_update(sudo.as_ref(), run_type)
linux::run_pihole_update(ctx.sudo().as_ref(), run_type)
})?;
runner.execute(Step::Firmware, "Firmware upgrades", || linux::run_fwupdmgr(&ctx))?;
runner.execute(Step::Restarts, "Restarts", || {
linux::run_needrestart(sudo.as_ref(), run_type)
linux::run_needrestart(ctx.sudo().as_ref(), run_type)
})?;
}
@@ -414,12 +450,12 @@ fn run() -> Result<()> {
#[cfg(target_os = "freebsd")]
runner.execute(Step::System, "FreeBSD Upgrade", || {
freebsd::upgrade_freebsd(sudo.as_ref(), run_type)
freebsd::upgrade_freebsd(ctx.sudo().as_ref(), run_type)
})?;
#[cfg(target_os = "openbsd")]
runner.execute(Step::System, "OpenBSD Upgrade", || {
openbsd::upgrade_openbsd(sudo.as_ref(), run_type)
openbsd::upgrade_openbsd(ctx.sudo().as_ref(), run_type)
})?;
#[cfg(windows)]
@@ -451,10 +487,10 @@ fn run() -> Result<()> {
}
#[cfg(target_os = "freebsd")]
freebsd::audit_packages(&sudo).ok();
freebsd::audit_packages(ctx.sudo().as_ref()).ok();
#[cfg(target_os = "dragonfly")]
dragonfly::audit_packages(&sudo).ok();
dragonfly::audit_packages(ctx.sudo().as_ref()).ok();
}
let mut post_command_failed = false;
@@ -471,10 +507,10 @@ fn run() -> Result<()> {
loop {
match get_key() {
Ok(Key::Char('s')) | Ok(Key::Char('S')) => {
run_shell();
run_shell().context("Failed to execute shell")?;
}
Ok(Key::Char('r')) | Ok(Key::Char('R')) => {
reboot();
reboot().context("Failed to reboot")?;
}
Ok(Key::Char('q')) | Ok(Key::Char('Q')) => (),
_ => {
@@ -493,8 +529,8 @@ fn run() -> Result<()> {
"Topgrade finished {}",
if failed { "with errors" } else { "successfully" }
),
None,
);
Some(Duration::from_secs(10)),
)
}
if failed {
@@ -524,12 +560,35 @@ fn main() {
.is_some());
if !skip_print {
// The `Debug` implementation of `anyhow::Result` prints a multi-line
// The `Debug` implementation of `eyre::Result` prints a multi-line
// error message that includes all the 'causes' added with
// `.with_context(...)` calls.
println!("Error: {:?}", error);
println!("Error: {error:?}");
}
exit(1);
}
}
}
pub fn install_tracing(filter_directives: &str) -> Result<()> {
use tracing_subscriber::fmt;
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
let env_filter = EnvFilter::try_new(filter_directives)
.or_else(|_| EnvFilter::try_from_default_env())
.or_else(|_| EnvFilter::try_new("info"))?;
let fmt_layer = fmt::layer()
.with_target(false)
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
.without_time();
let registry = tracing_subscriber::registry();
registry.with(env_filter).with(fmt_layer).init();
Ok(())
}

View File

@@ -34,7 +34,7 @@ impl<'a> Report<'a> {
if let Some((key, success)) = result {
let key = key.into();
debug_assert!(!self.data.iter().any(|(k, _)| k == &key), "{} already reported", key);
debug_assert!(!self.data.iter().any(|(k, _)| k == &key), "{key} already reported");
self.data.push((key, success));
}
}

View File

@@ -2,11 +2,12 @@ use crate::ctrlc;
use crate::error::{DryRun, SkipStep};
use crate::execution_context::ExecutionContext;
use crate::report::{Report, StepResult};
use crate::terminal::print_error;
use crate::{config::Step, terminal::should_retry};
use anyhow::Result;
use log::debug;
use color_eyre::eyre::Result;
use std::borrow::Cow;
use std::fmt::Debug;
use tracing::debug;
pub struct Runner<'a> {
ctx: &'a ExecutionContext<'a>,
@@ -55,7 +56,12 @@ impl<'a> Runner<'a> {
let ignore_failure = self.ctx.config().ignore_failure(step);
let should_ask = interrupted || !(self.ctx.config().no_retry() || ignore_failure);
let should_retry = should_ask && should_retry(interrupted, key.as_ref())?;
let should_retry = if should_ask {
print_error(&key, format!("{e:?}"));
should_retry(interrupted, key.as_ref())?
} else {
false
};
if !should_retry {
self.report.push_result(Some((

View File

@@ -1,8 +1,8 @@
#![cfg(windows)]
use anyhow::Result;
use log::{debug, error};
use color_eyre::eyre::Result;
use std::{env::current_exe, fs, path::PathBuf};
use tracing::{debug, error};
pub struct SelfRenamer {
exe_path: PathBuf,

View File

@@ -1,13 +1,15 @@
use std::env;
#[cfg(unix)]
use std::os::unix::process::CommandExt as _;
use std::process::Command;
use color_eyre::eyre::{bail, Result};
use self_update_crate::backends::github::Update;
use self_update_crate::update::UpdateStatus;
use super::terminal::*;
#[cfg(windows)]
use crate::error::Upgraded;
use anyhow::{bail, Result};
use self_update_crate::backends::github::Update;
use self_update_crate::update::UpdateStatus;
use std::env;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::process::Command;
pub fn self_update() -> Result<()> {
print_separator("Self update");
@@ -18,11 +20,7 @@ pub fn self_update() -> Result<()> {
.repo_owner("topgrade-rs")
.repo_name("topgrade")
.target(target)
.bin_name(if cfg!(windows) {
"topgrade-rs.exe"
} else {
"topgrade-rs"
})
.bin_name(if cfg!(windows) { "topgrade.exe" } else { "topgrade" })
.show_output(false)
.show_download_progress(true)
.current_version(self_update_crate::cargo_crate_version!())
@@ -33,7 +31,7 @@ pub fn self_update() -> Result<()> {
if let UpdateStatus::Updated(release) = &result {
println!("\nTopgrade upgraded to {}:\n", release.version);
if let Some(body) = &release.body {
println!("{}", body);
println!("{body}");
}
} else {
println!("Topgrade is up-to-date");
@@ -53,7 +51,8 @@ pub fn self_update() -> Result<()> {
#[cfg(windows)]
{
let status = command.spawn().and_then(|mut c| c.wait())?;
#[allow(clippy::disallowed_methods)]
let status = command.status()?;
bail!(Upgraded(status));
}
}

View File

@@ -1,13 +1,16 @@
use anyhow::Result;
use crate::error::{self, TopgradeError};
use crate::executor::CommandExt;
use crate::terminal::print_separator;
use crate::{execution_context::ExecutionContext, utils::require};
use log::{debug, error, warn};
use std::path::Path;
use std::process::Command;
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use tracing::{debug, error, warn};
use crate::command::CommandExt;
use crate::error::{self, TopgradeError};
use crate::terminal::print_separator;
use crate::{execution_context::ExecutionContext, utils::require};
// A string found in the output of docker for containers that weren't found in
// the docker registry. We use this to gracefully handle and skip containers
// that cannot be pulled, likely because they don't exist in the registry in
@@ -24,11 +27,10 @@ fn list_containers(crt: &Path) -> Result<Vec<String>> {
);
let output = Command::new(crt)
.args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}}"])
.output()?;
let output_str = String::from_utf8(output.stdout)?;
.output_checked_with_utf8(|_| Ok(()))?;
let mut retval = vec![];
for line in output_str.lines() {
for line in output.stdout.lines() {
if line.starts_with("localhost") {
// Don't know how to update self-built containers
debug!("Skipping self-built container '{}'", line);
@@ -60,7 +62,7 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
print_separator("Containers");
let mut success = true;
let containers = list_containers(&crt)?;
let containers = list_containers(&crt).context("Failed to list Docker containers")?;
debug!("Containers to inspect: {:?}", containers);
for container in containers.iter() {
@@ -68,7 +70,7 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
let args = vec!["pull", &container[..]];
let mut exec = ctx.run_type().execute(&crt);
if let Err(e) = exec.args(&args).check_run() {
if let Err(e) = exec.args(&args).status_checked() {
error!("Pulling container '{}' failed: {}", container, e);
// Find out if this is 'skippable'
@@ -77,10 +79,10 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
// practical consequence that all containers, whether self-built, created by
// docker-compose or pulled from the docker hub, look exactly the same to us. We can
// only find out what went wrong by manually parsing the output of the command...
if match exec.check_output() {
Ok(s) => s.contains(NONEXISTENT_REPO),
if match exec.output_checked_utf8() {
Ok(s) => s.stdout.contains(NONEXISTENT_REPO) || s.stderr.contains(NONEXISTENT_REPO),
Err(e) => match e.downcast_ref::<TopgradeError>() {
Some(TopgradeError::ProcessFailedWithOutput(_, stderr)) => stderr.contains(NONEXISTENT_REPO),
Some(TopgradeError::ProcessFailedWithOutput(_, _, stderr)) => stderr.contains(NONEXISTENT_REPO),
_ => false,
},
} {
@@ -95,7 +97,12 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
if ctx.config().cleanup() {
// Remove dangling images
debug!("Removing dangling images");
if let Err(e) = ctx.run_type().execute(&crt).args(["image", "prune", "-f"]).check_run() {
if let Err(e) = ctx
.run_type()
.execute(&crt)
.args(["image", "prune", "-f"])
.status_checked()
{
error!("Removing dangling images failed: {}", e);
success = false;
}
@@ -104,6 +111,6 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
if success {
Ok(())
} else {
Err(anyhow::anyhow!(error::StepFailed))
Err(eyre!(error::StepFailed))
}
}

View File

@@ -2,9 +2,10 @@
use std::env;
use std::path::{Path, PathBuf};
use anyhow::Result;
use color_eyre::eyre::Result;
use directories::BaseDirs;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::terminal::print_separator;
use crate::utils::{require, require_option, PathExt};
@@ -73,7 +74,7 @@ impl Emacs {
command.args(["upgrade"]);
command.check_run()
command.status_checked()
}
pub fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
@@ -105,6 +106,6 @@ impl Emacs {
#[cfg(not(unix))]
command.arg(EMACS_UPGRADE);
command.check_run()
command.status_checked()
}
}

View File

@@ -5,17 +5,20 @@ use std::process::Command;
use std::{env, path::Path};
use std::{fs, io::Write};
use anyhow::Result;
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use directories::BaseDirs;
use log::debug;
use tempfile::tempfile_in;
use tracing::{debug, error};
use crate::command::{CommandExt, Utf8Output};
use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, ExecutorOutput, RunType};
use crate::executor::{ExecutorOutput, RunType};
use crate::terminal::{print_separator, shell};
use crate::utils::{self, require_option, PathExt};
use crate::utils::{self, require, require_option, which, PathExt};
use crate::{
error::{SkipStep, TopgradeError},
error::{SkipStep, StepFailed, TopgradeError},
terminal::print_warning,
};
@@ -53,35 +56,21 @@ pub fn run_cargo_update(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type()
.execute(cargo_update)
.args(["install-update", "--git", "--all"])
.check_run()
.status_checked()
}
pub fn run_flutter_upgrade(run_type: RunType) -> Result<()> {
let flutter = utils::require("flutter")?;
print_separator("Flutter");
run_type.execute(flutter).arg("upgrade").check_run()
}
pub fn run_go(run_type: RunType) -> Result<()> {
let go = utils::require("go")?;
let go_output = run_type.execute(go).args(["env", "GOPATH"]).check_output()?;
let gopath = go_output.trim();
let go_global_update = utils::require("go-global-update")
.unwrap_or_else(|_| PathBuf::from(gopath).join("bin/go-global-update"))
.require()?;
print_separator("Go");
run_type.execute(go_global_update).check_run()
run_type.execute(flutter).arg("upgrade").status_checked()
}
pub fn run_gem(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let gem = utils::require("gem")?;
base_dirs.home_dir().join(".gem").require()?;
print_separator("RubyGems");
print_separator("Gems");
let mut command = run_type.execute(gem);
command.arg("update");
@@ -91,14 +80,35 @@ pub fn run_gem(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
command.arg("--user-install");
}
command.check_run()
command.status_checked()
}
pub fn run_rubygems(ctx: &ExecutionContext) -> Result<()> {
ctx.base_dirs().home_dir().join(".gem").require()?;
print_separator("RubyGems");
if let Some(sudo) = &ctx.sudo() {
if !std::path::Path::new("/usr/lib/ruby/vendor_ruby/rubygems/defaults/operating_system.rb").exists() {
ctx.run_type()
.execute(sudo)
.arg("-EH")
.arg(require("gem")?)
.args(["update", "--system"])
.status_checked()?;
}
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
pub fn run_haxelib_update(ctx: &ExecutionContext) -> Result<()> {
let haxelib = utils::require("haxelib")?;
let haxelib_dir =
PathBuf::from(std::str::from_utf8(&Command::new(&haxelib).arg("config").output()?.stdout)?.trim()).require()?;
PathBuf::from(std::str::from_utf8(&Command::new(&haxelib).arg("config").output_checked()?.stdout)?.trim())
.require()?;
let directory_writable = tempfile_in(&haxelib_dir).is_ok();
debug!("{:?} writable: {}", haxelib_dir, directory_writable);
@@ -115,7 +125,7 @@ pub fn run_haxelib_update(ctx: &ExecutionContext) -> Result<()> {
c
};
command.arg("update").check_run()
command.arg("update").status_checked()
}
pub fn run_sheldon(ctx: &ExecutionContext) -> Result<()> {
@@ -123,7 +133,10 @@ pub fn run_sheldon(ctx: &ExecutionContext) -> Result<()> {
print_separator("Sheldon");
ctx.run_type().execute(sheldon).args(["lock", "--update"]).check_run()
ctx.run_type()
.execute(sheldon)
.args(["lock", "--update"])
.status_checked()
}
pub fn run_fossil(run_type: RunType) -> Result<()> {
@@ -131,7 +144,7 @@ pub fn run_fossil(run_type: RunType) -> Result<()> {
print_separator("Fossil");
run_type.execute(fossil).args(["all", "sync"]).check_run()
run_type.execute(fossil).args(["all", "sync"]).status_checked()
}
pub fn run_micro(run_type: RunType) -> Result<()> {
@@ -139,13 +152,17 @@ pub fn run_micro(run_type: RunType) -> Result<()> {
print_separator("micro");
let stdout = run_type.execute(micro).args(["-plugin", "update"]).string_output()?;
let stdout = run_type
.execute(micro)
.args(["-plugin", "update"])
.output_checked_utf8()?
.stdout;
std::io::stdout().write_all(stdout.as_bytes())?;
if stdout.contains("Nothing to install / update") || stdout.contains("One or more plugins installed") {
Ok(())
} else {
Err(anyhow::anyhow!("micro output does not indicate success: {}", stdout))
Err(eyre!("micro output does not indicate success: {}", stdout))
}
}
@@ -160,7 +177,10 @@ pub fn run_apm(run_type: RunType) -> Result<()> {
print_separator("Atom Package Manager");
run_type.execute(apm).args(["upgrade", "--confirm=false"]).check_run()
run_type
.execute(apm)
.args(["upgrade", "--confirm=false"])
.status_checked()
}
pub fn run_rustup(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
@@ -169,10 +189,22 @@ pub fn run_rustup(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
print_separator("rustup");
if rustup.canonicalize()?.is_descendant_of(base_dirs.home_dir()) {
run_type.execute(&rustup).args(["self", "update"]).check_run()?;
run_type.execute(&rustup).args(["self", "update"]).status_checked()?;
}
run_type.execute(&rustup).arg("update").check_run()
run_type.execute(&rustup).arg("update").status_checked()
}
pub fn run_juliaup(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let juliaup = utils::require("juliaup")?;
print_separator("juliaup");
if juliaup.canonicalize()?.is_descendant_of(base_dirs.home_dir()) {
run_type.execute(&juliaup).args(["self", "update"]).status_checked()?;
}
run_type.execute(&juliaup).arg("update").status_checked()
}
pub fn run_choosenim(ctx: &ExecutionContext) -> Result<()> {
@@ -181,8 +213,8 @@ pub fn run_choosenim(ctx: &ExecutionContext) -> Result<()> {
print_separator("choosenim");
let run_type = ctx.run_type();
run_type.execute(&choosenim).args(["update", "self"]).check_run()?;
run_type.execute(&choosenim).args(["update", "stable"]).check_run()
run_type.execute(&choosenim).args(["update", "self"]).status_checked()?;
run_type.execute(&choosenim).args(["update", "stable"]).status_checked()
}
pub fn run_krew_upgrade(run_type: RunType) -> Result<()> {
@@ -190,7 +222,7 @@ pub fn run_krew_upgrade(run_type: RunType) -> Result<()> {
print_separator("Krew");
run_type.execute(krew).args(["upgrade"]).check_run()
run_type.execute(krew).args(["upgrade"]).status_checked()
}
pub fn run_gcloud_components_update(run_type: RunType) -> Result<()> {
@@ -204,7 +236,7 @@ pub fn run_gcloud_components_update(run_type: RunType) -> Result<()> {
run_type
.execute(gcloud)
.args(["components", "update", "--quiet"])
.check_run()
.status_checked()
}
}
@@ -213,7 +245,7 @@ pub fn run_jetpack(run_type: RunType) -> Result<()> {
print_separator("Jetpack");
run_type.execute(jetpack).args(["global", "update"]).check_run()
run_type.execute(jetpack).args(["global", "update"]).status_checked()
}
pub fn run_rtcl(ctx: &ExecutionContext) -> Result<()> {
@@ -221,7 +253,7 @@ pub fn run_rtcl(ctx: &ExecutionContext) -> Result<()> {
print_separator("rtcl");
ctx.run_type().execute(rupdate).check_run()
ctx.run_type().execute(rupdate).status_checked()
}
pub fn run_opam_update(ctx: &ExecutionContext) -> Result<()> {
@@ -229,28 +261,44 @@ pub fn run_opam_update(ctx: &ExecutionContext) -> Result<()> {
print_separator("OCaml Package Manager");
ctx.run_type().execute(&opam).arg("update").check_run()?;
ctx.run_type().execute(&opam).arg("upgrade").check_run()?;
ctx.run_type().execute(&opam).arg("update").status_checked()?;
ctx.run_type().execute(&opam).arg("upgrade").status_checked()?;
if ctx.config().cleanup() {
ctx.run_type().execute(&opam).arg("clean").check_run()?;
ctx.run_type().execute(&opam).arg("clean").status_checked()?;
}
Ok(())
}
pub fn run_vcpkg_update(run_type: RunType) -> Result<()> {
pub fn run_vcpkg_update(ctx: &ExecutionContext) -> Result<()> {
let vcpkg = utils::require("vcpkg")?;
print_separator("vcpkg");
run_type.execute(vcpkg).args(["upgrade", "--no-dry-run"]).check_run()
#[cfg(unix)]
let is_root_install = !&vcpkg.starts_with("/home");
#[cfg(not(unix))]
let is_root_install = false;
let mut command = if is_root_install {
ctx.run_type().execute(&vcpkg)
} else {
let mut c = ctx
.run_type()
.execute(ctx.sudo().as_ref().ok_or(TopgradeError::SudoRequired)?);
c.arg(&vcpkg);
c
};
command.args(["upgrade", "--no-dry-run"]).status_checked()
}
pub fn run_pipx_update(run_type: RunType) -> Result<()> {
let pipx = utils::require("pipx")?;
print_separator("pipx");
run_type.execute(pipx).arg("upgrade-all").check_run()
run_type.execute(pipx).arg("upgrade-all").status_checked()
}
pub fn run_conda_update(ctx: &ExecutionContext) -> Result<()> {
@@ -258,10 +306,9 @@ pub fn run_conda_update(ctx: &ExecutionContext) -> Result<()> {
let output = Command::new("conda")
.args(["config", "--show", "auto_activate_base"])
.output()?;
let string_output = String::from_utf8(output.stdout)?;
debug!("Conda output: {}", string_output);
if string_output.contains("False") {
.output_checked_utf8()?;
debug!("Conda output: {}", output.stdout);
if output.stdout.contains("False") {
return Err(SkipStep("auto_activate_base is set to False".to_string()).into());
}
@@ -270,14 +317,14 @@ pub fn run_conda_update(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type()
.execute(conda)
.args(["update", "--all", "-y"])
.check_run()
.status_checked()
}
pub fn run_pip3_update(run_type: RunType) -> Result<()> {
let python3 = utils::require("python3")?;
Command::new(&python3)
.args(["-m", "pip"])
.check_output()
.output_checked_utf8()
.map_err(|_| SkipStep("pip does not exists".to_string()))?;
print_separator("pip3");
@@ -289,7 +336,40 @@ pub fn run_pip3_update(run_type: RunType) -> Result<()> {
run_type
.execute(&python3)
.args(["-m", "pip", "install", "--upgrade", "--user", "pip"])
.check_run()
.status_checked()
}
pub fn run_pip_review_update(ctx: &ExecutionContext) -> Result<()> {
let pip_review = require("pip-review")?;
print_separator("pip-review");
if !ctx.config().enable_pip_review() {
print_warning(
"Pip-review is disabled by default. Enable it by setting enable_pip_review=true in the configuration.",
);
return Err(SkipStep(String::from("Pip-review is disabled by default")).into());
}
ctx.run_type()
.execute(pip_review)
.arg("--auto")
.status_checked_with_codes(&[1])?;
Ok(())
}
pub fn run_pipupgrade_update(ctx: &ExecutionContext) -> Result<()> {
let pipupgrade = require("pipupgrade")?;
print_separator("Pipupgrade");
if !ctx.config().enable_pip_review() {
print_warning(
"Pipupgrade is disabled by default. Enable it by setting enable_pipupgrade=true in the configuration.",
);
return Err(SkipStep(String::from("Pipupgrade is disabled by default")).into());
}
ctx.run_type().execute(pipupgrade).status_checked()?;
Ok(())
}
pub fn run_stack_update(run_type: RunType) -> Result<()> {
@@ -303,14 +383,14 @@ pub fn run_stack_update(run_type: RunType) -> Result<()> {
let stack = utils::require("stack")?;
print_separator("stack");
run_type.execute(stack).arg("upgrade").check_run()
run_type.execute(stack).arg("upgrade").status_checked()
}
pub fn run_ghcup_update(run_type: RunType) -> Result<()> {
let ghcup = utils::require("ghcup")?;
print_separator("ghcup");
run_type.execute(ghcup).arg("upgrade").check_run()
run_type.execute(ghcup).arg("upgrade").status_checked()
}
pub fn run_tlmgr_update(ctx: &ExecutionContext) -> Result<()> {
@@ -326,12 +406,10 @@ pub fn run_tlmgr_update(ctx: &ExecutionContext) -> Result<()> {
let kpsewhich = utils::require("kpsewhich")?;
let tlmgr_directory = {
let mut d = PathBuf::from(
std::str::from_utf8(
&Command::new(kpsewhich)
.arg("-var-value=SELFAUTOPARENT")
.output()?
.stdout,
)?
.output_checked_utf8()?
.stdout
.trim(),
);
d.push("tlpkg");
@@ -355,7 +433,7 @@ pub fn run_tlmgr_update(ctx: &ExecutionContext) -> Result<()> {
};
command.args(["update", "--self", "--all"]);
command.check_run()
command.status_checked()
}
pub fn run_chezmoi_update(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
@@ -364,7 +442,7 @@ pub fn run_chezmoi_update(base_dirs: &BaseDirs, run_type: RunType) -> Result<()>
print_separator("chezmoi");
run_type.execute(chezmoi).arg("update").check_run()
run_type.execute(chezmoi).arg("update").status_checked()
}
pub fn run_myrepos_update(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
@@ -378,27 +456,27 @@ pub fn run_myrepos_update(base_dirs: &BaseDirs, run_type: RunType) -> Result<()>
.arg("--directory")
.arg(base_dirs.home_dir())
.arg("checkout")
.check_run()?;
.status_checked()?;
run_type
.execute(&myrepos)
.arg("--directory")
.arg(base_dirs.home_dir())
.arg("update")
.check_run()
.status_checked()
}
pub fn run_custom_command(name: &str, command: &str, ctx: &ExecutionContext) -> Result<()> {
print_separator(name);
ctx.run_type().execute(shell()).arg("-c").arg(command).check_run()
ctx.run_type().execute(shell()).arg("-c").arg(command).status_checked()
}
pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> {
let composer = utils::require("composer")?;
let composer_home = Command::new(&composer)
.args(["global", "config", "--absolute", "--quiet", "home"])
.check_output()
.map_err(|e| (SkipStep(format!("Error getting the composer directory: {}", e))))
.map(|s| PathBuf::from(s.trim()))?
.output_checked_utf8()
.map_err(|e| (SkipStep(format!("Error getting the composer directory: {e}"))))
.map(|s| PathBuf::from(s.stdout.trim()))?
.require()?;
if !composer_home.is_descendant_of(ctx.base_dirs().home_dir()) {
@@ -425,26 +503,22 @@ pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> {
.execute(ctx.sudo().as_ref().unwrap())
.arg(&composer)
.arg("self-update")
.check_run()?;
.status_checked()?;
}
} else {
ctx.run_type().execute(&composer).arg("self-update").check_run()?;
ctx.run_type().execute(&composer).arg("self-update").status_checked()?;
}
}
}
let output = Command::new(&composer).args(["global", "update"]).output()?;
let status = output.status;
if !status.success() {
return Err(TopgradeError::ProcessFailed(status).into());
}
let stdout = String::from_utf8(output.stdout)?;
let stderr = String::from_utf8(output.stderr)?;
print!("{}\n{}", stdout, stderr);
if stdout.contains("valet") || stderr.contains("valet") {
let output = ctx.run_type().execute(&composer).args(["global", "update"]).output()?;
if let ExecutorOutput::Wet(output) = output {
let output: Utf8Output = output.try_into()?;
print!("{}\n{}", output.stdout, output.stderr);
if output.stdout.contains("valet") || output.stderr.contains("valet") {
if let Some(valet) = utils::which("valet") {
ctx.run_type().execute(valet).arg("install").check_run()?;
ctx.run_type().execute(valet).arg("install").status_checked()?;
}
}
}
@@ -454,18 +528,27 @@ pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> {
pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> {
let dotnet = utils::require("dotnet")?;
let output = Command::new(dotnet).args(["tool", "list", "--global"]).output()?;
if !output.status.success() {
return Err(SkipStep(format!("dotnet failed with exit code {:?}", output.status)).into());
//Skip when the `dotnet tool list` subcommand fails. (This is expected when a dotnet runtime is installed but no SDK.)
let output = match ctx
.run_type()
.execute(&dotnet)
.args(["tool", "list", "--global"])
.output_checked_utf8()
{
Ok(output) => output,
Err(_) => {
return Err(SkipStep(String::from(
"Error running `dotnet tool list`. This is expected when a dotnet runtime is installed but no SDK.",
))
.into())
}
};
let output = String::from_utf8(output.stdout)?;
if !output.starts_with("Package Id") {
if !output.stdout.starts_with("Package Id") {
return Err(SkipStep(String::from("dotnet did not output packages")).into());
}
let mut packages = output.split('\n').skip(2).filter(|line| !line.is_empty()).peekable();
let mut packages = output.stdout.lines().skip(2).filter(|line| !line.is_empty()).peekable();
if packages.peek().is_none() {
return Err(SkipStep(String::from("No dotnet global tools installed")).into());
@@ -476,39 +559,60 @@ pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> {
for package in packages {
let package_name = package.split_whitespace().next().unwrap();
ctx.run_type()
.execute("dotnet")
.execute(&dotnet)
.args(["tool", "update", package_name, "--global"])
.check_run()?;
.status_checked()
.with_context(|| format!("Failed to update .NET package {package_name}"))?;
}
Ok(())
}
pub fn run_helix_grammars(ctx: &ExecutionContext) -> Result<()> {
utils::require("helix")?;
print_separator("Helix");
ctx.run_type()
.execute(ctx.sudo().as_ref().ok_or(TopgradeError::SudoRequired)?)
.args(["helix", "--grammar", "fetch"])
.status_checked()
.with_context(|| "Failed to download helix grammars!")?;
ctx.run_type()
.execute(ctx.sudo().as_ref().ok_or(TopgradeError::SudoRequired)?)
.args(["helix", "--grammar", "build"])
.status_checked()
.with_context(|| "Failed to build helix grammars!")?;
Ok(())
}
pub fn run_raco_update(run_type: RunType) -> Result<()> {
let raco = utils::require("raco")?;
print_separator("Racket Package Manager");
run_type.execute(raco).args(["pkg", "update", "--all"]).check_run()
run_type.execute(raco).args(["pkg", "update", "--all"]).status_checked()
}
pub fn bin_update(ctx: &ExecutionContext) -> Result<()> {
let bin = utils::require("bin")?;
print_separator("Bin");
ctx.run_type().execute(bin).arg("update").check_run()
ctx.run_type().execute(bin).arg("update").status_checked()
}
pub fn spicetify_upgrade(ctx: &ExecutionContext) -> Result<()> {
let spicetify = utils::require("spicetify")?;
print_separator("Spicetify");
ctx.run_type().execute(spicetify).arg("upgrade").check_run()
ctx.run_type().execute(spicetify).arg("upgrade").status_checked()
}
pub fn run_ghcli_extensions_upgrade(ctx: &ExecutionContext) -> Result<()> {
let gh = utils::require("gh")?;
let result = Command::new(&gh).args(["extensions", "list"]).check_output();
let result = Command::new(&gh).args(["extensions", "list"]).output_checked_utf8();
if result.is_err() {
debug!("GH result {:?}", result);
return Err(SkipStep(String::from("GH failed")).into());
@@ -518,7 +622,7 @@ pub fn run_ghcli_extensions_upgrade(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type()
.execute(&gh)
.args(["extension", "upgrade", "--all"])
.check_run()
.status_checked()
}
pub fn update_julia_packages(ctx: &ExecutionContext) -> Result<()> {
@@ -529,5 +633,31 @@ pub fn update_julia_packages(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type()
.execute(julia)
.args(["-e", "using Pkg; Pkg.update()"])
.check_run()
.status_checked()
}
pub fn run_helm_repo_update(run_type: RunType) -> Result<()> {
let helm = utils::require("helm")?;
print_separator("Helm");
let no_repo = "no repositories found";
let mut success = true;
let mut exec = run_type.execute(helm);
if let Err(e) = exec.arg("repo").arg("update").status_checked() {
error!("Updating repositories failed: {}", e);
success = match exec.output_checked_utf8() {
Ok(s) => s.stdout.contains(no_repo) || s.stderr.contains(no_repo),
Err(e) => match e.downcast_ref::<TopgradeError>() {
Some(TopgradeError::ProcessFailedWithOutput(_, _, stderr)) => stderr.contains(no_repo),
_ => false,
},
};
}
if success {
Ok(())
} else {
Err(eyre!(StepFailed))
}
}

View File

@@ -3,17 +3,18 @@ use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use anyhow::{anyhow, Result};
use color_eyre::eyre::{eyre, Result};
use console::style;
use futures::stream::{iter, FuturesUnordered};
use futures::StreamExt;
use glob::{glob_with, MatchOptions};
use log::{debug, error};
use tokio::process::Command as AsyncCommand;
use tokio::runtime;
use tracing::{debug, error};
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, RunType};
use crate::executor::RunType;
use crate::terminal::print_separator;
use crate::utils::{which, PathExt};
use crate::{error::SkipStep, terminal::print_warning};
@@ -33,10 +34,10 @@ pub struct Repositories<'a> {
bad_patterns: Vec<String>,
}
fn check_output(output: Output) -> Result<()> {
fn output_checked_utf8(output: Output) -> Result<()> {
if !(output.status.success()) {
let stderr = String::from_utf8(output.stderr).unwrap();
Err(anyhow!(stderr))
Err(eyre!(stderr))
} else {
Ok(())
}
@@ -66,11 +67,11 @@ async fn pull_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) -
.stdin(Stdio::null())
.output()
.await?;
let result = check_output(pull_output).and_then(|_| check_output(submodule_output));
let result = output_checked_utf8(pull_output).and_then(|_| output_checked_utf8(submodule_output));
if let Err(message) = &result {
println!("{} pulling {}", style("Failed").red().bold(), &repo);
print!("{}", message);
print!("{message}");
} else {
let after_revision = get_head_revision(git, &repo);
@@ -86,12 +87,9 @@ async fn pull_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) -
"log",
"--no-decorate",
"--oneline",
&format!("{}..{}", before, after),
&format!("{before}..{after}"),
])
.spawn()
.unwrap()
.wait()
.unwrap();
.status_checked()?;
println!();
}
_ => {
@@ -108,8 +106,8 @@ fn get_head_revision(git: &Path, repo: &str) -> Option<String> {
.stdin(Stdio::null())
.current_dir(repo)
.args(["rev-parse", "HEAD"])
.check_output()
.map(|output| output.trim().to_string())
.output_checked_utf8()
.map(|output| output.stdout.trim().to_string())
.map_err(|e| {
error!("Error getting revision for {}: {}", repo, e);
@@ -123,8 +121,8 @@ fn has_remotes(git: &Path, repo: &str) -> Option<bool> {
.stdin(Stdio::null())
.current_dir(repo)
.args(["remote", "show"])
.check_output()
.map(|output| output.lines().count() > 0)
.output_checked_utf8()
.map(|output| output.stdout.lines().count() > 0)
.map_err(|e| {
error!("Error getting remotes for {}: {}", repo, e);
e
@@ -166,9 +164,9 @@ impl Git {
.stdin(Stdio::null())
.current_dir(path)
.args(["rev-parse", "--show-toplevel"])
.check_output()
.output_checked_utf8()
.ok()
.map(|output| output.trim().to_string());
.map(|output| output.stdout.trim().to_string());
return output;
}
}
@@ -189,7 +187,7 @@ impl Git {
repositories
.bad_patterns
.iter()
.for_each(|pattern| print_warning(format!("Path {} did not contain any git repositories", pattern)));
.for_each(|pattern| print_warning(format!("Path {pattern} did not contain any git repositories")));
self.multi_pull(repositories, ctx)
}

45
src/steps/go.rs Normal file
View File

@@ -0,0 +1,45 @@
use std::path::PathBuf;
use std::process::Command;
use color_eyre::eyre::Result;
use crate::command::CommandExt;
use crate::executor::RunType;
use crate::terminal::print_separator;
use crate::utils;
use crate::utils::PathExt;
/// <https://github.com/Gelio/go-global-update>
pub fn run_go_global_update(run_type: RunType) -> Result<()> {
let go_global_update = require_go_bin("go-global-update")?;
print_separator("go-global-update");
run_type.execute(go_global_update).status_checked()
}
/// <https://github.com/nao1215/gup>
pub fn run_go_gup(run_type: RunType) -> Result<()> {
let gup = require_go_bin("gup")?;
print_separator("gup");
run_type.execute(gup).arg("update").status_checked()
}
/// Get the path of a Go binary.
fn require_go_bin(name: &str) -> Result<PathBuf> {
utils::require(name).or_else(|_| {
let go = utils::require("go")?;
// TODO: Does this work? `go help gopath` says that:
// > The GOPATH environment variable lists places to look for Go code.
// > On Unix, the value is a colon-separated string.
// > On Windows, the value is a semicolon-separated string.
// > On Plan 9, the value is a list.
// Should we also fallback to the env variable?
let gopath_output = Command::new(go).args(["env", "GOPATH"]).output_checked_utf8()?;
let gopath = gopath_output.stdout.trim();
PathBuf::from(gopath).join("bin").join(name).require()
})
}

View File

@@ -1,10 +1,8 @@
use crate::error::TopgradeError;
use crate::terminal::print_separator;
use crate::utils::require;
use anyhow::Result;
use color_eyre::eyre::Result;
use crate::execution_context::ExecutionContext;
use crate::executor::ExecutorOutput;
const UPGRADE_KAK: &str = include_str!("upgrade.kak");
@@ -13,19 +11,13 @@ pub fn upgrade_kak_plug(ctx: &ExecutionContext) -> Result<()> {
print_separator("Kakoune");
let mut command = ctx.run_type().execute(kak);
command.args(["-ui", "dummy", "-e", UPGRADE_KAK]);
// TODO: Why supress output for this command?
ctx.run_type()
.execute(kak)
.args(["-ui", "dummy", "-e", UPGRADE_KAK])
.output()?;
let output = command.output()?;
if let ExecutorOutput::Wet(output) = output {
let status = output.status;
if !status.success() {
return Err(TopgradeError::ProcessFailed(status).into());
} else {
println!("Plugins upgraded")
}
}
println!("Plugins upgraded");
Ok(())
}

View File

@@ -2,6 +2,7 @@ pub mod containers;
pub mod emacs;
pub mod generic;
pub mod git;
pub mod go;
pub mod kakoune;
pub mod node;
pub mod os;

View File

@@ -1,19 +1,17 @@
#![allow(unused_imports)]
use std::fmt::Display;
#[cfg(unix)]
use std::os::unix::prelude::MetadataExt;
#[cfg(target_os = "linux")]
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::process::Command;
use anyhow::Result;
use directories::BaseDirs;
use log::debug;
#[cfg(unix)]
use crate::utils::require_option;
use color_eyre::eyre::Result;
#[cfg(target_os = "linux")]
use nix::unistd::Uid;
use semver::Version;
use tracing::debug;
use crate::executor::{CommandExt, RunType};
use crate::command::CommandExt;
use crate::terminal::print_separator;
use crate::utils::{require, PathExt};
use crate::{error::SkipStep, execution_context::ExecutionContext};
@@ -24,13 +22,6 @@ enum NPMVariant {
}
impl NPMVariant {
const fn long_name(&self) -> &str {
match self {
NPMVariant::Npm => "Node Package Manager",
NPMVariant::Pnpm => "PNPM",
}
}
const fn short_name(&self) -> &str {
match self {
NPMVariant::Npm => "npm",
@@ -85,25 +76,29 @@ impl NPM {
let args = ["root", self.global_location_arg()];
Command::new(&self.command)
.args(args)
.check_output()
.map(|s| PathBuf::from(s.trim()))
.output_checked_utf8()
.map(|s| PathBuf::from(s.stdout.trim()))
}
fn version(&self) -> Result<Version> {
let version_str = Command::new(&self.command)
.args(["--version"])
.check_output()
.map(|s| s.trim().to_owned());
.output_checked_utf8()
.map(|s| s.stdout.trim().to_owned());
Version::parse(&version_str?).map_err(|err| err.into())
}
fn upgrade(&self, run_type: RunType, use_sudo: bool) -> Result<()> {
print_separator(self.variant.long_name());
fn upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> {
let args = ["update", self.global_location_arg()];
if use_sudo {
run_type.execute("sudo").args(args).check_run()?;
let sudo = require_option(ctx.sudo().clone(), String::from("sudo is not installed"))?;
ctx.run_type()
.execute(sudo)
.arg(&self.command)
.args(args)
.status_checked()?;
} else {
run_type.execute(&self.command).args(args).check_run()?;
ctx.run_type().execute(&self.command).args(args).status_checked()?;
}
Ok(())
@@ -142,9 +137,9 @@ impl Yarn {
//
// As “yarn dlx” don't need to “upgrade”, we
// ignore the whole task if Yarn is 2.x or above.
let version = Command::new(&self.command).args(["--version"]).check_output();
let version = Command::new(&self.command).args(["--version"]).output_checked_utf8();
matches!(version, Ok(ver) if ver.starts_with('1') || ver.starts_with('0'))
matches!(version, Ok(ver) if ver.stdout.starts_with('1') || ver.stdout.starts_with('0'))
}
#[cfg(target_os = "linux")]
@@ -152,22 +147,22 @@ impl Yarn {
let args = ["global", "dir"];
Command::new(&self.command)
.args(args)
.check_output()
.map(|s| PathBuf::from(s.trim()))
.output_checked_utf8()
.map(|s| PathBuf::from(s.stdout.trim()))
}
fn upgrade(&self, run_type: RunType, use_sudo: bool) -> Result<()> {
print_separator("Yarn Package Manager");
fn upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> {
let args = ["global", "upgrade"];
if use_sudo {
run_type
.execute("sudo")
let sudo = require_option(ctx.sudo().clone(), String::from("sudo is not installed"))?;
ctx.run_type()
.execute(sudo)
.arg(self.yarn.as_ref().unwrap_or(&self.command))
.args(args)
.check_run()?;
.status_checked()?;
} else {
run_type.execute(&self.command).args(args).check_run()?;
ctx.run_type().execute(&self.command).args(args).status_checked()?;
}
Ok(())
@@ -218,28 +213,32 @@ fn should_use_sudo_yarn(yarn: &Yarn, ctx: &ExecutionContext) -> Result<bool> {
pub fn run_npm_upgrade(ctx: &ExecutionContext) -> Result<()> {
let npm = require("npm").map(|b| NPM::new(b, NPMVariant::Npm))?;
print_separator("Node Package Manager");
#[cfg(target_os = "linux")]
{
npm.upgrade(ctx.run_type(), should_use_sudo(&npm, ctx)?)
npm.upgrade(ctx, should_use_sudo(&npm, ctx)?)
}
#[cfg(not(target_os = "linux"))]
{
npm.upgrade(ctx.run_type(), false)
npm.upgrade(ctx, false)
}
}
pub fn run_pnpm_upgrade(ctx: &ExecutionContext) -> Result<()> {
let pnpm = require("pnpm").map(|b| NPM::new(b, NPMVariant::Pnpm))?;
print_separator("Node Package Manager");
#[cfg(target_os = "linux")]
{
pnpm.upgrade(ctx.run_type(), should_use_sudo(&pnpm, ctx)?)
pnpm.upgrade(ctx, should_use_sudo(&pnpm, ctx)?)
}
#[cfg(not(target_os = "linux"))]
{
pnpm.upgrade(ctx.run_type(), false)
pnpm.upgrade(ctx, false)
}
}
@@ -251,14 +250,16 @@ pub fn run_yarn_upgrade(ctx: &ExecutionContext) -> Result<()> {
return Ok(());
}
print_separator("Yarn Package Manager");
#[cfg(target_os = "linux")]
{
yarn.upgrade(ctx.run_type(), should_use_sudo_yarn(&yarn, ctx)?)
yarn.upgrade(ctx, should_use_sudo_yarn(&yarn, ctx)?)
}
#[cfg(not(target_os = "linux"))]
{
yarn.upgrade(ctx.run_type(), false)
yarn.upgrade(ctx, false)
}
}
@@ -272,5 +273,5 @@ pub fn deno_upgrade(ctx: &ExecutionContext) -> Result<()> {
}
print_separator("Deno");
ctx.run_type().execute(&deno).arg("upgrade").check_run()
ctx.run_type().execute(&deno).arg("upgrade").status_checked()
}

View File

@@ -1,8 +1,10 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::terminal::print_separator;
use crate::utils::require;
use crate::utils::which;
use crate::Step;
use anyhow::Result;
use color_eyre::eyre::Result;
pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
//let pkg = require("pkg")?;
@@ -10,7 +12,7 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
print_separator("Termux Packages");
let is_nala = pkg.end_with("nala");
let is_nala = pkg.ends_with("nala");
let mut command = ctx.run_type().execute(&pkg);
command.arg("upgrade");
@@ -18,11 +20,10 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
if ctx.config().yes(Step::System) {
command.arg("-y");
}
command.check_run()?;
command.status_checked()?;
if !is_nala {
if ctx.config().cleanup() {
ctx.run_type().execute(&pkg).arg("clean").check_run()?;
if !is_nala && ctx.config().cleanup() {
ctx.run_type().execute(&pkg).arg("clean").status_checked()?;
let apt = require("apt")?;
let mut command = ctx.run_type().execute(&apt);
@@ -30,8 +31,7 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
if ctx.config().yes(Step::System) {
command.arg("-y");
}
command.check_run()?;
}
command.status_checked()?;
}
Ok(())

View File

@@ -1,13 +1,15 @@
use std::env::var_os;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::Result;
use color_eyre::eyre;
use color_eyre::eyre::Result;
use walkdir::WalkDir;
use crate::command::CommandExt;
use crate::error::TopgradeError;
use crate::execution_context::ExecutionContext;
use crate::sudo::Sudo;
use crate::utils::which;
use crate::{config, Step};
@@ -29,11 +31,10 @@ pub struct YayParu {
impl ArchPackageManager for YayParu {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
if ctx.config().show_arch_news() {
Command::new(&self.executable)
ctx.run_type()
.execute(&self.executable)
.arg("-Pw")
.spawn()
.and_then(|mut p| p.wait())
.ok();
.status_checked_with_codes(&[1, 0])?;
}
let mut command = ctx.run_type().execute(&self.executable);
@@ -48,7 +49,7 @@ impl ArchPackageManager for YayParu {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.check_run()?;
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable);
@@ -56,7 +57,7 @@ impl ArchPackageManager for YayParu {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.check_run()?;
command.status_checked()?;
}
Ok(())
@@ -72,6 +73,37 @@ impl YayParu {
}
}
pub struct GarudaUpdate {
executable: PathBuf,
}
impl ArchPackageManager for GarudaUpdate {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let mut command = ctx.run_type().execute(&self.executable);
command
.env("PATH", get_execution_path())
.env("UPDATE_AUR", "1")
.env("SKIP_MIRRORLIST", "1");
if ctx.config().yes(Step::System) {
command.env("PACMAN_NOCONFIRM", "1");
}
command.args(ctx.config().garuda_update_arguments().split_whitespace());
command.status_checked()?;
Ok(())
}
}
impl GarudaUpdate {
fn get() -> Option<Self> {
Some(Self {
executable: which("garuda-update")?,
})
}
}
pub struct Trizen {
executable: PathBuf,
}
@@ -88,7 +120,7 @@ impl ArchPackageManager for Trizen {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.check_run()?;
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable);
@@ -96,7 +128,7 @@ impl ArchPackageManager for Trizen {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.check_run()?;
command.status_checked()?;
}
Ok(())
@@ -112,7 +144,7 @@ impl Trizen {
}
pub struct Pacman {
sudo: PathBuf,
sudo: Sudo,
executable: PathBuf,
}
@@ -126,7 +158,7 @@ impl ArchPackageManager for Pacman {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.check_run()?;
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.sudo);
@@ -134,7 +166,7 @@ impl ArchPackageManager for Pacman {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.check_run()?;
command.status_checked()?;
}
Ok(())
@@ -175,7 +207,7 @@ impl ArchPackageManager for Pikaur {
command.arg("--noconfirm");
}
command.check_run()?;
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable);
@@ -183,7 +215,7 @@ impl ArchPackageManager for Pikaur {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.check_run()?;
command.status_checked()?;
}
Ok(())
@@ -214,7 +246,7 @@ impl ArchPackageManager for Pamac {
command.arg("--no-confirm");
}
command.check_run()?;
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable);
@@ -222,7 +254,7 @@ impl ArchPackageManager for Pamac {
if ctx.config().yes(Step::System) {
command.arg("--no-confirm");
}
command.check_run()?;
command.status_checked()?;
}
Ok(())
@@ -231,7 +263,7 @@ impl ArchPackageManager for Pamac {
pub struct Aura {
executable: PathBuf,
sudo: PathBuf,
sudo: Sudo,
}
impl Aura {
@@ -257,7 +289,7 @@ impl ArchPackageManager for Aura {
aur_update.arg("--noconfirm");
}
aur_update.check_run()?;
aur_update.status_checked()?;
} else {
println!("Aura requires sudo installed to work with AUR packages")
}
@@ -270,7 +302,7 @@ impl ArchPackageManager for Aura {
if ctx.config().yes(Step::System) {
pacman_update.arg("--noconfirm");
}
pacman_update.check_run()?;
pacman_update.status_checked()?;
Ok(())
}
@@ -284,14 +316,16 @@ pub fn get_arch_package_manager(ctx: &ExecutionContext) -> Option<Box<dyn ArchPa
let pacman = which("powerpill").unwrap_or_else(|| PathBuf::from("pacman"));
match ctx.config().arch_package_manager() {
config::ArchPackageManager::Autodetect => YayParu::get("paru", &pacman)
config::ArchPackageManager::Autodetect => GarudaUpdate::get()
.map(box_package_manager)
.or_else(|| YayParu::get("paru", &pacman).map(box_package_manager))
.or_else(|| YayParu::get("yay", &pacman).map(box_package_manager))
.or_else(|| Trizen::get().map(box_package_manager))
.or_else(|| Pikaur::get().map(box_package_manager))
.or_else(|| Pamac::get().map(box_package_manager))
.or_else(|| Pacman::get(ctx).map(box_package_manager))
.or_else(|| Aura::get(ctx).map(box_package_manager)),
config::ArchPackageManager::GarudaUpdate => GarudaUpdate::get().map(box_package_manager),
config::ArchPackageManager::Trizen => Trizen::get().map(box_package_manager),
config::ArchPackageManager::Paru => YayParu::get("paru", &pacman).map(box_package_manager),
config::ArchPackageManager::Yay => YayParu::get("yay", &pacman).map(box_package_manager),
@@ -304,7 +338,7 @@ pub fn get_arch_package_manager(ctx: &ExecutionContext) -> Option<Box<dyn ArchPa
pub fn upgrade_arch_linux(ctx: &ExecutionContext) -> Result<()> {
let package_manager =
get_arch_package_manager(ctx).ok_or_else(|| anyhow::Error::from(TopgradeError::FailedGettingPackageManager))?;
get_arch_package_manager(ctx).ok_or_else(|| eyre::Report::from(TopgradeError::FailedGettingPackageManager))?;
package_manager.upgrade(ctx)
}

View File

@@ -1,26 +1,26 @@
use crate::command::CommandExt;
use crate::executor::RunType;
use crate::sudo::Sudo;
use crate::terminal::print_separator;
use crate::utils::require_option;
use anyhow::Result;
use std::path::PathBuf;
use color_eyre::eyre::Result;
use std::process::Command;
pub fn upgrade_packages(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> {
pub fn upgrade_packages(sudo: Option<&Sudo>, run_type: RunType) -> Result<()> {
let sudo = require_option(sudo, String::from("No sudo detected"))?;
print_separator("DrgaonFly BSD Packages");
print_separator("DragonFly BSD Packages");
run_type
.execute(sudo)
.args(&["/usr/local/sbin/pkg", "upgrade"])
.check_run()
.args(["/usr/local/sbin/pkg", "upgrade"])
.status_checked()
}
pub fn audit_packages(sudo: &Option<PathBuf>) -> Result<()> {
pub fn audit_packages(sudo: Option<&Sudo>) -> Result<()> {
if let Some(sudo) = sudo {
println!();
Command::new(sudo)
.args(&["/usr/local/sbin/pkg", "audit", "-Fr"])
.spawn()?
.wait()?;
.args(["/usr/local/sbin/pkg", "audit", "-Fr"])
.status_checked()?;
}
Ok(())
}

View File

@@ -1,32 +1,41 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::executor::RunType;
use crate::sudo::Sudo;
use crate::terminal::print_separator;
use crate::utils::require_option;
use anyhow::Result;
use std::path::PathBuf;
use crate::Step;
use color_eyre::eyre::Result;
use std::process::Command;
pub fn upgrade_freebsd(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> {
pub fn upgrade_freebsd(sudo: Option<&Sudo>, run_type: RunType) -> Result<()> {
let sudo = require_option(sudo, String::from("No sudo detected"))?;
print_separator("FreeBSD Update");
run_type
.execute(sudo)
.args(&["/usr/sbin/freebsd-update", "fetch", "install"])
.check_run()
.args(["/usr/sbin/freebsd-update", "fetch", "install"])
.status_checked()
}
pub fn upgrade_packages(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> {
pub fn upgrade_packages(ctx: &ExecutionContext, sudo: Option<&Sudo>, run_type: RunType) -> Result<()> {
let sudo = require_option(sudo, String::from("No sudo detected"))?;
print_separator("FreeBSD Packages");
run_type.execute(sudo).args(&["/usr/sbin/pkg", "upgrade"]).check_run()
let mut command = run_type.execute(sudo);
command.args(["/usr/sbin/pkg", "upgrade"]);
if ctx.config().yes(Step::System) {
command.arg("-y");
}
command.status_checked()
}
pub fn audit_packages(sudo: &Option<PathBuf>) -> Result<()> {
pub fn audit_packages(sudo: Option<&Sudo>) -> Result<()> {
if let Some(sudo) = sudo {
println!();
Command::new(sudo)
.args(&["/usr/sbin/pkg", "audit", "-Fr"])
.spawn()?
.wait()?;
.args(["/usr/sbin/pkg", "audit", "-Fr"])
.status_checked()?;
}
Ok(())
}

View File

@@ -1,14 +1,16 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::Result;
use color_eyre::eyre::Result;
use ini::Ini;
use log::{debug, warn};
use tracing::{debug, warn};
use crate::command::CommandExt;
use crate::error::{SkipStep, TopgradeError};
use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, RunType};
use crate::executor::RunType;
use crate::steps::os::archlinux;
use crate::sudo::Sudo;
use crate::terminal::{print_separator, print_warning};
use crate::utils::{require, require_option, which, PathExt};
use crate::Step;
@@ -27,7 +29,9 @@ pub enum Distribution {
Debian,
Gentoo,
OpenMandriva,
PCLinuxOS,
Suse,
SuseMicro,
Void,
Solus,
Exherbo,
@@ -53,8 +57,10 @@ impl Distribution {
Some("gentoo") => Distribution::Gentoo,
Some("exherbo") => Distribution::Exherbo,
Some("nixos") => Distribution::NixOS,
Some("opensuse-microos") => Distribution::SuseMicro,
Some("neon") => Distribution::KDENeon,
Some("openmandriva") => Distribution::OpenMandriva,
Some("pclinuxos") => Distribution::PCLinuxOS,
_ => {
if let Some(id_like) = id_like {
if id_like.contains(&"debian") || id_like.contains(&"ubuntu") {
@@ -101,6 +107,7 @@ impl Distribution {
Distribution::Debian => upgrade_debian(ctx),
Distribution::Gentoo => upgrade_gentoo(ctx),
Distribution::Suse => upgrade_suse(ctx),
Distribution::SuseMicro => upgrade_suse_micro(ctx),
Distribution::Void => upgrade_void(ctx),
Distribution::Solus => upgrade_solus(ctx),
Distribution::Exherbo => upgrade_exherbo(ctx),
@@ -108,6 +115,7 @@ impl Distribution {
Distribution::KDENeon => upgrade_neon(ctx),
Distribution::Bedrock => update_bedrock(ctx),
Distribution::OpenMandriva => upgrade_openmandriva(ctx),
Distribution::PCLinuxOS => upgrade_pclinuxos(ctx),
}
}
@@ -127,11 +135,10 @@ fn update_bedrock(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type().execute(sudo).args(["brl", "update"]);
let output = Command::new("brl").arg("list").output()?;
let output = Command::new("brl").arg("list").output_checked_utf8()?;
debug!("brl list: {:?} {:?}", output.stdout, output.stderr);
let parsed_output = String::from_utf8(output.stdout).unwrap();
for distribution in parsed_output.trim().split('\n') {
for distribution in output.stdout.trim().lines() {
debug!("Bedrock distribution {}", distribution);
match distribution {
"arch" => archlinux::upgrade_arch_linux(ctx)?,
@@ -148,7 +155,7 @@ fn update_bedrock(ctx: &ExecutionContext) -> Result<()> {
}
fn is_wsl() -> Result<bool> {
let output = Command::new("uname").arg("-r").check_output()?;
let output = Command::new("uname").arg("-r").output_checked_utf8()?.stdout;
debug!("Uname output: {}", output);
Ok(output.contains("microsoft"))
}
@@ -157,8 +164,8 @@ fn upgrade_alpine_linux(ctx: &ExecutionContext) -> Result<()> {
let apk = require("apk")?;
let sudo = ctx.sudo().as_ref().unwrap();
ctx.run_type().execute(sudo).arg(&apk).arg("update").check_run()?;
ctx.run_type().execute(sudo).arg(&apk).arg("upgrade").check_run()
ctx.run_type().execute(sudo).arg(&apk).arg("update").status_checked()?;
ctx.run_type().execute(sudo).arg(&apk).arg("upgrade").status_checked()
}
fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> {
@@ -166,7 +173,7 @@ fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> {
if ctx.config().rpm_ostree() {
let mut command = ctx.run_type().execute(ostree);
command.arg("upgrade");
return command.check_run();
return command.status_checked();
}
};
@@ -188,17 +195,16 @@ fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> {
command.arg("-y");
}
command.check_run()?;
command.status_checked()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_bedrock_strata(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = ctx.sudo() {
ctx.run_type().execute(sudo).args(["brl", "update"]).check_run()?;
ctx.run_type().execute(sudo).args(["brl", "update"]).status_checked()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
@@ -208,12 +214,27 @@ fn upgrade_bedrock_strata(ctx: &ExecutionContext) -> Result<()> {
fn upgrade_suse(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = ctx.sudo() {
ctx.run_type().execute(sudo).args(["zypper", "refresh"]).check_run()?;
ctx.run_type()
.execute(sudo)
.args(["zypper", "refresh"])
.status_checked()?;
ctx.run_type()
.execute(sudo)
.args(["zypper", "dist-upgrade"])
.check_run()?;
.status_checked()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_suse_micro(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = ctx.sudo() {
ctx.run_type()
.execute(sudo)
.args(["transactional-update", "dup"])
.status_checked()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
@@ -235,7 +256,34 @@ fn upgrade_openmandriva(ctx: &ExecutionContext) -> Result<()> {
command.arg("-y");
}
command.check_run()?;
command.status_checked()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_pclinuxos(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = &ctx.sudo() {
let mut command_update = ctx.run_type().execute(sudo);
command_update.arg(&which("apt-get").unwrap()).arg("update");
if let Some(args) = ctx.config().dnf_arguments() {
command_update.args(args.split_whitespace());
}
if ctx.config().yes(Step::System) {
command_update.arg("-y");
}
command_update.status_checked()?;
ctx.run_type()
.execute(sudo)
.arg(&which("apt-get").unwrap())
.arg("dist-upgrade")
.status_checked()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
@@ -250,14 +298,14 @@ fn upgrade_void(ctx: &ExecutionContext) -> Result<()> {
if ctx.config().yes(Step::System) {
command.arg("-y");
}
command.check_run()?;
command.status_checked()?;
let mut command = ctx.run_type().execute(sudo);
command.args(["xbps-install", "-u"]);
if ctx.config().yes(Step::System) {
command.arg("-y");
}
command.check_run()?;
command.status_checked()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
@@ -270,7 +318,11 @@ fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = &ctx.sudo() {
if let Some(layman) = which("layman") {
run_type.execute(sudo).arg(layman).args(["-s", "ALL"]).check_run()?;
run_type
.execute(sudo)
.arg(layman)
.args(["-s", "ALL"])
.status_checked()?;
}
println!("Syncing portage");
@@ -283,10 +335,10 @@ fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> {
.map(|s| s.split_whitespace().collect())
.unwrap_or_else(|| vec!["-q"]),
)
.check_run()?;
.status_checked()?;
if let Some(eix_update) = which("eix-update") {
run_type.execute(sudo).arg(eix_update).check_run()?;
run_type.execute(sudo).arg(eix_update).status_checked()?;
}
run_type
@@ -298,7 +350,7 @@ fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> {
.map(|s| s.split_whitespace().collect())
.unwrap_or_else(|| vec!["-uDNa", "--with-bdeps=y", "world"]),
)
.check_run()?;
.status_checked()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
@@ -309,12 +361,18 @@ fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> {
fn upgrade_debian(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = &ctx.sudo() {
let apt = which("apt-fast")
.or_else(|| which("nala"))
.or_else(|| {
if Path::new("/usr/bin/nala").exists() {
Some(Path::new("/usr/bin/nala").to_path_buf())
} else {
None
}
})
.unwrap_or_else(|| PathBuf::from("apt-get"));
let is_nala = apt.ends_with("nala");
if !is_nala {
ctx.run_type().execute(sudo).arg(&apt).arg("update").check_run()?;
ctx.run_type().execute(sudo).arg(&apt).arg("update").status_checked()?;
}
let mut command = ctx.run_type().execute(sudo);
@@ -330,17 +388,17 @@ fn upgrade_debian(ctx: &ExecutionContext) -> Result<()> {
if let Some(args) = ctx.config().apt_arguments() {
command.args(args.split_whitespace());
}
command.check_run()?;
command.status_checked()?;
if ctx.config().cleanup() {
ctx.run_type().execute(sudo).arg(&apt).arg("clean").check_run()?;
ctx.run_type().execute(sudo).arg(&apt).arg("clean").status_checked()?;
let mut command = ctx.run_type().execute(sudo);
command.arg(&apt).arg("autoremove");
if ctx.config().yes(Step::System) {
command.arg("-y");
}
command.check_run()?;
command.status_checked()?;
}
} else {
print_warning("No sudo detected. Skipping system upgrade");
@@ -354,11 +412,11 @@ pub fn run_deb_get(ctx: &ExecutionContext) -> Result<()> {
print_separator("deb-get");
ctx.execute_elevated(&deb_get, false)?.arg("update").check_run()?;
ctx.execute_elevated(&deb_get, false)?.arg("upgrade").check_run()?;
ctx.execute_elevated(&deb_get, false)?.arg("update").status_checked()?;
ctx.execute_elevated(&deb_get, false)?.arg("upgrade").status_checked()?;
if ctx.config().cleanup() {
ctx.execute_elevated(&deb_get, false)?.arg("clean").check_run()?;
ctx.execute_elevated(&deb_get, false)?.arg("clean").status_checked()?;
}
Ok(())
@@ -366,7 +424,10 @@ pub fn run_deb_get(ctx: &ExecutionContext) -> Result<()> {
fn upgrade_solus(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = ctx.sudo() {
ctx.run_type().execute(sudo).args(["eopkg", "upgrade"]).check_run()?;
ctx.run_type()
.execute(sudo)
.args(["eopkg", "upgrade"])
.status_checked()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
@@ -374,15 +435,25 @@ fn upgrade_solus(ctx: &ExecutionContext) -> Result<()> {
Ok(())
}
pub fn update_am(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = ctx.sudo() {
ctx.run_type().execute(sudo).args(["am", "-u"]).status_checked()?;
} else {
print_warning("No sudo detected. Skipping AM Step");
}
Ok(())
}
pub fn run_pacdef(ctx: &ExecutionContext) -> Result<()> {
let pacdef = require("pacdef")?;
print_separator("pacdef");
ctx.run_type().execute(&pacdef).arg("sync").check_run()?;
ctx.run_type().execute(&pacdef).arg("sync").status_checked()?;
println!();
ctx.run_type().execute(&pacdef).arg("review").check_run()
ctx.run_type().execute(&pacdef).arg("review").status_checked()
}
pub fn run_pacstall(ctx: &ExecutionContext) -> Result<()> {
@@ -390,13 +461,24 @@ pub fn run_pacstall(ctx: &ExecutionContext) -> Result<()> {
print_separator("Pacstall");
ctx.run_type().execute(&pacstall).arg("-U").check_run()?;
ctx.run_type().execute(pacstall).arg("-Up").check_run()
let mut update_cmd = ctx.run_type().execute(&pacstall);
let mut upgrade_cmd = ctx.run_type().execute(pacstall);
if ctx.config().yes(Step::Pacstall) {
update_cmd.arg("-P");
upgrade_cmd.arg("-P");
}
update_cmd.arg("-U").status_checked()?;
upgrade_cmd.arg("-Up").status_checked()
}
fn upgrade_clearlinux(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = &ctx.sudo() {
ctx.run_type().execute(sudo).args(["swupd", "update"]).check_run()?;
ctx.run_type()
.execute(sudo)
.args(["swupd", "update"])
.status_checked()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
@@ -406,26 +488,29 @@ fn upgrade_clearlinux(ctx: &ExecutionContext) -> Result<()> {
fn upgrade_exherbo(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = ctx.sudo() {
ctx.run_type().execute(sudo).args(["cave", "sync"]).check_run()?;
ctx.run_type().execute(sudo).args(["cave", "sync"]).status_checked()?;
ctx.run_type()
.execute(sudo)
.args(["cave", "resolve", "world", "-c1", "-Cs", "-km", "-Km", "-x"])
.check_run()?;
.status_checked()?;
if ctx.config().cleanup() {
ctx.run_type().execute(sudo).args(["cave", "purge", "-x"]).check_run()?;
ctx.run_type()
.execute(sudo)
.args(["cave", "purge", "-x"])
.status_checked()?;
}
ctx.run_type()
.execute(sudo)
.args(["cave", "fix-linkage", "-x", "--", "-Cs"])
.check_run()?;
.status_checked()?;
ctx.run_type()
.execute(sudo)
.args(["eclectic", "config", "interactive"])
.check_run()?;
.status_checked()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
@@ -435,16 +520,19 @@ fn upgrade_exherbo(ctx: &ExecutionContext) -> Result<()> {
fn upgrade_nixos(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = ctx.sudo() {
ctx.run_type()
.execute(sudo)
.args(["/run/current-system/sw/bin/nixos-rebuild", "switch", "--upgrade"])
.check_run()?;
let mut command = ctx.run_type().execute(sudo);
command.args(["/run/current-system/sw/bin/nixos-rebuild", "switch", "--upgrade"]);
if let Some(args) = ctx.config().nix_arguments() {
command.args(args.split_whitespace());
}
command.status_checked()?;
if ctx.config().cleanup() {
ctx.run_type()
.execute(sudo)
.args(["/run/current-system/sw/bin/nix-collect-garbage", "-d"])
.check_run()?;
.status_checked()?;
}
} else {
print_warning("No sudo detected. Skipping system upgrade");
@@ -462,7 +550,11 @@ fn upgrade_neon(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = &ctx.sudo() {
let pkcon = which("pkcon").unwrap();
// pkcon ignores update with update and refresh provided together
ctx.run_type().execute(sudo).arg(&pkcon).arg("refresh").check_run()?;
ctx.run_type()
.execute(sudo)
.arg(&pkcon)
.arg("refresh")
.status_checked()?;
let mut exe = ctx.run_type().execute(sudo);
let cmd = exe.arg(&pkcon).arg("update");
if ctx.config().yes(Step::System) {
@@ -472,13 +564,13 @@ fn upgrade_neon(ctx: &ExecutionContext) -> Result<()> {
cmd.arg("--autoremove");
}
// from pkcon man, exit code 5 is 'Nothing useful was done.'
cmd.check_run_with_codes(&[5])?;
cmd.status_checked_with_codes(&[5])?;
}
Ok(())
}
pub fn run_needrestart(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> {
pub fn run_needrestart(sudo: Option<&Sudo>, run_type: RunType) -> Result<()> {
let sudo = require_option(sudo, String::from("sudo is not installed"))?;
let needrestart = require("needrestart")?;
let distribution = Distribution::detect()?;
@@ -489,7 +581,7 @@ pub fn run_needrestart(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()>
print_separator("Check for needed restarts");
run_type.execute(sudo).arg(needrestart).check_run()?;
run_type.execute(sudo).arg(needrestart).status_checked()?;
Ok(())
}
@@ -506,7 +598,7 @@ pub fn run_fwupdmgr(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type()
.execute(&fwupdmgr)
.arg("refresh")
.check_run_with_codes(&[2])?;
.status_checked_with_codes(&[2])?;
let mut updmgr = ctx.run_type().execute(&fwupdmgr);
@@ -518,7 +610,7 @@ pub fn run_fwupdmgr(ctx: &ExecutionContext) -> Result<()> {
} else {
updmgr.arg("get-updates");
}
updmgr.check_run_with_codes(&[2])
updmgr.status_checked_with_codes(&[2])
}
pub fn flatpak_update(ctx: &ExecutionContext) -> Result<()> {
@@ -533,14 +625,14 @@ pub fn flatpak_update(ctx: &ExecutionContext) -> Result<()> {
if yes {
update_args.push("-y");
}
run_type.execute(&flatpak).args(&update_args).check_run()?;
run_type.execute(&flatpak).args(&update_args).status_checked()?;
if cleanup {
let mut cleanup_args = vec!["uninstall", "--user", "--unused"];
if yes {
cleanup_args.push("-y");
}
run_type.execute(&flatpak).args(&cleanup_args).check_run()?;
run_type.execute(&flatpak).args(&cleanup_args).status_checked()?;
}
print_separator("Flatpak System Packages");
@@ -549,33 +641,41 @@ pub fn flatpak_update(ctx: &ExecutionContext) -> Result<()> {
if yes {
update_args.push("-y");
}
run_type.execute(sudo).arg(&flatpak).args(&update_args).check_run()?;
run_type
.execute(sudo)
.arg(&flatpak)
.args(&update_args)
.status_checked()?;
if cleanup {
let mut cleanup_args = vec!["uninstall", "--system", "--unused"];
if yes {
cleanup_args.push("-y");
}
run_type.execute(sudo).arg(flatpak).args(&cleanup_args).check_run()?;
run_type
.execute(sudo)
.arg(flatpak)
.args(&cleanup_args)
.status_checked()?;
}
} else {
let mut update_args = vec!["update", "--system"];
if yes {
update_args.push("-y");
}
run_type.execute(&flatpak).args(&update_args).check_run()?;
run_type.execute(&flatpak).args(&update_args).status_checked()?;
if cleanup {
let mut cleanup_args = vec!["uninstall", "--system", "--unused"];
if yes {
cleanup_args.push("-y");
}
run_type.execute(flatpak).args(&cleanup_args).check_run()?;
run_type.execute(flatpak).args(&cleanup_args).status_checked()?;
}
}
Ok(())
}
pub fn run_snap(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> {
pub fn run_snap(sudo: Option<&Sudo>, run_type: RunType) -> Result<()> {
let sudo = require_option(sudo, String::from("sudo is not installed"))?;
let snap = require("snap")?;
@@ -584,17 +684,17 @@ pub fn run_snap(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> {
}
print_separator("snap");
run_type.execute(sudo).arg(snap).arg("refresh").check_run()
run_type.execute(sudo).arg(snap).arg("refresh").status_checked()
}
pub fn run_pihole_update(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> {
pub fn run_pihole_update(sudo: Option<&Sudo>, run_type: RunType) -> Result<()> {
let sudo = require_option(sudo, String::from("sudo is not installed"))?;
let pihole = require("pihole")?;
Path::new("/opt/pihole/update.sh").require()?;
print_separator("pihole");
run_type.execute(sudo).arg(pihole).arg("-up").check_run()
run_type.execute(sudo).arg(pihole).arg("-up").status_checked()
}
pub fn run_protonup_update(ctx: &ExecutionContext) -> Result<()> {
@@ -602,7 +702,7 @@ pub fn run_protonup_update(ctx: &ExecutionContext) -> Result<()> {
print_separator("protonup");
ctx.run_type().execute(protonup).check_run()?;
ctx.run_type().execute(protonup).status_checked()?;
Ok(())
}
@@ -628,7 +728,30 @@ pub fn run_distrobox_update(ctx: &ExecutionContext) -> Result<()> {
(r, true) => r.arg("--root"),
(r, false) => r,
}
.check_run()
.status_checked()
}
pub fn run_dkp_pacman_update(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), String::from("sudo is not installed"))?;
let dkp_pacman = require("dkp-pacman")?;
print_separator("Devkitpro pacman");
ctx.run_type()
.execute(sudo)
.arg(&dkp_pacman)
.arg("-Syu")
.status_checked()?;
if ctx.config().cleanup() {
ctx.run_type()
.execute(sudo)
.arg(&dkp_pacman)
.arg("-Scc")
.status_checked()?;
}
Ok(())
}
pub fn run_config_update(ctx: &ExecutionContext) -> Result<()> {
@@ -639,14 +762,14 @@ pub fn run_config_update(ctx: &ExecutionContext) -> Result<()> {
if let Ok(etc_update) = require("etc-update") {
print_separator("Configuration update");
ctx.run_type().execute(sudo).arg(etc_update).check_run()?;
ctx.run_type().execute(sudo).arg(etc_update).status_checked()?;
} else if let Ok(pacdiff) = require("pacdiff") {
if std::env::var("DIFFPROG").is_err() {
require("vim")?;
}
print_separator("Configuration update");
ctx.execute_elevated(&pacdiff, false)?.check_run()?;
ctx.execute_elevated(&pacdiff, false)?.status_checked()?;
}
Ok(())

View File

@@ -1,26 +1,30 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, RunType};
use crate::executor::RunType;
use crate::terminal::{print_separator, prompt_yesno};
use crate::{error::TopgradeError, utils::require, Step};
use anyhow::Result;
use log::debug;
use crate::{utils::require, Step};
use color_eyre::eyre::Result;
use std::fs;
use std::process::Command;
use tracing::debug;
pub fn run_macports(ctx: &ExecutionContext) -> Result<()> {
require("port")?;
let sudo = ctx.sudo().as_ref().unwrap();
print_separator("MacPorts");
ctx.run_type().execute(sudo).args(["port", "selfupdate"]).check_run()?;
ctx.run_type()
.execute(sudo)
.args(["port", "selfupdate"])
.status_checked()?;
ctx.run_type()
.execute(sudo)
.args(["port", "-u", "upgrade", "outdated"])
.check_run()?;
.status_checked()?;
if ctx.config().cleanup() {
ctx.run_type()
.execute(sudo)
.args(["port", "-N", "reclaim"])
.check_run()?;
.status_checked()?;
}
Ok(())
@@ -30,7 +34,7 @@ pub fn run_mas(run_type: RunType) -> Result<()> {
let mas = require("mas")?;
print_separator("macOS App Store");
run_type.execute(mas).arg("upgrade").check_run()
run_type.execute(mas).arg("upgrade").status_checked()
}
pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> {
@@ -58,20 +62,15 @@ pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> {
command.arg("--no-scan");
}
command.check_run()
command.status_checked()
}
fn system_update_available() -> Result<bool> {
let output = Command::new("softwareupdate").arg("--list").output()?;
let output = Command::new("softwareupdate").arg("--list").output_checked_utf8()?;
debug!("{:?}", output);
let status = output.status;
if !status.success() {
return Err(TopgradeError::ProcessFailed(status).into());
}
let string_output = String::from_utf8(output.stderr)?;
debug!("{:?}", string_output);
Ok(!string_output.contains("No new software available"))
Ok(!output.stderr.contains("No new software available"))
}
pub fn run_sparkle(ctx: &ExecutionContext) -> Result<()> {
@@ -83,12 +82,12 @@ pub fn run_sparkle(ctx: &ExecutionContext) -> Result<()> {
let probe = Command::new(&sparkle)
.args(["--probe", "--application"])
.arg(application.path())
.check_output();
.output_checked_utf8();
if probe.is_ok() {
let mut command = ctx.run_type().execute(&sparkle);
command.args(["bundle", "--check-immediately", "--application"]);
command.arg(application.path());
command.spawn()?.wait()?;
command.status_checked()?;
}
}
Ok(())

View File

@@ -1,17 +1,23 @@
use crate::executor::RunType;
use crate::terminal::print_separator;
use crate::utils::require_option;
use anyhow::Result;
use color_eyre::eyre::Result;
use std::path::PathBuf;
pub fn upgrade_openbsd(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> {
let sudo = require_option(sudo, String::from("No sudo detected"))?;
print_separator("OpenBSD Update");
run_type.execute(sudo).args(&["/usr/sbin/sysupgrade", "-n"]).check_run()
run_type
.execute(sudo)
.args(&["/usr/sbin/sysupgrade", "-n"])
.status_checked()
}
pub fn upgrade_packages(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> {
let sudo = require_option(sudo, String::from("No sudo detected"))?;
print_separator("OpenBSD Packages");
run_type.execute(sudo).args(&["/usr/sbin/pkg_add", "-u"]).check_run()
run_type
.execute(sudo)
.args(&["/usr/sbin/pkg_add", "-u"])
.status_checked()
}

View File

@@ -0,0 +1,9 @@
NAME="PCLinuxOS"
VERSION="2022"
ID=pclinuxos
VERSION_ID=2022
ID_LIKE="mandriva"
PRETTY_NAME="PCLinuxOS 2022"
ANSI_COLOR="1;37"
HOME_URL="http://www.pclinuxos.com/"
SUPPORT_URL="http://www.pclinuxos.com/"

View File

@@ -1,33 +1,43 @@
use crate::error::{SkipStep, TopgradeError};
use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, Executor, ExecutorExitStatus, RunType};
use crate::terminal::print_separator;
#[cfg(not(target_os = "macos"))]
use crate::utils::require_option;
use crate::utils::{require, PathExt};
use crate::Step;
use anyhow::Result;
use directories::BaseDirs;
use home;
use ini::Ini;
use log::debug;
use std::fs;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::process::Command;
use std::{env, path::Path};
use crate::command::CommandExt;
use crate::Step;
use color_eyre::eyre::Result;
use directories::BaseDirs;
use home;
use ini::Ini;
use tracing::debug;
use crate::error::SkipStep;
use crate::execution_context::ExecutionContext;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::executor::Executor;
use crate::executor::RunType;
use crate::terminal::print_separator;
#[cfg(not(any(target_os = "android", target_os = "macos")))]
use crate::utils::require_option;
use crate::utils::{require, PathExt};
#[cfg(any(target_os = "linux", target_os = "macos"))]
const INTEL_BREW: &str = "/usr/local/bin/brew";
#[cfg(any(target_os = "linux", target_os = "macos"))]
const ARM_BREW: &str = "/opt/homebrew/bin/brew";
#[derive(Copy, Clone, Debug)]
#[allow(dead_code)]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub enum BrewVariant {
Path,
MacIntel,
MacArm,
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl BrewVariant {
fn binary_name(self) -> &'static str {
match self {
@@ -77,30 +87,42 @@ impl BrewVariant {
}
}
pub fn run_fisher(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
pub fn run_fisher(run_type: RunType) -> Result<()> {
let fish = require("fish")?;
if env::var("fisher_path").is_err() {
base_dirs
.home_dir()
.join(".config/fish/functions/fisher.fish")
.require()?;
}
Command::new(&fish)
.args(["-c", "type -t fisher"])
.output_checked_utf8()
.map(|_| ())
.map_err(|_| SkipStep("`fisher` is not defined in `fish`".to_owned()))?;
Command::new(&fish)
.args(["-c", "echo \"$__fish_config_dir/fish_plugins\""])
.output_checked_utf8()
.and_then(|output| Path::new(&output.stdout.trim()).require().map(|_| ()))
.map_err(|err| SkipStep(format!("`fish_plugins` path doesn't exist: {err}")))?;
Command::new(&fish)
.args(["-c", "fish_update_completions"])
.output_checked_utf8()
.map(|_| ())
.map_err(|_| SkipStep("`fish_update_completions` is not available".to_owned()))?;
print_separator("Fisher");
let version_str = run_type
.execute(&fish)
.args(["-c", "fisher --version"])
.check_output()?;
.output_checked_utf8()?
.stdout;
debug!("Fisher version: {}", version_str);
if version_str.starts_with("fisher version 3.") {
// v3 - see https://github.com/topgrade-rs/topgrade/pull/37#issuecomment-1283844506
run_type.execute(&fish).args(["-c", "fisher"]).check_run()
run_type.execute(&fish).args(["-c", "fisher"]).status_checked()
} else {
// v4
run_type.execute(&fish).args(["-c", "fisher update"]).check_run()
run_type.execute(&fish).args(["-c", "fisher update"]).status_checked()
}
}
@@ -112,7 +134,7 @@ pub fn run_bashit(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type()
.execute("bash")
.args(["-lic", &format!("bash-it update {}", ctx.config().bashit_branch())])
.check_run()
.status_checked()
}
pub fn run_oh_my_fish(ctx: &ExecutionContext) -> Result<()> {
@@ -124,25 +146,27 @@ pub fn run_oh_my_fish(ctx: &ExecutionContext) -> Result<()> {
print_separator("oh-my-fish");
ctx.run_type().execute(fish).args(["-c", "omf update"]).check_run()
ctx.run_type().execute(fish).args(["-c", "omf update"]).status_checked()
}
pub fn run_pkgin(ctx: &ExecutionContext) -> Result<()> {
let pkgin = require("pkgin")?;
print_separator("Pkgin");
let mut command = ctx.run_type().execute(ctx.sudo().as_ref().unwrap());
command.arg(&pkgin).arg("update");
if ctx.config().yes(Step::Pkgin) {
command.arg("-y");
}
command.check_run()?;
command.status_checked()?;
let mut command = ctx.run_type().execute(ctx.sudo().as_ref().unwrap());
command.arg(&pkgin).arg("upgrade");
if ctx.config().yes(Step::Pkgin) {
command.arg("-y");
}
command.check_run()
command.status_checked()
}
pub fn run_fish_plug(ctx: &ExecutionContext) -> Result<()> {
@@ -154,7 +178,10 @@ pub fn run_fish_plug(ctx: &ExecutionContext) -> Result<()> {
print_separator("fish-plug");
ctx.run_type().execute(fish).args(["-c", "plug update"]).check_run()
ctx.run_type()
.execute(fish)
.args(["-c", "plug update"])
.status_checked()
}
/// Upgrades `fundle` and `fundle` plugins.
@@ -171,7 +198,7 @@ pub fn run_fundle(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type()
.execute(fish)
.args(["-c", "fundle self-update && fundle update"])
.check_run()
.status_checked()
}
#[cfg(not(any(target_os = "android", target_os = "macos")))]
@@ -192,10 +219,10 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
"--method",
"org.freedesktop.DBus.ListActivatableNames",
])
.check_output()?;
.output_checked_utf8()?;
debug!("Checking for gnome extensions: {}", output);
if !output.contains("org.gnome.Shell.Extensions") {
if !output.stdout.contains("org.gnome.Shell.Extensions") {
return Err(SkipStep(String::from("Gnome shell extensions are unregistered in DBus")).into());
}
@@ -213,9 +240,10 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
"--method",
"org.gnome.Shell.Extensions.CheckForUpdates",
])
.check_run()
.status_checked()
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()> {
#[allow(unused_variables)]
let binary_name = require(variant.binary_name())?;
@@ -230,18 +258,18 @@ pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<
print_separator(variant.step_title());
let run_type = ctx.run_type();
variant.execute(run_type).arg("update").check_run()?;
variant.execute(run_type).arg("update").status_checked()?;
variant
.execute(run_type)
.args(["upgrade", "--ignore-pinned", "--formula"])
.check_run()?;
.status_checked()?;
if ctx.config().cleanup() {
variant.execute(run_type).arg("cleanup").check_run()?;
variant.execute(run_type).arg("cleanup").status_checked()?;
}
if ctx.config().brew_autoremove() {
variant.execute(run_type).arg("autoremove").check_run()?;
variant.execute(run_type).arg("autoremove").status_checked()?;
}
Ok(())
@@ -259,8 +287,8 @@ pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()>
let cask_upgrade_exists = variant
.execute(RunType::Wet)
.args(["--repository", "buo/cask-upgrade"])
.check_output()
.map(|p| Path::new(p.trim()).exists())?;
.output_checked_utf8()
.map(|p| Path::new(p.stdout.trim()).exists())?;
let mut brew_args = vec![];
@@ -276,10 +304,10 @@ pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()>
}
}
variant.execute(run_type).args(&brew_args).check_run()?;
variant.execute(run_type).args(&brew_args).status_checked()?;
if ctx.config().cleanup() {
variant.execute(run_type).arg("cleanup").check_run()?;
variant.execute(run_type).arg("cleanup").status_checked()?;
}
Ok(())
@@ -290,7 +318,7 @@ pub fn run_guix(ctx: &ExecutionContext) -> Result<()> {
let run_type = ctx.run_type();
let output = Command::new(&guix).arg("pull").check_output();
let output = Command::new(&guix).arg("pull").output_checked_utf8();
debug!("guix pull output: {:?}", output);
let should_upgrade = output.is_ok();
debug!("Can Upgrade Guix: {:?}", should_upgrade);
@@ -298,7 +326,7 @@ pub fn run_guix(ctx: &ExecutionContext) -> Result<()> {
print_separator("Guix");
if should_upgrade {
return run_type.execute(&guix).args(["package", "-u"]).check_run();
return run_type.execute(&guix).args(["package", "-u"]).status_checked();
}
Err(SkipStep(String::from("Guix Pull Failed, Skipping")).into())
}
@@ -314,7 +342,7 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
debug!("nix profile: {:?}", profile_path);
let manifest_json_path = profile_path.join("manifest.json");
let output = Command::new(&nix_env).args(["--query", "nix"]).check_output();
let output = Command::new(&nix_env).args(["--query", "nix"]).output_checked_utf8();
debug!("nix-env output: {:?}", output);
let should_self_upgrade = output.is_ok();
@@ -346,13 +374,13 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
if should_self_upgrade {
if multi_user {
ctx.execute_elevated(&nix, true)?.arg("upgrade-nix").check_run()?;
ctx.execute_elevated(&nix, true)?.arg("upgrade-nix").status_checked()?;
} else {
run_type.execute(&nix).arg("upgrade-nix").check_run()?;
run_type.execute(&nix).arg("upgrade-nix").status_checked()?;
}
}
run_type.execute(nix_channel).arg("--update").check_run()?;
run_type.execute(nix_channel).arg("--update").status_checked()?;
if std::path::Path::new(&manifest_json_path).exists() {
run_type
@@ -360,9 +388,9 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
.arg("profile")
.arg("upgrade")
.arg(".*")
.check_run()
.status_checked()
} else {
run_type.execute(&nix_env).arg("--upgrade").check_run()
run_type.execute(&nix_env).arg("--upgrade").status_checked()
}
}
@@ -371,42 +399,40 @@ pub fn run_yadm(ctx: &ExecutionContext) -> Result<()> {
print_separator("yadm");
ctx.run_type().execute(yadm).arg("pull").check_run()
ctx.run_type().execute(yadm).arg("pull").status_checked()
}
pub fn run_asdf(run_type: RunType) -> Result<()> {
let asdf = require("asdf")?;
print_separator("asdf");
let exit_status = run_type.execute(&asdf).arg("update").spawn()?.wait()?;
run_type.execute(&asdf).arg("update").status_checked_with_codes(&[42])?;
if let ExecutorExitStatus::Wet(e) = exit_status {
if !(e.success() || e.code().map(|c| c == 42).unwrap_or(false)) {
return Err(TopgradeError::ProcessFailed(e).into());
}
}
run_type.execute(&asdf).args(["plugin", "update", "--all"]).check_run()
run_type
.execute(&asdf)
.args(["plugin", "update", "--all"])
.status_checked()
}
pub fn run_home_manager(run_type: RunType) -> Result<()> {
let home_manager = require("home-manager")?;
print_separator("home-manager");
run_type.execute(home_manager).arg("switch").check_run()
run_type.execute(home_manager).arg("switch").status_checked()
}
pub fn run_tldr(run_type: RunType) -> Result<()> {
let tldr = require("tldr")?;
print_separator("TLDR");
run_type.execute(tldr).arg("--update").check_run()
run_type.execute(tldr).arg("--update").status_checked()
}
pub fn run_pearl(run_type: RunType) -> Result<()> {
let pearl = require("pearl")?;
print_separator("pearl");
run_type.execute(pearl).arg("update").check_run()
run_type.execute(pearl).arg("update").status_checked()
}
pub fn run_sdkman(base_dirs: &BaseDirs, cleanup: bool, run_type: RunType) -> Result<()> {
@@ -440,27 +466,33 @@ pub fn run_sdkman(base_dirs: &BaseDirs, cleanup: bool, run_type: RunType) -> Res
run_type
.execute(&bash)
.args(["-c", cmd_selfupdate.as_str()])
.check_run()?;
.status_checked()?;
}
let cmd_update = format!("source {} && sdk update", &sdkman_init_path);
run_type.execute(&bash).args(["-c", cmd_update.as_str()]).check_run()?;
run_type
.execute(&bash)
.args(["-c", cmd_update.as_str()])
.status_checked()?;
let cmd_upgrade = format!("source {} && sdk upgrade", &sdkman_init_path);
run_type.execute(&bash).args(["-c", cmd_upgrade.as_str()]).check_run()?;
run_type
.execute(&bash)
.args(["-c", cmd_upgrade.as_str()])
.status_checked()?;
if cleanup {
let cmd_flush_archives = format!("source {} && sdk flush archives", &sdkman_init_path);
run_type
.execute(&bash)
.args(["-c", cmd_flush_archives.as_str()])
.check_run()?;
.status_checked()?;
let cmd_flush_temp = format!("source {} && sdk flush temp", &sdkman_init_path);
run_type
.execute(&bash)
.args(["-c", cmd_flush_temp.as_str()])
.check_run()?;
.status_checked()?;
}
Ok(())
@@ -471,7 +503,7 @@ pub fn run_bun(ctx: &ExecutionContext) -> Result<()> {
print_separator("Bun");
ctx.run_type().execute(bun).arg("upgrade").check_run()
ctx.run_type().execute(bun).arg("upgrade").status_checked()
}
/// Update dotfiles with `rcm(7)`.
@@ -481,10 +513,10 @@ pub fn run_rcm(ctx: &ExecutionContext) -> Result<()> {
let rcup = require("rcup")?;
print_separator("rcm");
ctx.run_type().execute(rcup).arg("-v").check_run()
ctx.run_type().execute(rcup).arg("-v").status_checked()
}
pub fn reboot() {
pub fn reboot() -> Result<()> {
print!("Rebooting...");
Command::new("sudo").arg("reboot").spawn().unwrap().wait().unwrap();
Command::new("sudo").arg("reboot").status_checked()
}

View File

@@ -2,11 +2,12 @@ use std::convert::TryFrom;
use std::path::Path;
use std::{ffi::OsStr, process::Command};
use anyhow::Result;
use log::debug;
use color_eyre::eyre::Result;
use tracing::debug;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, RunType};
use crate::executor::RunType;
use crate::terminal::{print_separator, print_warning};
use crate::utils::require;
use crate::{error::SkipStep, steps::git::Repositories};
@@ -18,23 +19,22 @@ pub fn run_chocolatey(ctx: &ExecutionContext) -> Result<()> {
print_separator("Chocolatey");
let mut cmd = &choco;
let mut args = vec!["upgrade", "all"];
if let Some(sudo) = ctx.sudo() {
cmd = sudo;
args.insert(0, "choco");
let mut command = match ctx.sudo() {
Some(sudo) => {
let mut command = ctx.run_type().execute(sudo);
command.arg(choco);
command
}
None => ctx.run_type().execute(choco),
};
let mut command = ctx.run_type().execute(cmd);
command.args(&args);
command.args(["upgrade", "all"]);
if yes {
command.arg("--yes");
}
command.check_run()
command.status_checked()
}
pub fn run_winget(ctx: &ExecutionContext) -> Result<()> {
@@ -47,7 +47,10 @@ pub fn run_winget(ctx: &ExecutionContext) -> Result<()> {
return Err(SkipStep(String::from("Winget is disabled by default")).into());
}
ctx.run_type().execute(&winget).args(["upgrade", "--all"]).check_run()
ctx.run_type()
.execute(winget)
.args(["upgrade", "--all"])
.status_checked()
}
pub fn run_scoop(cleanup: bool, run_type: RunType) -> Result<()> {
@@ -55,18 +58,37 @@ pub fn run_scoop(cleanup: bool, run_type: RunType) -> Result<()> {
print_separator("Scoop");
run_type.execute(&scoop).args(["update"]).check_run()?;
run_type.execute(&scoop).args(["update", "*"]).check_run()?;
run_type.execute(&scoop).args(["update"]).status_checked()?;
run_type.execute(&scoop).args(["update", "*"]).status_checked()?;
if cleanup {
run_type.execute(&scoop).args(["cleanup", "*"]).check_run()?;
run_type.execute(&scoop).args(["cleanup", "*"]).status_checked()?;
}
Ok(())
}
pub fn update_wsl(ctx: &ExecutionContext) -> Result<()> {
let wsl = require("wsl")?;
print_separator("Update WSL");
let mut wsl_command = ctx.run_type().execute(wsl);
wsl_command.args(["--update"]);
if ctx.config().wsl_update_pre_release() {
wsl_command.args(["--pre-release"]);
}
if ctx.config().wsl_update_use_web_download() {
wsl_command.args(["--web-download"]);
}
wsl_command.status_checked()?;
Ok(())
}
fn get_wsl_distributions(wsl: &Path) -> Result<Vec<String>> {
let output = Command::new(wsl).args(["--list", "-q"]).check_output()?;
let output = Command::new(wsl).args(["--list", "-q"]).output_checked_utf8()?.stdout;
Ok(output
.lines()
.filter(|s| !s.is_empty())
@@ -77,19 +99,19 @@ fn get_wsl_distributions(wsl: &Path) -> Result<Vec<String>> {
fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> Result<()> {
let topgrade = Command::new(wsl)
.args(["-d", dist, "bash", "-lc", "which topgrade"])
.check_output()
.output_checked_utf8()
.map_err(|_| SkipStep(String::from("Could not find Topgrade installed in WSL")))?;
let mut command = ctx.run_type().execute(wsl);
command
.args(["-d", dist, "bash", "-c"])
.arg(format!("TOPGRADE_PREFIX={} exec {}", dist, topgrade));
.arg(format!("TOPGRADE_PREFIX={dist} exec {topgrade}"));
if ctx.config().yes(Step::Wsl) {
command.arg("-y");
}
command.check_run()
command.status_checked()
}
pub fn run_wsl_topgrade(ctx: &ExecutionContext) -> Result<()> {
@@ -129,12 +151,17 @@ pub fn windows_update(ctx: &ExecutionContext) -> Result<()> {
print_separator("Windows Update");
println!("Running Windows Update. Check the control panel for progress.");
ctx.run_type().execute(&usoclient).arg("ScanInstallWait").check_run()?;
ctx.run_type().execute(&usoclient).arg("StartInstall").check_run()
ctx.run_type()
.execute(&usoclient)
.arg("ScanInstallWait")
.status_checked()?;
ctx.run_type().execute(&usoclient).arg("StartInstall").status_checked()
}
pub fn reboot() {
Command::new("shutdown").args(["/R", "/T", "0"]).spawn().ok();
pub fn reboot() -> Result<()> {
// If this works, it won't return, but if it doesn't work, it may return a useful error
// message.
Command::new("shutdown").args(["/R", "/T", "0"]).status_checked()
}
pub fn insert_startup_scripts(ctx: &ExecutionContext, git_repos: &mut Repositories) -> Result<()> {

View File

@@ -3,10 +3,10 @@ use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use anyhow::Result;
use color_eyre::eyre::Result;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::executor::CommandExt;
use crate::terminal::{is_dumb, print_separator};
use crate::utils::{require_option, which, PathExt};
use crate::Step;
@@ -27,8 +27,8 @@ impl Powershell {
let profile = path.as_ref().and_then(|path| {
Command::new(path)
.args(["-NoProfile", "-Command", "Split-Path $profile"])
.check_output()
.map(|output| PathBuf::from(output.trim()))
.output_checked_utf8()
.map(|output| PathBuf::from(output.stdout.trim()))
.and_then(|p| p.require())
.ok()
});
@@ -50,10 +50,10 @@ impl Powershell {
.args([
"-NoProfile",
"-Command",
&format!("Get-Module -ListAvailable {}", command),
&format!("Get-Module -ListAvailable {command}"),
])
.check_output()
.map(|result| !result.is_empty())
.output_checked_utf8()
.map(|result| !result.stdout.is_empty())
.unwrap_or(false)
}
@@ -81,7 +81,7 @@ impl Powershell {
.execute(powershell)
// This probably doesn't need `shell_words::join`.
.args(["-NoProfile", "-Command", &cmd.join(" ")])
.check_run()
.status_checked()
}
#[cfg(windows)]
@@ -119,6 +119,6 @@ impl Powershell {
}
),
])
.check_run()
.status_checked()
}
}

View File

@@ -1,6 +1,8 @@
use anyhow::Result;
use color_eyre::eyre::Result;
use crate::{error::SkipStep, execution_context::ExecutionContext, terminal::print_separator, utils};
use crate::{
command::CommandExt, error::SkipStep, execution_context::ExecutionContext, terminal::print_separator, utils,
};
fn prepare_async_ssh_command(args: &mut Vec<&str>) {
args.insert(0, "ssh");
@@ -17,14 +19,14 @@ pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> {
args.extend(ssh_arguments.split_whitespace());
}
let env = format!("TOPGRADE_PREFIX={}", hostname);
let env = format!("TOPGRADE_PREFIX={hostname}");
args.extend(["env", &env, "$SHELL", "-lc", topgrade]);
if ctx.config().run_in_tmux() && !ctx.run_type().dry() {
#[cfg(unix)]
{
prepare_async_ssh_command(&mut args);
crate::tmux::run_command(ctx, &shell_words::join(args))?;
crate::tmux::run_command(ctx, hostname, &shell_words::join(args))?;
Err(SkipStep(String::from("Remote Topgrade launched in Tmux")).into())
}
@@ -41,12 +43,12 @@ pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> {
args.extend(ssh_arguments.split_whitespace());
}
let env = format!("TOPGRADE_PREFIX={}", hostname);
let env = format!("TOPGRADE_PREFIX={hostname}");
args.extend(["env", &env, "$SHELL", "-lc", topgrade]);
print_separator(format!("Remote ({})", hostname));
println!("Connecting to {}...", hostname);
print_separator(format!("Remote ({hostname})"));
println!("Connecting to {hostname}...");
ctx.run_type().execute(ssh).args(&args).check_run()
ctx.run_type().execute(ssh).args(&args).status_checked()
}
}

View File

@@ -2,13 +2,13 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use std::{fmt::Display, rc::Rc, str::FromStr};
use anyhow::Result;
use log::{debug, error};
use color_eyre::eyre::Result;
use regex::Regex;
use strum::EnumString;
use tracing::{debug, error};
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::executor::CommandExt;
use crate::terminal::print_separator;
use crate::{error::SkipStep, utils, Step};
@@ -61,10 +61,11 @@ impl Vagrant {
let output = Command::new(&self.path)
.arg("status")
.current_dir(directory)
.check_output()?;
.output_checked_utf8()?;
debug!("Vagrant output in {}: {}", directory, output);
let boxes = output
.stdout
.split('\n')
.skip(2)
.take_while(|line| !(line.is_empty() || line.starts_with('\r')))
@@ -115,7 +116,7 @@ impl<'a> TemporaryPowerOn<'a> {
.execute(vagrant)
.args([subcommand, &vagrant_box.name])
.current_dir(vagrant_box.path.clone())
.check_run()?;
.status_checked()?;
Ok(TemporaryPowerOn {
vagrant,
vagrant_box,
@@ -142,7 +143,7 @@ impl<'a> Drop for TemporaryPowerOn<'a> {
.execute(self.vagrant)
.args([subcommand, &self.vagrant_box.name])
.current_dir(self.vagrant_box.path.clone())
.check_run()
.status_checked()
.ok();
}
}
@@ -182,7 +183,7 @@ pub fn topgrade_vagrant_box(ctx: &ExecutionContext, vagrant_box: &VagrantBox) ->
let mut _poweron = None;
if !vagrant_box.initial_status.powered_on() {
if !(ctx.config().vagrant_power_on().unwrap_or(true)) {
return Err(SkipStep(format!("Skipping powered off box {}", vagrant_box)).into());
return Err(SkipStep(format!("Skipping powered off box {vagrant_box}")).into());
} else {
print_separator(seperator);
_poweron = Some(vagrant.temporary_power_on(vagrant_box, ctx)?);
@@ -199,7 +200,7 @@ pub fn topgrade_vagrant_box(ctx: &ExecutionContext, vagrant_box: &VagrantBox) ->
.execute(&vagrant.path)
.current_dir(&vagrant_box.path)
.args(["ssh", "-c", &command])
.check_run()
.status_checked()
}
pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> {
@@ -208,12 +209,12 @@ pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> {
let outdated = Command::new(&vagrant)
.args(["box", "outdated", "--global"])
.check_output()?;
.output_checked_utf8()?;
let re = Regex::new(r"\* '(.*?)' for '(.*?)' is outdated").unwrap();
let mut found = false;
for ele in re.captures_iter(&outdated) {
for ele in re.captures_iter(&outdated.stdout) {
found = true;
let _ = ctx
.run_type()
@@ -222,13 +223,16 @@ pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> {
.arg(ele.get(1).unwrap().as_str())
.arg("--provider")
.arg(ele.get(2).unwrap().as_str())
.check_run();
.status_checked();
}
if !found {
println!("No outdated boxes")
} else {
ctx.run_type().execute(&vagrant).args(["box", "prune"]).check_run()?;
ctx.run_type()
.execute(&vagrant)
.args(["box", "prune"])
.status_checked()?;
}
Ok(())

View File

@@ -1,16 +1,22 @@
use std::env;
use std::path::PathBuf;
use std::process::Command;
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use directories::BaseDirs;
use crate::command::CommandExt;
use crate::executor::RunType;
use crate::terminal::print_separator;
use crate::{
execution_context::ExecutionContext,
utils::{which, Check, PathExt},
utils::{which, PathExt},
};
use anyhow::Result;
use directories::BaseDirs;
use std::env;
use std::io;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{exit, Command};
#[cfg(unix)]
use std::os::unix::process::CommandExt as _;
pub fn run_tpm(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let tpm = base_dirs
@@ -20,7 +26,7 @@ pub fn run_tpm(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
print_separator("tmux plugins");
run_type.execute(tpm).arg("all").check_run()
run_type.execute(tpm).arg("all").status_checked()
}
struct Tmux {
@@ -44,73 +50,130 @@ impl Tmux {
command
}
fn has_session(&self, session_name: &str) -> Result<bool, io::Error> {
fn has_session(&self, session_name: &str) -> Result<bool> {
Ok(self
.build()
.args(["has-session", "-t", session_name])
.output()?
.output_checked_with(|_| Ok(()))?
.status
.success())
}
fn new_session(&self, session_name: &str) -> Result<bool, io::Error> {
Ok(self
/// Create a new tmux session with the given name, running the given command.
/// The command is passed to `sh` (see "shell-command arguments are sh(1) commands" in the
/// `tmux(1)` man page).
fn new_session(&self, session_name: &str, window_name: &str, command: &str) -> Result<()> {
let _ = self
.build()
.args(["new-session", "-d", "-s", session_name, "-n", "dummy"])
.spawn()?
.wait()?
.success())
// `-d`: initial size comes from the global `default-size` option (instead
// of passing `-x` and `-y` arguments.
// (What do those even do?)
// `-s`: session name
// `-n`: window name (always `topgrade`)
.args(["new-session", "-d", "-s", session_name, "-n", window_name, command])
.output_checked()?;
Ok(())
}
fn run_in_session(&self, command: &str) -> Result<()> {
self.build()
.args(["new-window", "-t", "topgrade", command])
.spawn()?
.wait()?
.check()?;
/// Like [`new_session`] but it appends a digit to the session name (if necessary) to
/// avoid duplicate session names.
///
/// The session name is returned.
fn new_unique_session(&self, session_name: &str, window_name: &str, command: &str) -> Result<String> {
let mut session = session_name.to_owned();
for i in 1.. {
if !self
.has_session(&session)
.context("Error determining if a tmux session exists")?
{
self.new_session(&session, window_name, command)
.context("Error running Topgrade in tmux")?;
return Ok(session);
}
session = format!("{session_name}-{i}");
}
unreachable!()
}
Ok(())
/// Create a new window in the given tmux session, running the given command.
fn new_window(&self, session_name: &str, window_name: &str, command: &str) -> Result<()> {
self.build()
// `-d`: initial size comes from the global `default-size` option (instead
// of passing `-x` and `-y` arguments.
// (What do those even do?)
// `-s`: session name
// `-n`: window name
.args([
"new-window",
"-a",
"-t",
&format!("{session_name}:{window_name}"),
"-n",
window_name,
command,
])
.env_remove("TMUX")
.status_checked()
}
fn window_indices(&self, session_name: &str) -> Result<Vec<usize>> {
self.build()
.args(["list-windows", "-F", "#{window_index}", "-t", session_name])
.output_checked_utf8()?
.stdout
.lines()
.map(|l| l.parse())
.collect::<Result<Vec<usize>, _>>()
.context("Failed to compute tmux windows")
}
}
pub fn run_in_tmux(args: Vec<String>) -> ! {
pub fn run_in_tmux(args: Vec<String>) -> Result<()> {
let command = {
let mut command = vec![
String::from("env"),
String::from("TOPGRADE_KEEP_END=1"),
String::from("TOPGRADE_INSIDE_TMUX=1"),
];
// TODO: Should we use `topgrade` instead of the first argument here, which may be
// a local path?
command.extend(env::args());
shell_words::join(command)
};
let tmux = Tmux::new(args);
if !tmux.has_session("topgrade").expect("Error detecting a tmux session") {
tmux.new_session("topgrade").expect("Error creating a tmux session");
}
tmux.run_in_session(&command).expect("Error running Topgrade in tmux");
tmux.build()
.args(["kill-window", "-t", "topgrade:dummy"])
.output()
.expect("Error killing the dummy tmux window");
// Find an unused session and run `topgrade` in it with the current command's arguments.
let session_name = "topgrade";
let window_name = "topgrade";
let session = tmux.new_unique_session(session_name, window_name, &command)?;
// Only attach to the newly-created session if we're not currently in a tmux session.
if env::var("TMUX").is_err() {
let err = tmux.build().args(["attach", "-t", "topgrade"]).exec();
panic!("{:?}", err);
let err = tmux.build().args(["attach-session", "-t", &session]).exec();
Err(eyre!("{err}")).context("Failed to `execvp(3)` tmux")
} else {
println!("Topgrade launched in a new tmux session");
exit(0);
Ok(())
}
}
pub fn run_command(ctx: &ExecutionContext, command: &str) -> Result<()> {
Tmux::new(ctx.config().tmux_arguments()?)
.build()
.args(["new-window", "-a", "-t", "topgrade:1", command])
.env_remove("TMUX")
.spawn()?
.wait()?
.check()
pub fn run_command(ctx: &ExecutionContext, window_name: &str, command: &str) -> Result<()> {
let tmux = Tmux::new(ctx.config().tmux_arguments()?);
match ctx.get_tmux_session() {
Some(session_name) => {
let indices = tmux.window_indices(&session_name)?;
let last_window = indices
.iter()
.last()
.ok_or_else(|| eyre!("tmux session {session_name} has no windows"))?;
tmux.new_window(&session_name, &format!("{last_window}"), command)?;
}
None => {
let name = tmux.new_unique_session("topgrade", window_name, command)?;
ctx.set_tmux_session(name);
}
}
Ok(())
}

View File

@@ -1,17 +1,20 @@
use anyhow::Result;
use color_eyre::eyre::Result;
use crate::command::CommandExt;
use crate::config::Step;
use crate::terminal::print_separator;
use crate::{execution_context::ExecutionContext, utils::require};
use log::debug;
use std::path::Path;
use std::{path::PathBuf, process::Command};
use tracing::debug;
fn list_toolboxes(toolbx: &Path) -> Result<Vec<String>> {
let output = Command::new(toolbx).args(["list", "--containers"]).output()?;
let output_str = String::from_utf8(output.stdout)?;
let output = Command::new(toolbx)
.args(["list", "--containers"])
.output_checked_utf8()?;
let proc: Vec<String> = output_str
let proc: Vec<String> = output
.stdout
.lines()
// Skip the first line since that contains only status information
.skip(1)
@@ -39,7 +42,7 @@ pub fn run_toolbx(ctx: &ExecutionContext) -> Result<()> {
let topgrade_path = topgrade_path.to_str().unwrap();
for tb in toolboxes.iter() {
let topgrade_prefix = format!("TOPGRADE_PREFIX='Toolbx {}'", tb);
let topgrade_prefix = format!("TOPGRADE_PREFIX='Toolbx {tb}'");
let mut args = vec![
"run",
"-c",
@@ -54,7 +57,7 @@ pub fn run_toolbx(ctx: &ExecutionContext) -> Result<()> {
args.push("--yes");
}
let _output = ctx.run_type().execute(&toolbx).args(&args).check_run();
ctx.run_type().execute(&toolbx).args(&args).status_checked()?;
}
Ok(())

View File

@@ -33,15 +33,29 @@ if exists(":PaqUpdate")
PaqUpdate
endif
if exists(":CocUpdateSync")
if exists(":Lazy")
echo "Lazy Update"
+Lazy! sync
endif
function! UpdateCoCAndTS()
if exists(":CocUpdateSync")
echo "CocUpdateSync"
CocUpdateSync
endif
endif
if exists(":TSUpdateSync")
echo "TreeSitter Update"
TSUpdate
endif
quitall
endfunction
if exists(':PackerSync')
echo "Packer"
autocmd User PackerComplete quitall
autocmd User PackerComplete * call UpdateCoCAndTS()
PackerSync
else
quitall
call UpdateCoCAndTS()
endif

View File

@@ -1,19 +1,20 @@
use crate::command::CommandExt;
use crate::error::{SkipStep, TopgradeError};
use anyhow::Result;
use color_eyre::eyre::Result;
use crate::executor::{CommandExt, Executor, ExecutorOutput, RunType};
use crate::executor::{Executor, ExecutorOutput, RunType};
use crate::terminal::print_separator;
use crate::{
execution_context::ExecutionContext,
utils::{require, PathExt},
};
use directories::BaseDirs;
use log::debug;
use std::path::PathBuf;
use std::{
io::{self, Write},
process::Command,
};
use tracing::debug;
const UPGRADE_VIM: &str = include_str!("upgrade.vim");
@@ -63,7 +64,7 @@ fn upgrade(command: &mut Executor, ctx: &ExecutionContext) -> Result<()> {
}
if !status.success() {
return Err(TopgradeError::ProcessFailed(status).into());
return Err(TopgradeError::ProcessFailed(command.get_program(), status).into());
} else {
println!("Plugins upgraded")
}
@@ -84,22 +85,22 @@ pub fn upgrade_ultimate_vimrc(ctx: &ExecutionContext) -> Result<()> {
.execute(&git)
.current_dir(&config_dir)
.args(["reset", "--hard"])
.check_run()?;
.status_checked()?;
ctx.run_type()
.execute(&git)
.current_dir(&config_dir)
.args(["clean", "-d", "--force"])
.check_run()?;
.status_checked()?;
ctx.run_type()
.execute(&git)
.current_dir(&config_dir)
.args(["pull", "--rebase"])
.check_run()?;
.status_checked()?;
ctx.run_type()
.execute(python)
.current_dir(config_dir)
.arg(update_plugins)
.check_run()?;
.status_checked()?;
Ok(())
}
@@ -107,8 +108,8 @@ pub fn upgrade_ultimate_vimrc(ctx: &ExecutionContext) -> Result<()> {
pub fn upgrade_vim(base_dirs: &BaseDirs, ctx: &ExecutionContext) -> Result<()> {
let vim = require("vim")?;
let output = Command::new(&vim).arg("--version").check_output()?;
if !output.starts_with("VIM") {
let output = Command::new(&vim).arg("--version").output_checked_utf8()?;
if !output.stdout.starts_with("VIM") {
return Err(SkipStep(String::from("vim binary might be actually nvim")).into());
}
@@ -147,5 +148,5 @@ pub fn run_voom(_base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
print_separator("voom");
run_type.execute(voom).arg("update").check_run()
run_type.execute(voom).arg("update").status_checked()
}

View File

@@ -1,16 +1,19 @@
use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, RunType};
use crate::git::Repositories;
use crate::terminal::print_separator;
use crate::utils::{require, PathExt};
use anyhow::Result;
use directories::BaseDirs;
use log::debug;
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
use color_eyre::eyre::Result;
use directories::BaseDirs;
use tracing::debug;
use walkdir::WalkDir;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::executor::RunType;
use crate::git::Repositories;
use crate::terminal::print_separator;
use crate::utils::{require, PathExt};
pub fn run_zr(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let zsh = require("zsh")?;
@@ -19,7 +22,7 @@ pub fn run_zr(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
print_separator("zr");
let cmd = format!("source {} && zr --update", zshrc(base_dirs).display());
run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).check_run()
run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).status_checked()
}
pub fn zshrc(base_dirs: &BaseDirs) -> PathBuf {
@@ -34,7 +37,7 @@ pub fn run_antibody(run_type: RunType) -> Result<()> {
print_separator("antibody");
run_type.execute(antibody).arg("update").check_run()
run_type.execute(antibody).arg("update").status_checked()
}
pub fn run_antigen(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
@@ -48,7 +51,7 @@ pub fn run_antigen(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
print_separator("antigen");
let cmd = format!("source {} && (antigen selfupdate ; antigen update)", zshrc.display());
run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).check_run()
run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).status_checked()
}
pub fn run_zgenom(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
@@ -62,7 +65,7 @@ pub fn run_zgenom(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
print_separator("zgenom");
let cmd = format!("source {} && zgenom selfupdate && zgenom update", zshrc.display());
run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).check_run()
run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).status_checked()
}
pub fn run_zplug(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
@@ -76,7 +79,10 @@ pub fn run_zplug(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
print_separator("zplug");
run_type.execute(zsh).args(["-i", "-c", "zplug update"]).check_run()
run_type
.execute(zsh)
.args(["-i", "-c", "zplug update"])
.status_checked()
}
pub fn run_zinit(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
@@ -91,7 +97,7 @@ pub fn run_zinit(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
print_separator("zinit");
let cmd = format!("source {} && zinit self-update && zinit update --all", zshrc.display(),);
run_type.execute(zsh).args(["-i", "-c", cmd.as_str()]).check_run()
run_type.execute(zsh).args(["-i", "-c", cmd.as_str()]).status_checked()
}
pub fn run_zi(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
@@ -103,7 +109,7 @@ pub fn run_zi(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
print_separator("zi");
let cmd = format!("source {} && zi self-update && zi update --all", zshrc.display(),);
run_type.execute(zsh).args(["-i", "-c", &cmd]).check_run()
run_type.execute(zsh).args(["-i", "-c", &cmd]).status_checked()
}
pub fn run_zim(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
@@ -111,8 +117,10 @@ pub fn run_zim(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
env::var("ZIM_HOME")
.or_else(|_| {
Command::new("zsh")
// TODO: Should these be quoted?
.args(["-c", "[[ -n ${ZIM_HOME} ]] && print -n ${ZIM_HOME}"])
.check_output()
.output_checked_utf8()
.map(|o| o.stdout)
})
.map(PathBuf::from)
.unwrap_or_else(|_| base_dirs.home_dir().join(".zim"))
@@ -123,7 +131,7 @@ pub fn run_zim(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
run_type
.execute(zsh)
.args(["-i", "-c", "zimfw upgrade && zimfw update"])
.check_run()
.status_checked()
}
pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
@@ -135,8 +143,10 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
let custom_dir = env::var::<_>("ZSH_CUSTOM")
.or_else(|_| {
Command::new("zsh")
// TODO: Should these be quoted?
.args(["-c", "test $ZSH_CUSTOM && echo -n $ZSH_CUSTOM"])
.check_output()
.output_checked_utf8()
.map(|o| o.stdout)
})
.map(PathBuf::from)
.unwrap_or_else(|e| {
@@ -168,5 +178,5 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
.execute("zsh")
.env("ZSH", &oh_my_zsh)
.arg(&oh_my_zsh.join("tools/upgrade.sh"))
.check_run_with_codes(&[80])
.status_checked_with_codes(&[80])
}

108
src/sudo.rs Normal file
View File

@@ -0,0 +1,108 @@
use std::ffi::OsStr;
use std::path::Path;
use std::path::PathBuf;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::executor::Executor;
use crate::terminal::print_separator;
use crate::utils::which;
#[derive(Clone, Debug)]
pub struct Sudo {
/// The path to the `sudo` binary.
path: PathBuf,
/// The type of program being used as `sudo`.
kind: SudoKind,
}
impl Sudo {
/// Get the `sudo` binary for this platform.
pub fn detect() -> Option<Self> {
which("doas")
.map(|p| (p, SudoKind::Doas))
.or_else(|| which("sudo").map(|p| (p, SudoKind::Sudo)))
.or_else(|| which("gsudo").map(|p| (p, SudoKind::Gsudo)))
.or_else(|| which("pkexec").map(|p| (p, SudoKind::Pkexec)))
.map(|(path, kind)| Self { path, kind })
}
/// Elevate permissions with `sudo`.
///
/// This helps prevent blocking `sudo` prompts from stopping the run in the middle of a
/// step.
///
/// See: https://github.com/topgrade-rs/topgrade/issues/205
pub fn elevate(&self, ctx: &ExecutionContext) -> Result<()> {
print_separator("Sudo");
let mut cmd = ctx.run_type().execute(self);
match self.kind {
SudoKind::Doas => {
// `doas` doesn't have anything like `sudo -v` to cache credentials,
// so we just execute a dummy `echo` command so we have something
// unobtrusive to run.
// See: https://man.openbsd.org/doas
cmd.arg("echo");
}
SudoKind::Sudo => {
// From `man sudo` on macOS:
// -v, --validate
// Update the user's cached credentials, authenticating the user
// if necessary. For the sudoers plugin, this extends the sudo
// timeout for another 5 minutes by default, but does not run a
// command. Not all security policies support cached credentials.
cmd.arg("-v");
}
SudoKind::Gsudo => {
// Shows current user, cache and console status.
// See: https://gerardog.github.io/gsudo/docs/usage
cmd.arg("status");
}
SudoKind::Pkexec => {
// I don't think this does anything; `pkexec` usually asks for
// authentication every time, although it can be configured
// differently.
//
// See the note for `doas` above.
//
// See: https://linux.die.net/man/1/pkexec
cmd.arg("echo");
}
}
cmd.status_checked().wrap_err("Failed to elevate permissions")
}
/// Execute a command with `sudo`.
pub fn execute_elevated(&self, ctx: &ExecutionContext, command: &Path, interactive: bool) -> Executor {
let mut cmd = ctx.run_type().execute(self);
if let SudoKind::Sudo = self.kind {
cmd.arg("--preserve-env=DIFFPROG");
}
if interactive {
cmd.arg("-i");
}
cmd.arg(command);
cmd
}
}
#[derive(Clone, Copy, Debug)]
enum SudoKind {
Doas,
Sudo,
Gsudo,
Pkexec,
}
impl AsRef<OsStr> for Sudo {
fn as_ref(&self) -> &OsStr {
self.path.as_ref()
}
}

View File

@@ -8,18 +8,22 @@ use std::sync::Mutex;
use std::time::Duration;
use chrono::{Local, Timelike};
use color_eyre::eyre;
use color_eyre::eyre::Context;
use console::{style, Key, Term};
use lazy_static::lazy_static;
use log::{debug, error};
#[cfg(target_os = "macos")]
use notify_rust::{Notification, Timeout};
use tracing::{debug, error};
#[cfg(windows)]
use which_crate::which;
use crate::command::CommandExt;
use crate::report::StepResult;
#[cfg(target_os = "linux")]
use crate::terminal;
#[cfg(target_os = "linux")]
use crate::utils::which;
lazy_static! {
static ref TERMINAL: Mutex<Terminal> = Mutex::new(Terminal::new());
}
@@ -34,13 +38,8 @@ pub fn shell() -> &'static str {
which("pwsh").map(|_| "pwsh").unwrap_or("powershell")
}
pub fn run_shell() {
Command::new(shell())
.env("IN_TOPGRADE", "1")
.spawn()
.unwrap()
.wait()
.unwrap();
pub fn run_shell() -> eyre::Result<()> {
Command::new(shell()).env("IN_TOPGRADE", "1").status_checked()
}
struct Terminal {
@@ -61,7 +60,7 @@ impl Terminal {
width: term.size_checked().map(|(_, w)| w),
term,
prefix: env::var("TOPGRADE_PREFIX")
.map(|prefix| format!("({}) ", prefix))
.map(|prefix| format!("({prefix}) "))
.unwrap_or_else(|_| String::new()),
set_title: true,
display_time: true,
@@ -106,7 +105,9 @@ impl Terminal {
}
command.args(["-a", "Topgrade", "Topgrade"]);
command.arg(message.as_ref());
command.output().ok();
if let Err(err) = command.output_checked() {
terminal::print_warning("Sending notification failed with {err:?}");
}
}
}
}
@@ -142,7 +143,7 @@ impl Terminal {
.write_fmt(format_args!(
"{}\n",
style(format_args!(
"\n―― {} {:^border$}",
"\n── {} {:^border$}",
message,
"",
border = max(
@@ -158,11 +159,24 @@ impl Terminal {
.ok();
}
None => {
self.term.write_fmt(format_args!("―― {} ――\n", message)).ok();
self.term.write_fmt(format_args!("―― {message} ――\n")).ok();
}
}
}
#[allow(dead_code)]
fn print_error<P: AsRef<str>, Q: AsRef<str>>(&mut self, key: Q, message: P) {
let key = key.as_ref();
let message = message.as_ref();
self.term
.write_fmt(format_args!(
"{} {}",
style(format!("{key} failed:")).red().bold(),
message
))
.ok();
}
#[allow(dead_code)]
fn print_warning<P: AsRef<str>>(&mut self, message: P) {
let message = message.as_ref();
@@ -201,7 +215,7 @@ impl Terminal {
self.term
.write_fmt(format_args!(
"{}",
style(format!("{} (y)es/(N)o", question,)).yellow().bold()
style(format!("{question} (y)es/(N)o",)).yellow().bold()
))
.ok();
@@ -214,7 +228,7 @@ impl Terminal {
}
}
#[allow(unused_variables)]
fn should_retry(&mut self, interrupted: bool, step_name: &str) -> Result<bool, io::Error> {
fn should_retry(&mut self, interrupted: bool, step_name: &str) -> eyre::Result<bool> {
if self.width.is_none() {
return Ok(false);
}
@@ -223,31 +237,35 @@ impl Terminal {
self.term.set_title("Topgrade - Awaiting user");
}
self.notify_desktop(format!("{} failed", step_name), None);
if self.desktop_notification {
self.notify_desktop(format!("{step_name} failed"), None);
}
self.term
.write_fmt(format_args!(
"\n{}",
style(format!("{}Retry? (y)es/(N)o/(s)hell/(q)uit", self.prefix))
let prompt_inner = style(format!("{}Retry? (y)es/(N)o/(s)hell/(q)uit", self.prefix))
.yellow()
.bold()
))
.ok();
.bold();
self.term.write_fmt(format_args!("\n{prompt_inner}")).ok();
let answer = loop {
match self.term.read_key() {
Ok(Key::Char('y')) | Ok(Key::Char('Y')) => break Ok(true),
Ok(Key::Char('s')) | Ok(Key::Char('S')) => {
println!("\n\nDropping you to shell. Fix what you need and then exit the shell.\n");
run_shell();
if let Err(err) = run_shell().context("Failed to run shell") {
self.term.write_fmt(format_args!("{err:?}\n{prompt_inner}")).ok();
} else {
break Ok(true);
}
}
Ok(Key::Char('n')) | Ok(Key::Char('N')) | Ok(Key::Enter) => break Ok(false),
Err(e) => {
error!("Error reading from terminal: {}", e);
break Ok(false);
}
Ok(Key::Char('q')) | Ok(Key::Char('Q')) => return Err(io::Error::from(io::ErrorKind::Interrupted)),
Ok(Key::Char('q')) | Ok(Key::Char('Q')) => {
return Err(io::Error::from(io::ErrorKind::Interrupted)).context("Quit from user input")
}
_ => (),
}
};
@@ -268,7 +286,7 @@ impl Default for Terminal {
}
}
pub fn should_retry(interrupted: bool, step_name: &str) -> Result<bool, io::Error> {
pub fn should_retry(interrupted: bool, step_name: &str) -> eyre::Result<bool> {
TERMINAL.lock().unwrap().should_retry(interrupted, step_name)
}
@@ -276,6 +294,11 @@ pub fn print_separator<P: AsRef<str>>(message: P) {
TERMINAL.lock().unwrap().print_separator(message)
}
#[allow(dead_code)]
pub fn print_error<P: AsRef<str>, Q: AsRef<str>>(key: Q, message: P) {
TERMINAL.lock().unwrap().print_error(key, message)
}
#[allow(dead_code)]
pub fn print_warning<P: AsRef<str>>(message: P) {
TERMINAL.lock().unwrap().print_warning(message)

View File

@@ -1,46 +1,11 @@
use crate::error::{SkipStep, TopgradeError};
use anyhow::Result;
use crate::error::SkipStep;
use color_eyre::eyre::Result;
use log::{debug, error};
use std::env;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::process::{ExitStatus, Output};
pub trait Check {
fn check(self) -> Result<()>;
}
impl Check for Output {
fn check(self) -> Result<()> {
self.status.check()
}
}
pub trait CheckWithCodes {
fn check_with_codes(self, codes: &[i32]) -> Result<()>;
}
// Anything that implements CheckWithCodes also implements check
// if check_with_codes is given an empty array of codes to check
impl<T: CheckWithCodes> Check for T {
fn check(self) -> Result<()> {
self.check_with_codes(&[])
}
}
impl CheckWithCodes for ExitStatus {
fn check_with_codes(self, codes: &[i32]) -> Result<()> {
// Set the default to be -1 because the option represents a signal termination
let code = self.code().unwrap_or(-1);
if self.success() || codes.contains(&code) {
Ok(())
} else {
Err(TopgradeError::ProcessFailed(self).into())
}
}
}
use tracing::{debug, error};
pub trait PathExt
where
@@ -102,13 +67,6 @@ pub fn which<T: AsRef<OsStr> + Debug>(binary_name: T) -> Option<PathBuf> {
}
}
pub fn sudo() -> Option<PathBuf> {
which("doas")
.or_else(|| which("sudo"))
.or_else(|| which("gsudo"))
.or_else(|| which("pkexec"))
}
pub fn editor() -> Vec<String> {
env::var("EDITOR")
.unwrap_or_else(|_| String::from(if cfg!(windows) { "notepad" } else { "vi" }))
@@ -142,3 +100,55 @@ pub fn require_option<T>(option: Option<T>, cause: String) -> Result<T> {
Err(SkipStep(cause).into())
}
}
/* sys-info-rs
*
* Copyright (c) 2015 Siyu Wang
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#[cfg(target_family = "unix")]
pub fn hostname() -> Result<String> {
use std::ffi;
extern crate libc;
unsafe {
let buf_size = libc::sysconf(libc::_SC_HOST_NAME_MAX) as usize;
let mut buf = Vec::<u8>::with_capacity(buf_size + 1);
if libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf_size) < 0 {
return Err(SkipStep(format!("Failed to get hostname: {}", std::io::Error::last_os_error())).into());
}
let hostname_len = libc::strnlen(buf.as_ptr() as *const libc::c_char, buf_size);
buf.set_len(hostname_len);
Ok(ffi::CString::new(buf).unwrap().into_string().unwrap())
}
}
#[cfg(target_family = "windows")]
pub fn hostname() -> Result<String> {
use crate::command::CommandExt;
use std::process::Command;
Command::new("hostname")
.output_checked_utf8()
.map_err(|err| SkipStep(format!("Failed to get hostname: {err}")).into())
.map(|output| output.stdout.trim().to_owned())
}

View File

@@ -1,80 +0,0 @@
.hy
.TH "topgrade" "8"
.SH NAME
.PP
Topgrade \- Upgrade everything
.SH SYNOPSIS
.PP
topgrade [\fIoptions\f[]]
.SH DESCRIPTION
.PP
Keeping your system up to date usually involves invoking multiple package managers.
This results in big, non-portable shell one-liners saved in your shell.
To remedy this, \fBTopgrade\fR detects which tools you use and runs the appropriate commands to update them.
.SH OPTIONS
.TP
.B \-\-only <only>
Run only specific steps
.RS
.RE
.TP
.B \-\-disable <disable>
Disable specific steps
.RS
.RE
.TP
.B \-c, \-\-cleanup
Cleanup temporary or old files
.RS
.RE
.TP
.B \-n, \-\-dry\-run
List the commands that would be run
.RS
.RE
.TP
.B \-\-edit\-config
Edit the configuration file
.RS
.RE
.TP
.B \-h, \-\-help
Print help information
.RS
.RE
.TP
.B \-k, \-\-keep
Prompt for a key before exiting
.RS
.RE
.TP
.B \-\-no\-retry
Do not ask to retry failed steps
.RS
.RE
.TP
.B \-t, \-\-tmux
Run inside tmux
.RS
.RE
.TP
.B \-V, \-\-version
Print version information
.RS
.RE
.TP
.B \-v, \-\-verbose
Output logs
.RS
.RE
.B \-y, \-\-yes
Skip package manager's prompts (experimental)
.SH ARGUMENT FORMAT
Options can be given in any order.
A list of steps must be provided as a list of separate arguments, i.e. 'topgrade --only system shell'.
.SH BUGS
For a list of bugs see <\fIhttps://github.com/r-darwish/topgrade/issues\fR>.
.SH AUTHOR
\fBTopgrade\fR is maintained by Roey Dror (\[aq]r\-darwish\[aq]) and many other contributors.
You can view the full list at
<\fIhttps://github.com/r-darwish/topgrade/graphs/contributors\fR>