Compare commits

..

1 Commits

Author SHA1 Message Date
Thomas Schönauer
9e694d8e13 Added last version note (#75) 2022-10-26 11:33:43 +00:00
73 changed files with 2475 additions and 5451 deletions

View File

@@ -2,79 +2,32 @@
name: Bug report
about: Topgrade is misbehaving
title: ''
labels: 'bug'
labels: ''
assignees: ''
---
<!--
Thanks for taking the time to fill out this bug report!
Please make sure to
[search for existing issues](https://github.com/topgrade-rs/topgrade/issues)
before filing a new one!
<!-- If you're here to report about a "No asset found" error, please make sure that an hour has been passed since the last release was made. -->
Questions labeled with `Optional` can be skipped.
-->
## What did you expect to happen?
<!--
If you're here to report about a "No asset found" error, please make sure that
an hour has been passed since the last release was made.
-->
## Erroneous Behavior
<!--
What actually happened?
-->
## What actually happened?
## Expected Behavior
<!--
Describe the expected behavior
-->
## Steps to reproduce
<!--
A minimal example to reproduce the issue
-->
## Possible Cause (Optional)
<!--
If you know the possible cause of the issue, please tell us.
-->
## Problem persists without calling from topgrade
<!--
Execute the erroneous command directly to see if the problem persists
-->
- [ ] Yes
## Configuration file (Optional)
<!--
Paste your configuration file inside the code block if you think this issue is
related to configuration.
-->
```toml
```
## Additional Details
- Operation System/Version
<!-- For example, Fedora Linux 38 -->
- Which operating system or Linux distribution are you using?
- How did you install Topgrade?
- Which version are you running? <!-- Check with `topgrade -V` -->
- Installation
<!--
How did you install topgrade: build from repo / crates.io (cargo install topgrade)
/ package manager (which one) / other (describe)
-->
- Topgrade version (`topgrade -V`)
## Verbose Output (`topgrade -v`)
<!--
Paste the verbose output into the pre-tags
Run `topgrade --dry-run` to see which commands Topgrade is running.
If the command seems wrong and you know why please tell us so.
If the command seems fine try to run it yourself and tell us if you got a different result from Topgrade.
-->
<details>
<!-- Paste the output of the problematic command with `-v` into the pre-tags -->
<pre>
</pre>

View File

@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: GitHub Discussions
url: https://github.com/topgrade-rs/topgrade/discussions
about: Please ask and answer questions here.

View File

@@ -1,7 +1,6 @@
## Standards checklist:
- [ ] The PR title is descriptive.
- [ ] I have read `CONTRIBUTING.md`
- [ ] The code compiles (`cargo build`)
- [ ] The code passes rustfmt (`cargo fmt`)
- [ ] The code passes clippy (`cargo clippy`)

24
.github/workflows/build-and-test.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Cargo Build & Test
on:
push:
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
build_and_test:
name: Rust project - latest
runs-on: ubuntu-latest
strategy:
matrix:
toolchain:
- stable
- beta
- nightly
steps:
- uses: actions/checkout@v3
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- run: cargo build --verbose
- run: cargo test --verbose

View File

@@ -4,88 +4,52 @@ on:
branches:
- main
name: CI
env:
RUST_VER: '1.68.0'
CROSS_VER: '0.2.5'
CARGO_NET_RETRY: 3
name: Check and Lint
jobs:
check:
name: Check
runs-on: ubuntu-latest
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
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: '${{ env.RUST_VER }}'
components: rustfmt
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- 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:
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
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: '${{ env.RUST_VER }}'
toolchain: stable
components: clippy
- name: Setup Rust Cache
uses: Swatinem/rust-cache@v2
override: true
- uses: actions-rs/clippy-check@v1
with:
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
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
name: Clippy Output

View File

@@ -1,27 +0,0 @@
on:
release:
types: [published, edited]
name: Check SemVer compliance
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2022-08-03
override: true
components: rustfmt, clippy
semver:
runs-on: ubuntu-latest
steps:
- uses: actions-rs/cargo@v1
with:
command: install
args: --git https://github.com/rust-lang/rust-semverver
- run: eval "current_version=$(grep -e '^version = .*$' Cargo.toml | cut -d ' ' -f 3)"
- run: cargo semver | tee semver_out
- run: (head -n 1 semver_out | grep "\-> $current_version") || (echo "versioning mismatch" && return 1)

View File

@@ -1,25 +1,30 @@
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
release:
types: [published, edited]
name: Publish to crates.io on release
name: Check SemVer compliance and publish on release
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
toolchain: nightly-2022-08-03
override: true
publish:
semver:
runs-on: ubuntu-latest
steps:
- uses: actions-rs/cargo@v1
with:
command: install
args: --git https://github.com/rust-lang/rust-semverver
- run: eval "current_version=$(grep -e '^version = .*$' Cargo.toml | cut -d ' ' -f 3)"
- run: cargo semver | tee semver_out
- run: (head -n 1 semver_out | grep "\-> $current_version") || (echo "versioning mismatch" && return 1)
publish:
steps:
- uses: katyo/publish-crates@v1
with:

View File

@@ -1,70 +0,0 @@
name: Publish release files for non-cd-native environments
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
release:
types: [ created ]
jobs:
build:
strategy:
fail-fast: false
matrix:
target: [ "aarch64-unknown-linux-gnu", "armv7-unknown-linux-gnueabihf", "x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl", "x86_64-unknown-freebsd", ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
default: true
override: true
target: ${{ matrix.target }}
components: rustfmt, clippy
- uses: actions-rs/cargo@v1.0.1
name: Check format
with:
use-cross: true
command: fmt
args: --all -- --check
- uses: actions-rs/cargo@v1.0.1
name: Run clippy
with:
command: clippy
use-cross: true
args: --all-targets --locked --target ${{matrix.target}} -- -D warnings
- uses: actions-rs/cargo@v1.0.1
name: Run clippy (All features)
with:
command: clippy
use-cross: true
args: --locked --all-features --target ${{matrix.target}} -- -D warnings
- uses: actions-rs/cargo@v1.0.1
name: Run tests
with:
command: test
use-cross: true
args: --target ${{matrix.target}}
- uses: actions-rs/cargo@v1.0.1
name: Build
with:
command: build
use-cross: true
args: --release --all-features --target ${{matrix.target}}
- name: Rename Release
run: |
mkdir assets
FILENAME=topgrade-${{github.event.release.tag_name}}-${{matrix.target}}
mv target/${{matrix.target}}/release/topgrade assets
cd assets
tar --format=ustar -czf $FILENAME.tar.gz topgrade
rm topgrade
ls .
- name: Release
uses: softprops/action-gh-release@v1
with:
files: assets/*

View File

@@ -0,0 +1,28 @@
on:
workflow_dispatch:
push:
branches:
- main
name: Release Packaging
jobs:
release:
name: Release Packaging
env:
PROJECT_NAME_UNDERSCORE: topgrade-rs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Release Build
run: cargo build --release
- name: 'Upload Artifact'
uses: actions/upload-artifact@v2
with:
name: ${{ env.PROJECT_NAME_UNDERSCORE }}
path: target/release/${{ env.PROJECT_NAME_UNDERSCORE }}

View File

@@ -1,10 +1,7 @@
name: Publish release files for CD native environments
name: CD Win/MacOS Native
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
workflow_dispatch:
release:
types: [ created ]
@@ -13,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-latest, macos-latest, windows-latest ]
platform: [ macos-latest, windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
@@ -23,21 +20,6 @@ jobs:
profile: minimal
override: true
components: rustfmt, clippy
- uses: actions-rs/cargo@v1.0.1
name: Check format
with:
command: fmt
args: --all -- --check
- uses: actions-rs/cargo@v1.0.1
name: Run clippy
with:
command: clippy
args: --all-targets --locked -- -D warnings
- uses: actions-rs/cargo@v1.0.1
name: Run clippy (All features)
with:
command: clippy
args: --all-targets --locked --all-features -- -D warnings
- uses: actions-rs/cargo@v1.0.1
name: Run tests
with:

24
.github/workflows/rust-ubuntu.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Rust
on:
push:
branches: [ "master", "dev" ]
pull_request:
branches: [ "master", "dev" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
- name: Fmt
run: cargo fmt --check --all
- name: Run tests
run: cargo test --verbose

View File

@@ -1,12 +1,10 @@
on:
pull_request:
push:
branches:
- main
env:
CARGO_TERM_COLOR: always
name: Test with Code Coverage
jobs:
@@ -57,10 +55,3 @@ jobs:
# token: ${{ secrets.CODECOV_TOKEN }}
files: ./lcov.info
fail_ci_if_error: true
- name: Test creation of config file
run: |
CONFIG_PATH=~/.config/topgrade.toml;
if [ -f "$CONFIG_PATH" ]; then rm $CONFIG_PATH; fi
cargo build;
./target/debug/topgrade --dry-run --only system;
stat $CONFIG_PATH;

View File

@@ -1,22 +0,0 @@
name: Publish to AUR
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
push:
tags:
- "v*"
jobs:
aur-publish:
runs-on: ubuntu-latest
steps:
- name: Publish AUR package
uses: ATiltedTree/create-aur-release@v1
with:
package_name: topgrade
commit_username: "Thomas Schönauer"
commit_email: t.schoenauer@hgs-wt.at
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}

View File

@@ -1,38 +0,0 @@
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

6
.vscode/launch.json vendored
View File

@@ -12,11 +12,11 @@
"cargo": {
"args": [
"build",
"--bin=topgrade-rs",
"--package=topgrade-rs"
"--bin=topgrade",
"--package=topgrade"
],
"filter": {
"name": "topgrade-rs",
"name": "topgrade",
"kind": "bin"
}
},

View File

@@ -1,128 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
open an issue on GitHub .
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -1,134 +0,0 @@
## Contributing to `topgrade`
Thank you for your interest in contributing to `topgrade`! We welcome and encourage
contributions of all kinds, such as:
1. Issue reports or feature requests
2. Documentation improvements
3. Code (PR or PR Review)
## Adding a new `step`
In `topgrade`'s term, package manager is called `step`. To add a new `step` to
`topgrade`:
1. Add a new variant to
[`enum Step`](https://github.com/topgrade-rs/topgrade/blob/cb7adc8ced8a77addf2cb051d18bba9f202ab866/src/config.rs#L100)
```rust
pub enum Step {
// Existed steps
// ...
// Your new step here!
// You may want it to be sorted alphabetically because that looks great:)
Xxx,
}
```
2. Implement the update function
You need to find the appropriate location where this update function goes, it should be
a file under [`src/steps`](https://github.com/topgrade-rs/topgrade/tree/master/src/steps),
the file names are self-explanatory, for example, `step`s related to `zsh` are
placed in [`steps/zsh.rs`](https://github.com/topgrade-rs/topgrade/blob/master/src/steps/zsh.rs).
Then you implement the update function, and put it in the file where it belongs.
```rust
pub fn run_xxx(ctx: &ExecutionContext) -> Result<()> {
// Check if this step is installed, if not, then this update will be skipped.
let xxx = require("xxx")?;
// Print the separator
print_separator("xxx");
// Invoke the new step to get things updated!
ctx.run_type()
.execute("xxx")
.arg(/* args required by this step */)
.status_checked()
}
```
Such a update function would be conventionally named `run_xxx()`, where `xxx`
is the name of the new step, and it should take a argument of type
`&ExecutionContext`, this is adequate for most cases unless some extra stuff is
needed (You can find some examples where extra arguments are needed
[here](https://github.com/topgrade-rs/topgrade/blob/7e48c5dedcfd5d0124bb9f39079a03e27ed23886/src/main.rs#L201-L219)).
Update function would usually do 3 things:
1. Check if the step is installed
2. Output the Separator
3. Invoke the step
Still, this is sufficient for most tools, but you may need some extra stuff
with complicated `step`.
3. Finally, invoke that update function in `main.rs`
```rust
runner.execute(Step::Xxx, "xxx", || ItsModule::run_xxx(&ctx))?;
```
We use [conditional compilation](https://doc.rust-lang.org/reference/conditional-compilation.html)
to separate the steps, for example, for steps that are Linux-only, it goes
like this:
```
#[cfg(target_os = "linux")]
{
// Xxx is Linux-only
runner.execute(Step::Xxx, "xxx", || ItsModule::run_xxx(&ctx))?;
}
```
Congrats, you just added a new `step`:)
## Modification to the configuration entries
If your PR has the configuration options
(in [`src/config.rs`](https://github.com/topgrade-rs/topgrade/blob/master/src/config.rs))
modified:
1. Adding new options
2. Changing the existing options
Be sure to apply your changes to
[`config.example.toml`](https://github.com/topgrade-rs/topgrade/blob/master/config.example.toml),
and have some basic documentations guiding user how to use these options.
## Before you submit your PR
Make sure your patch passes the following tests on your host:
```shell
$ cargo build
$ cargo fmt
$ cargo clippy
$ cargo test
```
Don't worry about other platforms, we have most of them covered in our CI.
## Some tips
1. Locale
Some `step` respects locale, which means their output can be in language other
than English, we should not do check on it.
For example, one may want to check if a tool works by doing this:
```rust
let output = Command::new("xxx").arg("--help").output().unwrap();
let stdout = from_utf8(output.stdout).expect("Assume it is UTF-8 encoded");
if stdout.contains("help") {
// xxx works
}
```
If `xxx` respects locale, then the above code should work on English system,
on a system that does not use English, e.g., it uses Chinese, that `"help"` may be
translated to `"帮助"`, and the above code won't work.

1546
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
[package]
name = "topgrade"
description = "Upgrade all the things"
name = "topgrade-rs"
description = "Upgrade all the things, successor of topgrade"
categories = ["os"]
keywords = ["upgrade", "update"]
license = "GPL-3.0"
license-file = "LICENSE"
repository = "https://github.com/topgrade-rs/topgrade"
version = "12.0.0"
version = "10.0.1"
authors = ["Roey Darwish Dror <roey.ghost@gmail.com>", "Thomas Schönauer <t.schoenauer@hgs-wt.at>"]
exclude = ["doc/screenshot.gif"]
edition = "2021"
@@ -16,60 +16,44 @@ readme = "README.md"
name = "topgrade"
path = "src/main.rs"
##[lib]
##name = "topgrade_lib"
[dependencies]
home = "~0.5"
etcetera = "~0.8"
once_cell = "~1.17"
serde = { version = "~1.0", features = ["derive"] }
home = "0.5"
directories = "4.0"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
which_crate = { version = "~4.1", package = "which" }
shellexpand = "~2.1"
clap = { version = "~3.1", features = ["cargo", "derive"] }
clap_complete = "~3.1"
clap_mangen = "~0.1"
walkdir = "~2.3"
console = "~0.15"
lazy_static = "~1.4"
chrono = "~0.4"
glob = "~0.3"
strum = { version = "~0.24", features = ["derive"] }
thiserror = "~1.0"
tempfile = "~3.2"
cfg-if = "~1.0"
tokio = { version = "~1.18", features = ["process", "rt-multi-thread"] }
futures = "~0.3"
regex = "~1.7"
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"] }
merge = "~0.1"
regex-split = "~0.1"
notify-rust = "~4.8"
which_crate = { version = "4.1", package = "which" }
shellexpand = "2.1"
clap = { version = "3.1", features = ["cargo", "derive"] }
log = "0.4"
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"] }
futures = "0.3"
regex = "1.5"
sys-info = "0.9"
semver = "1.0"
[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(target_os = "macos")'.dependencies]
notify-rust = "4.5"
[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"] }
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"] }
[target.'cfg(windows)'.dependencies]
self_update_crate = { version = "~0.30", default-features = false, optional = true, package = "self_update", features = ["archive-zip", "compression-zip-deflate", "rustls"] }
winapi = "~0.3"
parselnk = "~0.1"
self_update_crate = { version = "0.30", default-features = false, optional = true, package = "self_update", features = ["archive-zip", "compression-zip-deflate", "rustls"] }
winapi = "0.3"
parselnk = "0.1"
[profile.release]
lto = true

114
README.md
View File

@@ -1,109 +1,59 @@
<div align="center">
<h1>
<img alt="Topgrade" src="doc/topgrade_transparent.png" width="850px">
</h1>
![Topgrade](doc/topgrade.png)
<!---
![GitHub release](https://img.shields.io/github/release/r-darwish/topgrade.svg)
[![Crates.io](https://img.shields.io/crates/v/topgrade.svg)](https://crates.io/crates/topgrade)
[![AUR](https://img.shields.io/aur/version/topgrade.svg)](https://aur.archlinux.org/packages/topgrade/)
![homebrew](https://img.shields.io/homebrew/v/topgrade.svg) -->
--->
<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>
![Demo](doc/screenshot.gif)
<img alt="Demo" src="doc/screenshot.gif" width="550px">
</div>
## Fork
This is a fork of [topgrade by r-darwish](https://github.com/r-darwish/topgrade) to keep it maintained.
## Maintainers Wanted
I currently have not enough time to maintain this project on the level required and which the project deserves. For this reason I'm asking the community to help supporting the project, to help and work on resolving issues and create new features. Thanks for all your help.
## Introduction
> **Note**
> This is a fork of [topgrade by r-darwish](https://github.com/r-darwish/topgrade) to keep it maintained.
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, **Topgrade** detects which tools you use and runs the appropriate commands to update them.
To remedy this, _topgrade_ detects which tools you use and runs the appropriate commands to update them.
## Installation
- Arch Linux: [AUR](https://aur.archlinux.org/packages/topgrade/) package.
- NixOS: _topgrade_ package in `nixpkgs`.
- macOS: [Homebrew](https://brew.sh/) or [MacPorts](https://www.macports.org/install.php).
[![Packaging status](https://repology.org/badge/vertical-allrepos/topgrade.svg)](https://repology.org/project/topgrade/versions)
- 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/)
- Windows: [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/topgrade.json)
Other systems users can either use `cargo install` or the compiled binaries from the release page.
Other systems users can either use `cargo install` or use the compiled binaries from the release page.
The compiled binaries contain a self-upgrading feature.
Topgrade requires Rust 1.60 or above.
Topgrade requires Rust 1.51 or above.
## Documentation[WIP]
You can visit the documentation at [topgrade-rs.github.io](https://topgrade-rs.github.io/) .
## Usage
Just run `topgrade`.
See [the wiki](https://github.com/r-darwish/topgrade/wiki/Step-list) 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
## Configuration
## Customization
See `config.example.toml` for an example configuration file.
### Configuration Path
### Configuration path
#### `CONFIG_DIR` on each platform
- **Windows**: `%APPDATA%`
- **macOS** and **other Unix systems**: `${XDG_CONFIG_HOME:-~/.config}`
The configuration should be placed in the following paths depending by the operating system:
`topgrade` will look for the configuration file in the following places, in order of priority:
1. `CONFIG_DIR/topgrade.toml`
2. `CONFIG_DIR/topgrade/topgrade.toml`
If the file with higher priority is present, no matter it is valid or not, the other configuration files will be ignored.
On the first run(no configuration file exists), `topgrade` will create a configuration file at `CONFIG_DIR/topgrade.toml` for you.
### Custom Commands
Custom commands can be defined in the config file which can be run before, during, or after the inbuilt commands, as required.
By default, the custom commands are run using a new shell according to the `$SHELL` environment variable on unix (falls back to `sh`) or `pwsh` on windows (falls back to `powershell`).
On unix, if you want to run your command using an interactive shell, for example to source your shell's rc files, you can add `-i` at the start of your custom command.
But note that this requires the command to exit the shell correctly or else the shell will hang indefinitely.
## 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.
* **Windows** - `%APPDATA%/topgrade.toml`
* **macOS** and **other Unix systems** - `${XDG_CONFIG_HOME:-~/.config}/topgrade.toml`
## Contribution
### Problems or missing features?
Open a new issue describing your problem and if possible provide a solution.
Open a new Issue describing your problem and if possible with a possible 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?
Just fork the repository and start coding.
### Contribution Guidelines
See [CONTRIBUTING.md](https://github.com/topgrade-rs/topgrade/blob/master/CONTRIBUTING.md)
## Roadmap
- [ ] Add a proper testing framework to the code base.
- [ ] Add unit tests for package managers.
- [ ] Split up code into more maintainable parts, eg. putting every linux package manager in a own submodule of linux.rs.
## 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.

View File

@@ -1,11 +0,0 @@
# 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: |

View File

@@ -1,5 +0,0 @@
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

@@ -1,10 +1,3 @@
# Include any additional configuration file(s)
# [include] sections are processed in the order you write them
# Files in $CONFIG_DIR/topgrade/topgrade.d/ are automatically included at the beginning of this file
[include]
#paths = ["/etc/topgrade.toml"]
[misc]
# Don't ask for confirmations
#assume_yes = true
@@ -20,13 +13,6 @@
# Do not ask to retry failed steps (default: false)
#no_retry = true
# Sudo command to be used
#sudo_command = "sudo"
# 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
@@ -54,9 +40,6 @@
# Skip sending a notification at the end of a run
#skip_notify = true
# Whether to self update (this is ignored if the binary has been built without self update support, available also via setting the environment variable TOPGRADE_NO_SELF_UPGRADE)
#no_self_update = true
[git]
#max_concurrency = 5
# Additional git repositories to pull
@@ -81,22 +64,18 @@
# Custom commands
[commands]
#"Python Environment" = "~/dev/.env/bin/pip install -i https://pypi.python.org/simple -U --upgrade-strategy eager jupyter"
#"Custom command using interactive shell (unix)" = "-i vim_upgrade"
[brew]
#greedy_cask = true
#autoremove = true
[linux]
# Arch Package Manager to use. Allowed values: autodetect, aura, garuda_update, pacman, pamac, paru, pikaur, trizen, yay.
# Arch Package Manager to use. Allowed values: autodetect, trizen, aura, paru, yay, pikaur, pacman, pamac.
#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 = ""
@@ -105,22 +84,12 @@
#emerge_sync_flags = "-q"
#emerge_update_flags = "-uDNa --with-bdeps=y world"
#redhat_distro_sync = false
#suse_dup = false
#rpm_ostree = false
#nix_arguments = "--flake"
[python]
#enable_pip_review = true ###disabled by default
#enable_pip_review_local = true ###disabled by default
#enable_pipupgrade = true ###disabled by default
#pipupgrade_arguments = "-y -u --pip-path pip" ###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
@@ -138,7 +107,3 @@
[flatpak]
# Use sudo for updating the system-wide installation
#use_sudo = true
[distrobox]
#use_root = false
#containers = ["archlinux-latest"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,244 +0,0 @@
//! 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
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,10 @@
use lazy_static::lazy_static;
use std::sync::atomic::{AtomicBool, Ordering};
/// A global variable telling whether the application has been interrupted.
static INTERRUPTED: AtomicBool = AtomicBool::new(false);
lazy_static! {
/// A global variable telling whether the application has been interrupted.
static ref INTERRUPTED: AtomicBool = AtomicBool::new(false);
}
/// Tells whether the program has been interrupted
pub fn interrupted() -> bool {

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) } {
tracing::error!("Cannot set a control C handler")
log::error!("Cannot set a control C handler")
}
}

View File

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

View File

@@ -1,38 +1,51 @@
#![allow(dead_code)]
use crate::executor::RunType;
use crate::git::Git;
use crate::sudo::Sudo;
use crate::utils::{require_option, REQUIRE_SUDO};
use crate::utils::require_option;
use crate::{config::Config, executor::Executor};
use color_eyre::eyre::Result;
use std::path::Path;
use std::sync::Mutex;
use anyhow::Result;
use directories::BaseDirs;
use std::path::{Path, PathBuf};
pub struct ExecutionContext<'a> {
run_type: RunType,
sudo: Option<Sudo>,
sudo: &'a Option<PathBuf>,
git: &'a Git,
config: &'a Config,
/// 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>>,
base_dirs: &'a BaseDirs,
}
impl<'a> ExecutionContext<'a> {
pub fn new(run_type: RunType, sudo: Option<Sudo>, git: &'a Git, config: &'a Config) -> Self {
Self {
pub fn new(
run_type: RunType,
sudo: &'a Option<PathBuf>,
git: &'a Git,
config: &'a Config,
base_dirs: &'a BaseDirs,
) -> ExecutionContext<'a> {
ExecutionContext {
run_type,
sudo,
git,
config,
tmux_session: Mutex::new(None),
base_dirs,
}
}
pub fn execute_elevated(&self, command: &Path, interactive: bool) -> Result<Executor> {
let sudo = require_option(self.sudo.as_ref(), REQUIRE_SUDO.to_string())?;
Ok(sudo.execute_elevated(self, command, interactive))
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)
}
pub fn run_type(&self) -> RunType {
@@ -43,19 +56,15 @@ impl<'a> ExecutionContext<'a> {
self.git
}
pub fn sudo(&self) -> &Option<Sudo> {
&self.sudo
pub fn sudo(&self) -> &Option<PathBuf> {
self.sudo
}
pub fn config(&self) -> &Config {
self.config
}
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()
pub fn base_dirs(&self) -> &BaseDirs {
self.base_dirs
}
}

View File

@@ -1,13 +1,11 @@
//! 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, Output};
use color_eyre::eyre::Result;
use tracing::debug;
use crate::command::CommandExt;
use crate::error::DryRun;
use std::process::{Child, Command, ExitStatus};
/// An enum telling whether Topgrade should perform dry runs or actually perform the steps.
#[derive(Clone, Copy, Debug)]
@@ -58,16 +56,6 @@ 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 {
@@ -151,7 +139,7 @@ impl Executor {
let result = match self {
Executor::Wet(c) => {
debug!("Running {:?}", c);
c.spawn_checked().map(ExecutorChild::Wet)?
c.spawn().map(ExecutorChild::Wet)?
}
Executor::Dry(c) => {
c.dry_run();
@@ -165,7 +153,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_checked()?)),
Executor::Wet(c) => Ok(ExecutorOutput::Wet(c.output()?)),
Executor::Dry(c) => {
c.dry_run();
Ok(ExecutorOutput::Dry)
@@ -173,28 +161,23 @@ impl Executor {
}
}
/// An extension of `status_checked` that allows you to set a sequence of codes
/// 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
/// that can indicate success of a script
#[allow(dead_code)]
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 fn check_run_with_codes(&mut self, codes: &[i32]) -> Result<()> {
self.spawn()?.wait()?.check_with_codes(codes)
}
}
pub enum ExecutorOutput {
Wet(Output),
Wet(std::process::Output),
Dry,
}
@@ -211,12 +194,11 @@ impl DryCommand {
print!(
"Dry running: {} {}",
self.program.to_string_lossy(),
shell_words::join(
self.args
.iter()
.map(|a| String::from(a.to_string_lossy()))
.collect::<Vec<String>>()
)
.join(" ")
);
match &self.directory {
Some(dir) => println!(" in {}", dir.to_string_lossy()),
@@ -231,33 +213,78 @@ pub enum ExecutorChild {
Dry,
}
impl CommandExt for Executor {
type Child = ExecutorChild;
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,
};
// 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<(), ()>) -> Result<Output> {
match self {
Executor::Wet(c) => c.output_checked_with(succeeded),
Executor::Dry(c) => {
c.dry_run();
Err(DryRun().into())
}
}
}
fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> Result<()> {
match self {
Executor::Wet(c) => c.status_checked_with(succeeded),
Executor::Dry(c) => {
c.dry_run();
Ok(())
}
}
}
fn spawn_checked(&mut self) -> Result<Self::Child> {
self.spawn()
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());
}
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)?)
}
}

View File

@@ -1,2 +0,0 @@
//pub mod steps;
//pub mod utils;

View File

@@ -2,20 +2,14 @@
use std::env;
use std::io;
use std::path::PathBuf;
use std::process::exit;
use std::time::Duration;
use clap::CommandFactory;
use anyhow::{anyhow, Result};
use clap::{crate_version, Parser};
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use console::Key;
#[cfg(windows)]
use etcetera::base_strategy::Windows;
use etcetera::base_strategy::{BaseStrategy, Xdg};
use once_cell::sync::Lazy;
use tracing::debug;
use log::debug;
use log::LevelFilter;
use pretty_env_logger::formatted_timed_builder;
use self::config::{CommandLineArgs, Config, Step};
use self::error::StepFailed;
@@ -24,7 +18,6 @@ use self::error::Upgraded;
use self::steps::{remote::*, *};
use self::terminal::*;
mod command;
mod config;
mod ctrlc;
mod error;
@@ -37,35 +30,16 @@ mod self_renamer;
#[cfg(feature = "self-update")]
mod self_update;
mod steps;
mod sudo;
mod terminal;
mod utils;
pub static HOME_DIR: Lazy<PathBuf> = Lazy::new(|| home::home_dir().expect("No home directory"));
pub static XDG_DIRS: Lazy<Xdg> = Lazy::new(|| Xdg::new().expect("No home directory"));
#[cfg(windows)]
pub static WINDOWS_DIRS: Lazy<Windows> = Lazy::new(|| Windows::new().expect("No home directory"));
fn run() -> Result<()> {
color_eyre::install()?;
ctrlc::set_handler();
let base_dirs = directories::BaseDirs::new().ok_or_else(|| anyhow!("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 io::stdout());
return Ok(());
}
if opt.gen_manpage {
let man = clap_mangen::Man::new(CommandLineArgs::command());
man.render(&mut 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();
@@ -73,20 +47,28 @@ 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()?;
Config::edit(&base_dirs)?;
return Ok(());
};
if opt.show_config_reference() {
print!("{}", config::EXAMPLE_CONFIG);
print!("{}", crate::config::EXAMPLE_CONFIG);
return Ok(());
}
let config = Config::load(opt)?;
set_title(config.set_title());
display_time(config.display_time());
set_desktop_notifications(config.notify_each_step());
let config = Config::load(&base_dirs, opt)?;
terminal::set_title(config.set_title());
terminal::display_time(config.display_time());
terminal::set_desktop_notifications(config.notify_each_step());
debug!("Version: {}", crate_version!());
debug!("OS: {}", env!("TARGET"));
@@ -97,29 +79,23 @@ fn run() -> Result<()> {
if config.run_in_tmux() && env::var("TOPGRADE_INSIDE_TMUX").is_err() {
#[cfg(unix)]
{
tmux::run_in_tmux(config.tmux_arguments()?)?;
return Ok(());
tmux::run_in_tmux(config.tmux_arguments());
}
}
let git = git::Git::new();
let mut git_repos = git::Repositories::new(&git);
let powershell = powershell::Powershell::new();
let should_run_powershell = powershell.profile().is_some() && config.should_run(Step::Powershell);
let emacs = emacs::Emacs::new();
#[cfg(target_os = "linux")]
let distribution = linux::Distribution::detect();
let sudo = config.sudo_command().map_or_else(sudo::Sudo::detect, sudo::Sudo::new);
let sudo = utils::sudo();
let run_type = executor::RunType::new(config.dry_run());
let ctx = execution_context::ExecutionContext::new(run_type, sudo, &git, &config);
let ctx = execution_context::ExecutionContext::new(run_type, &sudo, &git, &config, &base_dirs);
let mut runner = runner::Runner::new(&ctx);
#[cfg(feature = "self-update")]
{
let config_self_upgrade = env::var("TOPGRADE_NO_SELF_UPGRADE").is_err() && !config.no_self_update();
if !run_type.dry() && config_self_upgrade {
if !run_type.dry() && env::var("TOPGRADE_NO_SELF_UPGRADE").is_err() {
let result = self_update::self_update();
if let Err(e) = &result {
@@ -129,7 +105,7 @@ fn run() -> Result<()> {
return result;
}
}
print_warning(format!("Self update error: {e}"));
print_warning(format!("Self update error: {}", e));
}
}
}
@@ -147,42 +123,31 @@ 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))?;
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})"), || {
ssh::ssh_step(&ctx, remote_topgrade)
runner.execute(Step::Remotes, format!("Remote ({})", remote_topgrade), || {
remote::ssh::ssh_step(&ctx, remote_topgrade)
})?;
}
}
#[cfg(windows)]
{
runner.execute(Step::Wsl, "WSL", || windows::run_wsl_topgrade(&ctx))?;
runner.execute(Step::WslUpdate, "WSL", || windows::update_wsl(&ctx))?;
runner.execute(Step::Chocolatey, "Chocolatey", || windows::run_chocolatey(&ctx))?;
runner.execute(Step::Scoop, "Scoop", || windows::run_scoop(&ctx))?;
runner.execute(Step::Winget, "Winget", || windows::run_winget(&ctx))?;
runner.execute(Step::System, "Windows update", || windows::windows_update(&ctx))?;
}
#[cfg(target_os = "linux")]
{
// NOTE: Due to breaking `nu` updates, `packer.nu` needs to be updated before `nu` get updated
// by other package managers.
runner.execute(Step::Shell, "packer.nu", || linux::run_packer_nu(&ctx))?;
let distribution = linux::Distribution::detect();
#[cfg(target_os = r#"linux"#)]
{
match &distribution {
Ok(distribution) => {
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))?;
@@ -190,21 +155,13 @@ fn run() -> Result<()> {
runner.execute(Step::BrewFormula, "Brew", || {
unix::run_brew_formula(&ctx, unix::BrewVariant::Path)
})?;
}
runner.execute(Step::AM, "am", || linux::run_am(&ctx))?;
runner.execute(Step::AppMan, "appman", || linux::run_appman(&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::run_flatpak(&ctx))?;
runner.execute(Step::Snap, "snap", || linux::run_snap(&ctx))?;
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))?;
runner.execute(Step::System, "pihole", || linux::run_pihole_update(&ctx))?;
runner.execute(Step::Firmware, "Firmware upgrades", || linux::run_fwupdmgr(&ctx))?;
runner.execute(Step::Restarts, "Restarts", || linux::run_needrestart(&ctx))?;
#[cfg(windows)]
{
runner.execute(Step::Chocolatey, "Chocolatey", || windows::run_chocolatey(&ctx))?;
runner.execute(Step::Scoop, "Scoop", || windows::run_scoop(config.cleanup(), run_type))?;
runner.execute(Step::Winget, "Winget", || windows::run_winget(&ctx))?;
}
#[cfg(target_os = "macos")]
@@ -228,35 +185,6 @@ fn run() -> Result<()> {
unix::run_brew_cask(&ctx, unix::BrewVariant::Path)
})?;
runner.execute(Step::Macports, "MacPorts", || macos::run_macports(&ctx))?;
runner.execute(Step::Sparkle, "Sparkle", || macos::run_sparkle(&ctx))?;
runner.execute(Step::Mas, "App Store", || macos::run_mas(&ctx))?;
runner.execute(Step::System, "System upgrade", || macos::upgrade_macos(&ctx))?;
}
#[cfg(target_os = "dragonfly")]
{
runner.execute(Step::Pkg, "DragonFly BSD Packages", || {
dragonfly::upgrade_packages(&ctx)
})?;
dragonfly::audit_packages(&ctx)?;
}
#[cfg(target_os = "freebsd")]
{
runner.execute(Step::Pkg, "FreeBSD Packages", || freebsd::upgrade_packages(&ctx))?;
runner.execute(Step::System, "FreeBSD Upgrade", || freebsd::upgrade_freebsd(&ctx))?;
freebsd::audit_packages(&ctx)?;
}
#[cfg(target_os = "openbsd")]
{
runner.execute(Step::Pkg, "OpenBSD Packages", || openbsd::upgrade_packages(&ctx))?;
runner.execute(Step::System, "OpenBSD Upgrade", || openbsd::upgrade_openbsd(&ctx))?;
}
#[cfg(target_os = "android")]
{
runner.execute(Step::Pkg, "Termux Packages", || android::upgrade_packages(&ctx))?;
}
#[cfg(unix)]
@@ -264,107 +192,27 @@ fn run() -> Result<()> {
runner.execute(Step::Yadm, "yadm", || unix::run_yadm(&ctx))?;
runner.execute(Step::Nix, "nix", || unix::run_nix(&ctx))?;
runner.execute(Step::Guix, "guix", || unix::run_guix(&ctx))?;
runner.execute(Step::HomeManager, "home-manager", || unix::run_home_manager(&ctx))?;
runner.execute(Step::Asdf, "asdf", || unix::run_asdf(&ctx))?;
runner.execute(Step::HomeManager, "home-manager", || unix::run_home_manager(run_type))?;
runner.execute(Step::Asdf, "asdf", || unix::run_asdf(run_type))?;
runner.execute(Step::Pkgin, "pkgin", || unix::run_pkgin(&ctx))?;
runner.execute(Step::Bun, "bun", || unix::run_bun(&ctx))?;
runner.execute(Step::Shell, "zr", || zsh::run_zr(&ctx))?;
runner.execute(Step::Shell, "antibody", || zsh::run_antibody(&ctx))?;
runner.execute(Step::Shell, "antidote", || zsh::run_antidote(&ctx))?;
runner.execute(Step::Shell, "antigen", || zsh::run_antigen(&ctx))?;
runner.execute(Step::Shell, "zgenom", || zsh::run_zgenom(&ctx))?;
runner.execute(Step::Shell, "zplug", || zsh::run_zplug(&ctx))?;
runner.execute(Step::Shell, "zinit", || zsh::run_zinit(&ctx))?;
runner.execute(Step::Shell, "zi", || zsh::run_zi(&ctx))?;
runner.execute(Step::Shell, "zim", || zsh::run_zim(&ctx))?;
runner.execute(Step::Shell, "oh-my-zsh", || zsh::run_oh_my_zsh(&ctx))?;
runner.execute(Step::Shell, "oh-my-bash", || unix::run_oh_my_bash(&ctx))?;
runner.execute(Step::Shell, "fisher", || unix::run_fisher(&ctx))?;
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))?;
runner.execute(Step::Shell, "fundle", || unix::run_fundle(&ctx))?;
runner.execute(Step::Tmux, "tmux", || tmux::run_tpm(&ctx))?;
runner.execute(Step::Tldr, "TLDR", || unix::run_tldr(&ctx))?;
runner.execute(Step::Pearl, "pearl", || unix::run_pearl(&ctx))?;
#[cfg(not(any(target_os = "macos", target_os = "android")))]
runner.execute(Step::GnomeShellExtensions, "Gnome Shell Extensions", || {
unix::upgrade_gnome_extensions(&ctx)
})?;
runner.execute(Step::Sdkman, "SDKMAN!", || unix::run_sdkman(&ctx))?;
runner.execute(Step::Rcm, "rcm", || unix::run_rcm(&ctx))?;
runner.execute(Step::Maza, "maza", || unix::run_maza(&ctx))?;
}
#[cfg(not(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
{
runner.execute(Step::Atom, "apm", || generic::run_apm(&ctx))?;
}
// The following update function should be executed on all OSes.
runner.execute(Step::Fossil, "fossil", || generic::run_fossil(&ctx))?;
runner.execute(Step::Rustup, "rustup", || generic::run_rustup(&ctx))?;
runner.execute(Step::Juliaup, "juliaup", || generic::run_juliaup(&ctx))?;
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(&ctx))?;
runner.execute(Step::Go, "go-global-update", || go::run_go_global_update(&ctx))?;
runner.execute(Step::Go, "gup", || go::run_go_gup(&ctx))?;
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(&ctx))?;
runner.execute(Step::Pipx, "pipx", || generic::run_pipx_update(&ctx))?;
runner.execute(Step::Conda, "conda", || generic::run_conda_update(&ctx))?;
runner.execute(Step::Mamba, "mamba", || generic::run_mamba_update(&ctx))?;
runner.execute(Step::Pip3, "pip3", || generic::run_pip3_update(&ctx))?;
runner.execute(Step::PipReview, "pip-review", || generic::run_pip_review_update(&ctx))?;
runner.execute(Step::PipReviewLocal, "pip-review (local)", || {
generic::run_pip_review_local_update(&ctx)
#[cfg(target_os = "dragonfly")]
runner.execute(Step::Pkg, "DragonFly BSD Packages", || {
dragonfly::upgrade_packages(sudo.as_ref(), run_type)
})?;
runner.execute(Step::Pipupgrade, "pipupgrade", || generic::run_pipupgrade_update(&ctx))?;
runner.execute(Step::Ghcup, "ghcup", || generic::run_ghcup_update(&ctx))?;
runner.execute(Step::Stack, "stack", || generic::run_stack_update(&ctx))?;
runner.execute(Step::Tlmgr, "tlmgr", || generic::run_tlmgr_update(&ctx))?;
runner.execute(Step::Myrepos, "myrepos", || generic::run_myrepos_update(&ctx))?;
runner.execute(Step::Chezmoi, "chezmoi", || generic::run_chezmoi_update(&ctx))?;
runner.execute(Step::Jetpack, "jetpack", || generic::run_jetpack(&ctx))?;
runner.execute(Step::Vim, "vim", || vim::upgrade_vim(&ctx))?;
runner.execute(Step::Vim, "Neovim", || vim::upgrade_neovim(&ctx))?;
runner.execute(Step::Vim, "The Ultimate vimrc", || vim::upgrade_ultimate_vimrc(&ctx))?;
runner.execute(Step::Vim, "voom", || vim::run_voom(&ctx))?;
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::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(&ctx))?;
runner.execute(Step::Helm, "helm", || generic::run_helm_repo_update(&ctx))?;
runner.execute(Step::Gem, "gem", || generic::run_gem(&ctx))?;
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))?;
runner.execute(Step::Stew, "stew", || generic::run_stew(&ctx))?;
runner.execute(Step::Rtcl, "rtcl", || generic::run_rtcl(&ctx))?;
runner.execute(Step::Bin, "bin", || generic::bin_update(&ctx))?;
runner.execute(Step::Gcloud, "gcloud", || generic::run_gcloud_components_update(&ctx))?;
runner.execute(Step::Micro, "micro", || generic::run_micro(&ctx))?;
runner.execute(Step::Raco, "raco", || generic::run_raco_update(&ctx))?;
runner.execute(Step::Spicetify, "spicetify", || generic::spicetify_upgrade(&ctx))?;
runner.execute(Step::GithubCliExtensions, "GitHub CLI Extensions", || {
generic::run_ghcli_extensions_upgrade(&ctx)
})?;
runner.execute(Step::Bob, "Bob", || generic::run_bob(&ctx))?;
#[cfg(target_os = "freebsd")]
runner.execute(Step::Pkg, "FreeBSD Packages", || {
freebsd::upgrade_packages(sudo.as_ref(), run_type)
})?;
#[cfg(target_os = "android")]
runner.execute(Step::Pkg, "Termux Packages", || android::upgrade_packages(&ctx))?;
let emacs = emacs::Emacs::new(&base_dirs);
if config.use_predefined_git_repos() {
if config.should_run(Step::Emacs) {
if !emacs.is_doom() {
@@ -372,43 +220,39 @@ fn run() -> Result<()> {
git_repos.insert_if_repo(directory);
}
}
git_repos.insert_if_repo(HOME_DIR.join(".doom.d"));
git_repos.insert_if_repo(base_dirs.home_dir().join(".doom.d"));
}
if config.should_run(Step::Vim) {
git_repos.insert_if_repo(HOME_DIR.join(".vim"));
git_repos.insert_if_repo(HOME_DIR.join(".config/nvim"));
git_repos.insert_if_repo(base_dirs.home_dir().join(".vim"));
git_repos.insert_if_repo(base_dirs.home_dir().join(".config/nvim"));
}
git_repos.insert_if_repo(HOME_DIR.join(".ideavimrc"));
git_repos.insert_if_repo(HOME_DIR.join(".intellimacs"));
if config.should_run(Step::Rcm) {
git_repos.insert_if_repo(HOME_DIR.join(".dotfiles"));
}
git_repos.insert_if_repo(base_dirs.home_dir().join(".ideavimrc"));
git_repos.insert_if_repo(base_dirs.home_dir().join(".intellimacs"));
#[cfg(unix)]
{
git_repos.insert_if_repo(zsh::zshrc());
git_repos.insert_if_repo(zsh::zshrc(&base_dirs));
if config.should_run(Step::Tmux) {
git_repos.insert_if_repo(HOME_DIR.join(".tmux"));
git_repos.insert_if_repo(base_dirs.home_dir().join(".tmux"));
}
git_repos.insert_if_repo(HOME_DIR.join(".config/fish"));
git_repos.insert_if_repo(XDG_DIRS.config_dir().join("openbox"));
git_repos.insert_if_repo(XDG_DIRS.config_dir().join("bspwm"));
git_repos.insert_if_repo(XDG_DIRS.config_dir().join("i3"));
git_repos.insert_if_repo(XDG_DIRS.config_dir().join("sway"));
git_repos.insert_if_repo(base_dirs.home_dir().join(".config/fish"));
git_repos.insert_if_repo(base_dirs.config_dir().join("openbox"));
git_repos.insert_if_repo(base_dirs.config_dir().join("bspwm"));
git_repos.insert_if_repo(base_dirs.config_dir().join("i3"));
git_repos.insert_if_repo(base_dirs.config_dir().join("sway"));
}
#[cfg(windows)]
git_repos.insert_if_repo(
WINDOWS_DIRS
.cache_dir()
base_dirs
.data_local_dir()
.join("Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState"),
);
#[cfg(windows)]
windows::insert_startup_scripts(&mut git_repos).ok();
windows::insert_startup_scripts(&ctx, &mut git_repos).ok();
if let Some(profile) = powershell.profile() {
git_repos.insert_if_repo(profile);
@@ -432,6 +276,100 @@ fn run() -> Result<()> {
})?;
}
#[cfg(unix)]
{
runner.execute(Step::Shell, "zr", || zsh::run_zr(&base_dirs, run_type))?;
runner.execute(Step::Shell, "antibody", || zsh::run_antibody(run_type))?;
runner.execute(Step::Shell, "antigen", || zsh::run_antigen(&base_dirs, run_type))?;
runner.execute(Step::Shell, "zgenom", || zsh::run_zgenom(&base_dirs, run_type))?;
runner.execute(Step::Shell, "zplug", || zsh::run_zplug(&base_dirs, run_type))?;
runner.execute(Step::Shell, "zinit", || zsh::run_zinit(&base_dirs, run_type))?;
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, "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))?;
runner.execute(Step::Tmux, "tmux", || tmux::run_tpm(&base_dirs, run_type))?;
runner.execute(Step::Tldr, "TLDR", || unix::run_tldr(run_type))?;
runner.execute(Step::Pearl, "pearl", || unix::run_pearl(run_type))?;
#[cfg(not(any(target_os = "macos", target_os = "android")))]
runner.execute(Step::GnomeShellExtensions, "Gnome Shell Extensions", || {
unix::upgrade_gnome_extensions(&ctx)
})?;
runner.execute(Step::Sdkman, "SDKMAN!", || {
unix::run_sdkman(&base_dirs, config.cleanup(), run_type)
})?;
}
#[cfg(not(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
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::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::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::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::Stack, "stack", || generic::run_stack_update(run_type))?;
runner.execute(Step::Tlmgr, "tlmgr", || generic::run_tlmgr_update(&ctx))?;
runner.execute(Step::Myrepos, "myrepos", || {
generic::run_myrepos_update(&base_dirs, run_type)
})?;
runner.execute(Step::Chezmoi, "chezmoi", || {
generic::run_chezmoi_update(&base_dirs, run_type)
})?;
runner.execute(Step::Jetpack, "jetpack", || generic::run_jetpack(run_type))?;
runner.execute(Step::Vim, "vim", || vim::upgrade_vim(&base_dirs, &ctx))?;
runner.execute(Step::Vim, "Neovim", || vim::upgrade_neovim(&base_dirs, &ctx))?;
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::Node, "npm", || node::run_npm_upgrade(&ctx))?;
runner.execute(Step::Node, "yarn", || node::run_yarn_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::Gem, "gem", || generic::run_gem(&base_dirs, run_type))?;
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))?;
runner.execute(Step::Rtcl, "rtcl", || generic::run_rtcl(&ctx))?;
runner.execute(Step::Bin, "bin", || generic::bin_update(&ctx))?;
runner.execute(Step::Gcloud, "gcloud", || {
generic::run_gcloud_components_update(run_type)
})?;
runner.execute(Step::Micro, "micro", || generic::run_micro(run_type))?;
runner.execute(Step::Raco, "raco", || generic::run_raco_update(run_type))?;
runner.execute(Step::Spicetify, "spicetify", || generic::spicetify_upgrade(&ctx))?;
runner.execute(Step::GithubCliExtensions, "GitHub CLI Extensions", || {
generic::run_ghcli_extensions_upgrade(&ctx)
})?;
#[cfg(target_os = "linux")]
{
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::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))?;
}
if let Some(commands) = config.commands() {
for (name, command) in commands {
if config.should_run_custom_command(name) {
@@ -442,6 +380,32 @@ fn run() -> Result<()> {
}
}
#[cfg(target_os = "linux")]
{
runner.execute(Step::System, "pihole", || {
linux::run_pihole_update(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)
})?;
}
#[cfg(target_os = "macos")]
{
runner.execute(Step::Sparkle, "Sparkle", || macos::run_sparkle(&ctx))?;
runner.execute(Step::Mas, "App Store", || macos::run_mas(run_type))?;
runner.execute(Step::System, "System upgrade", || macos::upgrade_macos(&ctx))?;
}
#[cfg(target_os = "freebsd")]
runner.execute(Step::System, "FreeBSD Upgrade", || {
freebsd::upgrade_freebsd(sudo.as_ref(), run_type)
})?;
#[cfg(windows)]
runner.execute(Step::System, "Windows update", || windows::windows_update(&ctx))?;
if config.should_run(Step::Vagrant) {
if let Ok(boxes) = vagrant::collect_boxes(&ctx) {
for vagrant_box in boxes {
@@ -466,6 +430,12 @@ fn run() -> Result<()> {
distribution.show_summary();
}
}
#[cfg(target_os = "freebsd")]
freebsd::audit_packages(&sudo).ok();
#[cfg(target_os = "dragonfly")]
dragonfly::audit_packages(&sudo).ok();
}
let mut post_command_failed = false;
@@ -482,10 +452,10 @@ fn run() -> Result<()> {
loop {
match get_key() {
Ok(Key::Char('s')) | Ok(Key::Char('S')) => {
run_shell().context("Failed to execute shell")?;
run_shell();
}
Ok(Key::Char('r')) | Ok(Key::Char('R')) => {
reboot().context("Failed to reboot")?;
reboot();
}
Ok(Key::Char('q')) | Ok(Key::Char('Q')) => (),
_ => {
@@ -499,13 +469,13 @@ fn run() -> Result<()> {
let failed = post_command_failed || runner.report().data().iter().any(|(_, result)| result.failed());
if !config.skip_notify() {
notify_desktop(
terminal::notify_desktop(
format!(
"Topgrade finished {}",
if failed { "with errors" } else { "successfully" }
),
Some(Duration::from_secs(10)),
)
None,
);
}
if failed {
@@ -516,6 +486,9 @@ fn run() -> Result<()> {
}
fn main() {
print_info("Due to r-darwish giving this project access to the original topgrade crate, this crate will no longer receive any updates beyond the 10.0.1 update. To install the supported version, please run the following command: \n");
println!("cargo uninstall topgrade-rs");
println!("cargo install topgrade");
match run() {
Ok(()) => {
exit(0);
@@ -535,35 +508,9 @@ fn main() {
.is_some());
if !skip_print {
// 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);
}
}
}
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), "{key} already reported");
debug_assert!(!self.data.iter().any(|(k, _)| k == &key), "{} already reported", key);
self.data.push((key, success));
}
}

View File

@@ -2,12 +2,11 @@ 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 color_eyre::eyre::Result;
use anyhow::Result;
use log::debug;
use std::borrow::Cow;
use std::fmt::Debug;
use tracing::debug;
pub struct Runner<'a> {
ctx: &'a ExecutionContext<'a>,
@@ -56,12 +55,7 @@ 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 = if should_ask {
print_error(&key, format!("{e:?}"));
should_retry(interrupted, key.as_ref())?
} else {
false
};
let should_retry = should_ask && should_retry(interrupted, key.as_ref())?;
if !should_retry {
self.report.push_result(Some((

View File

@@ -1,8 +1,8 @@
#![cfg(windows)]
use color_eyre::eyre::Result;
use anyhow::Result;
use log::{debug, error};
use std::{env::current_exe, fs, path::PathBuf};
use tracing::{debug, error};
pub struct SelfRenamer {
exe_path: PathBuf,
@@ -31,7 +31,7 @@ impl Drop for SelfRenamer {
}
match fs::rename(&self.temp_path, &self.exe_path) {
Ok(_) => debug!("Moved Topgrade back from {:?} to {:?}", self.temp_path, self.exe_path),
Ok(_) => debug!("Moved topgrade back from {:?} to {:?}", self.temp_path, self.exe_path),
Err(e) => error!(
"Could not move Topgrade from {} back to {}: {}",
self.temp_path.display(),

View File

@@ -1,15 +1,13 @@
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");
@@ -17,7 +15,7 @@ pub fn self_update() -> Result<()> {
let target = self_update_crate::get_target();
let result = Update::configure()
.repo_owner("topgrade-rs")
.repo_owner("r-darwish")
.repo_name("topgrade")
.target(target)
.bin_name(if cfg!(windows) { "topgrade.exe" } else { "topgrade" })
@@ -31,7 +29,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");
@@ -51,8 +49,7 @@ pub fn self_update() -> Result<()> {
#[cfg(windows)]
{
#[allow(clippy::disallowed_methods)]
let status = command.status()?;
let status = command.spawn().and_then(|mut c| c.wait())?;
bail!(Upgraded(status));
}
}

View File

@@ -1,16 +1,12 @@
use std::fmt::{Display, Formatter};
use std::path::Path;
use std::process::Command;
use anyhow::Result;
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::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;
// 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
@@ -19,46 +15,20 @@ use crate::{execution_context::ExecutionContext, utils::require};
// themselves or when using docker-compose.
const NONEXISTENT_REPO: &str = "repository does not exist";
/// Uniquely identifies a `Container`.
#[derive(Debug)]
struct Container {
/// `Repository` and `Tag`
///
/// format: `Repository:Tag`, e.g., `nixos/nix:latest`.
repo_tag: String,
/// Platform
///
/// format: `OS/Architecture`, e.g., `linux/amd64`.
platform: String,
}
impl Container {
/// Construct a new `Container`.
fn new(repo_tag: String, platform: String) -> Self {
Self { repo_tag, platform }
}
}
impl Display for Container {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
// e.g., "`fedora:latest` for `linux/amd64`"
write!(f, "`{}` for `{}`", self.repo_tag, self.platform)
}
}
/// Returns a Vector of all containers, with Strings in the format
/// "REGISTRY/[PATH/]CONTAINER_NAME:TAG"
fn list_containers(crt: &Path) -> Result<Vec<Container>> {
fn list_containers(crt: &Path) -> Result<Vec<String>> {
debug!(
"Querying '{} image ls --format \"{{{{.Repository}}}}:{{{{.Tag}}}}/{{{{.ID}}}}\"' for containers",
"Querying '{} image ls --format \"{{{{.Repository}}}}:{{{{.Tag}}}}\"' for containers",
crt.display()
);
let output = Command::new(crt)
.args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}"])
.output_checked_with_utf8(|_| Ok(()))?;
.args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}}"])
.output()?;
let output_str = String::from_utf8(output.stdout)?;
let mut retval = vec![];
for line in output.stdout.lines() {
for line in output_str.lines() {
if line.starts_with("localhost") {
// Don't know how to update self-built containers
debug!("Skipping self-built container '{}'", line);
@@ -77,26 +47,7 @@ fn list_containers(crt: &Path) -> Result<Vec<Container>> {
}
debug!("Using container '{}'", line);
// line is of format: `Repository:Tag ImageID`, e.g., `nixos/nix:latest d80fea9c32b4`
let split_res = line.split(' ').collect::<Vec<&str>>();
assert_eq!(split_res.len(), 2);
let (repo_tag, image_id) = (split_res[0], split_res[1]);
debug!(
"Querying '{} image inspect --format \"{{{{.Os}}}}/{{{{.Architecture}}}}\"' for container {}",
crt.display(),
image_id
);
let inspect_output = Command::new(crt)
.args(["image", "inspect", image_id, "--format", "{{.Os}}/{{.Architecture}}"])
.output_checked_with_utf8(|_| Ok(()))?;
let mut platform = inspect_output.stdout;
// truncate the tailing new line character
platform.truncate(platform.len() - 1);
assert!(platform.contains('/'));
retval.push(Container::new(repo_tag.to_string(), platform));
retval.push(String::from(line));
}
Ok(retval)
@@ -109,20 +60,15 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
print_separator("Containers");
let mut success = true;
let containers = list_containers(&crt).context("Failed to list Docker containers")?;
let containers = list_containers(&crt)?;
debug!("Containers to inspect: {:?}", containers);
for container in containers.iter() {
debug!("Pulling container '{}'", container);
let args = vec![
"pull",
container.repo_tag.as_str(),
"--platform",
container.platform.as_str(),
];
let args = vec!["pull", &container[..]];
let mut exec = ctx.run_type().execute(&crt);
if let Err(e) = exec.args(&args).status_checked() {
if let Err(e) = exec.args(&args).check_run() {
error!("Pulling container '{}' failed: {}", container, e);
// Find out if this is 'skippable'
@@ -131,10 +77,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.output_checked_utf8() {
Ok(s) => s.stdout.contains(NONEXISTENT_REPO) || s.stderr.contains(NONEXISTENT_REPO),
if match exec.check_output() {
Ok(s) => s.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,
},
} {
@@ -149,12 +95,7 @@ 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"])
.status_checked()
{
if let Err(e) = ctx.run_type().execute(&crt).args(["image", "prune", "-f"]).check_run() {
error!("Removing dangling images failed: {}", e);
success = false;
}
@@ -163,6 +104,6 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
if success {
Ok(())
} else {
Err(eyre!(error::StepFailed))
Err(anyhow::anyhow!(error::StepFailed))
}
}

View File

@@ -1,11 +1,10 @@
#[cfg(any(windows))]
#[cfg(any(windows, target_os = "macos"))]
use std::env;
use std::path::{Path, PathBuf};
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
use anyhow::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};
@@ -23,12 +22,20 @@ pub struct Emacs {
}
impl Emacs {
fn directory_path() -> Option<PathBuf> {
fn directory_path(base_dirs: &BaseDirs) -> Option<PathBuf> {
#[cfg(unix)]
return {
let emacs_xdg_dir = crate::XDG_DIRS.config_dir().join("emacs").if_exists();
crate::HOME_DIR.join(".emacs.d").if_exists().or(emacs_xdg_dir)
};
cfg_if::cfg_if! {
if #[cfg(target_os = "macos")] {
let emacs_xdg_dir = env::var("XDG_CONFIG_HOME")
.ok()
.and_then(|config| PathBuf::from(config).join("emacs").if_exists())
.or_else(|| base_dirs.home_dir().join(".config/emacs").if_exists());
} else {
let emacs_xdg_dir = base_dirs.config_dir().join("emacs").if_exists();
}
}
#[cfg(unix)]
return base_dirs.home_dir().join(".emacs.d").if_exists().or(emacs_xdg_dir);
#[cfg(windows)]
return env::var("HOME")
@@ -39,11 +46,11 @@ impl Emacs {
.if_exists()
.or_else(|| PathBuf::from(&home).join(".config\\emacs").if_exists())
})
.or_else(|| crate::WINDOWS_DIRS.data_dir().join(".emacs.d").if_exists());
.or_else(|| base_dirs.data_dir().join(".emacs.d").if_exists());
}
pub fn new() -> Self {
let directory = Emacs::directory_path();
pub fn new(base_dirs: &BaseDirs) -> Self {
let directory = Emacs::directory_path(base_dirs);
let doom = directory.as_ref().and_then(|d| d.join(DOOM_PATH).if_exists());
Self { directory, doom }
}
@@ -66,7 +73,7 @@ impl Emacs {
command.args(["upgrade"]);
command.status_checked()
command.check_run()
}
pub fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
@@ -80,7 +87,7 @@ impl Emacs {
print_separator("Emacs");
let mut command = ctx.run_type().execute(emacs);
let mut command = ctx.run_type().execute(&emacs);
command
.args(["--batch", "--debug-init", "-l"])
@@ -98,6 +105,6 @@ impl Emacs {
#[cfg(not(unix))]
command.arg(EMACS_UPGRADE);
command.status_checked()
command.check_run()
}
}

View File

@@ -5,30 +5,26 @@ use std::process::Command;
use std::{env, path::Path};
use std::{fs, io::Write};
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use anyhow::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::ExecutorOutput;
use crate::executor::{CommandExt, ExecutorOutput, RunType};
use crate::terminal::{print_separator, shell};
use crate::utils::{self, check_is_python_2_or_shim, require, require_option, which, PathExt, REQUIRE_SUDO};
use crate::Step;
use crate::HOME_DIR;
use crate::utils::{self, require_option, PathExt};
use crate::{
error::{SkipStep, StepFailed, TopgradeError},
error::{SkipStep, TopgradeError},
terminal::print_warning,
};
pub fn run_cargo_update(ctx: &ExecutionContext) -> Result<()> {
let cargo_dir = env::var_os("CARGO_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| HOME_DIR.join(".cargo"))
.unwrap_or_else(|| ctx.base_dirs().home_dir().join(".cargo"))
.require()?;
require("cargo").or_else(|_| {
utils::require("cargo").or_else(|_| {
require_option(
cargo_dir.join("bin/cargo").if_exists(),
String::from("No cargo detected"),
@@ -42,7 +38,7 @@ pub fn run_cargo_update(ctx: &ExecutionContext) -> Result<()> {
}
print_separator("Cargo");
let cargo_update = require("cargo-install-update")
let cargo_update = utils::require("cargo-install-update")
.ok()
.or_else(|| cargo_dir.join("bin/cargo-install-update").if_exists());
let cargo_update = match cargo_update {
@@ -57,40 +53,37 @@ pub fn run_cargo_update(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type()
.execute(cargo_update)
.args(["install-update", "--git", "--all"])
.status_checked()?;
if ctx.config().cleanup() {
let cargo_cache = require("cargo-cache")
.ok()
.or_else(|| cargo_dir.join("bin/cargo-cache").if_exists());
match cargo_cache {
Some(e) => {
ctx.run_type().execute(e).args(["-a"]).status_checked()?;
}
None => {
let message = String::from("cargo-cache isn't installed so Topgrade can't cleanup cargo packages.\nInstall cargo-cache by running `cargo install cargo-cache`");
print_warning(message);
}
}
}
Ok(())
.check_run()
}
pub fn run_flutter_upgrade(ctx: &ExecutionContext) -> Result<()> {
let flutter = require("flutter")?;
pub fn run_flutter_upgrade(run_type: RunType) -> Result<()> {
let flutter = utils::require("flutter")?;
print_separator("Flutter");
ctx.run_type().execute(flutter).arg("upgrade").status_checked()
run_type.execute(&flutter).arg("upgrade").check_run()
}
pub fn run_gem(ctx: &ExecutionContext) -> Result<()> {
let gem = require("gem")?;
HOME_DIR.join(".gem").require()?;
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();
print_separator("Gems");
let go_global_update = utils::require("go-global-update")
.unwrap_or_else(|_| PathBuf::from(gopath).join("bin/go-global-update"))
.require()?;
let mut command = ctx.run_type().execute(gem);
print_separator("Go");
run_type.execute(&go_global_update).check_run()
}
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");
let mut command = run_type.execute(&gem);
command.arg("update");
if env::var_os("RBENV_SHELL").is_none() {
@@ -98,41 +91,14 @@ pub fn run_gem(ctx: &ExecutionContext) -> Result<()> {
command.arg("--user-install");
}
command.status_checked()
}
pub fn run_rubygems(ctx: &ExecutionContext) -> Result<()> {
HOME_DIR.join(".gem").require()?;
let gem = require("gem")?;
print_separator("RubyGems");
let gem_path_str = gem.as_os_str();
if gem_path_str.to_str().unwrap().contains("asdf") {
ctx.run_type()
.execute(gem)
.args(["update", "--system"])
.status_checked()?;
} else {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
if !Path::new("/usr/lib/ruby/vendor_ruby/rubygems/defaults/operating_system.rb").exists() {
ctx.run_type()
.execute(sudo)
.arg("-EH")
.arg(gem)
.args(["update", "--system"])
.status_checked()?;
}
}
Ok(())
command.check_run()
}
pub fn run_haxelib_update(ctx: &ExecutionContext) -> Result<()> {
let haxelib = require("haxelib")?;
let haxelib = utils::require("haxelib")?;
let haxelib_dir =
PathBuf::from(std::str::from_utf8(&Command::new(&haxelib).arg("config").output_checked()?.stdout)?.trim())
.require()?;
PathBuf::from(std::str::from_utf8(&Command::new(&haxelib).arg("config").output()?.stdout)?.trim()).require()?;
let directory_writable = tempfile_in(&haxelib_dir).is_ok();
debug!("{:?} writable: {}", haxelib_dir, directory_writable);
@@ -142,51 +108,44 @@ pub fn run_haxelib_update(ctx: &ExecutionContext) -> Result<()> {
let mut command = if directory_writable {
ctx.run_type().execute(&haxelib)
} else {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let mut c = ctx.run_type().execute(sudo);
let mut c = ctx
.run_type()
.execute(ctx.sudo().as_ref().ok_or(TopgradeError::SudoRequired)?);
c.arg(&haxelib);
c
};
command.arg("update").status_checked()
command.arg("update").check_run()
}
pub fn run_sheldon(ctx: &ExecutionContext) -> Result<()> {
let sheldon = require("sheldon")?;
let sheldon = utils::require("sheldon")?;
print_separator("Sheldon");
ctx.run_type()
.execute(sheldon)
.args(["lock", "--update"])
.status_checked()
ctx.run_type().execute(&sheldon).args(["lock", "--update"]).check_run()
}
pub fn run_fossil(ctx: &ExecutionContext) -> Result<()> {
let fossil = require("fossil")?;
pub fn run_fossil(run_type: RunType) -> Result<()> {
let fossil = utils::require("fossil")?;
print_separator("Fossil");
ctx.run_type().execute(fossil).args(["all", "sync"]).status_checked()
run_type.execute(&fossil).args(["all", "sync"]).check_run()
}
pub fn run_micro(ctx: &ExecutionContext) -> Result<()> {
let micro = require("micro")?;
pub fn run_micro(run_type: RunType) -> Result<()> {
let micro = utils::require("micro")?;
print_separator("micro");
let stdout = ctx
.run_type()
.execute(micro)
.args(["-plugin", "update"])
.output_checked_utf8()?
.stdout;
let stdout = run_type.execute(&micro).args(["-plugin", "update"]).string_output()?;
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(eyre!("micro output does not indicate success: {}", stdout))
Err(anyhow::anyhow!("micro output does not indicate success: {}", stdout))
}
}
@@ -196,295 +155,144 @@ pub fn run_micro(ctx: &ExecutionContext) -> Result<()> {
target_os = "netbsd",
target_os = "dragonfly"
)))]
pub fn run_apm(ctx: &ExecutionContext) -> Result<()> {
let apm = require("apm")?;
pub fn run_apm(run_type: RunType) -> Result<()> {
let apm = utils::require("apm")?;
print_separator("Atom Package Manager");
ctx.run_type()
.execute(apm)
.args(["upgrade", "--confirm=false"])
.status_checked()
run_type.execute(&apm).args(["upgrade", "--confirm=false"]).check_run()
}
pub fn run_rustup(ctx: &ExecutionContext) -> Result<()> {
let rustup = require("rustup")?;
pub fn run_rustup(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let rustup = utils::require("rustup")?;
print_separator("rustup");
ctx.run_type().execute(rustup).arg("update").status_checked()
}
pub fn run_juliaup(ctx: &ExecutionContext) -> Result<()> {
let juliaup = require("juliaup")?;
print_separator("juliaup");
if juliaup.canonicalize()?.is_descendant_of(&HOME_DIR) {
ctx.run_type()
.execute(&juliaup)
.args(["self", "update"])
.status_checked()?;
if rustup.canonicalize()?.is_descendant_of(base_dirs.home_dir()) {
run_type.execute(&rustup).args(["self", "update"]).check_run()?;
}
ctx.run_type().execute(&juliaup).arg("update").status_checked()
run_type.execute(&rustup).arg("update").check_run()
}
pub fn run_choosenim(ctx: &ExecutionContext) -> Result<()> {
let choosenim = require("choosenim")?;
let choosenim = utils::require("choosenim")?;
print_separator("choosenim");
let run_type = ctx.run_type();
run_type.execute(&choosenim).args(["update", "self"]).status_checked()?;
run_type.execute(&choosenim).args(["update", "stable"]).status_checked()
run_type.execute(&choosenim).args(["update", "self"]).check_run()?;
run_type.execute(&choosenim).args(["update", "stable"]).check_run()
}
pub fn run_krew_upgrade(ctx: &ExecutionContext) -> Result<()> {
let krew = require("kubectl-krew")?;
pub fn run_krew_upgrade(run_type: RunType) -> Result<()> {
let krew = utils::require("kubectl-krew")?;
print_separator("Krew");
ctx.run_type().execute(krew).args(["upgrade"]).status_checked()
run_type.execute(&krew).args(["upgrade"]).check_run()
}
pub fn run_gcloud_components_update(ctx: &ExecutionContext) -> Result<()> {
let gcloud = require("gcloud")?;
pub fn run_gcloud_components_update(run_type: RunType) -> Result<()> {
let gcloud = utils::require("gcloud")?;
if gcloud.starts_with("/snap") {
Ok(())
} else {
print_separator("gcloud");
ctx.run_type()
.execute(gcloud)
run_type
.execute(&gcloud)
.args(["components", "update", "--quiet"])
.status_checked()
}
.check_run()
}
pub fn run_jetpack(ctx: &ExecutionContext) -> Result<()> {
let jetpack = require("jetpack")?;
pub fn run_jetpack(run_type: RunType) -> Result<()> {
let jetpack = utils::require("jetpack")?;
print_separator("Jetpack");
ctx.run_type()
.execute(jetpack)
.args(["global", "update"])
.status_checked()
run_type.execute(&jetpack).args(["global", "update"]).check_run()
}
pub fn run_rtcl(ctx: &ExecutionContext) -> Result<()> {
let rupdate = require("rupdate")?;
let rupdate = utils::require("rupdate")?;
print_separator("rtcl");
ctx.run_type().execute(rupdate).status_checked()
ctx.run_type().execute(&rupdate).check_run()
}
pub fn run_opam_update(ctx: &ExecutionContext) -> Result<()> {
let opam = require("opam")?;
let opam = utils::require("opam")?;
print_separator("OCaml Package Manager");
ctx.run_type().execute(&opam).arg("update").status_checked()?;
ctx.run_type().execute(&opam).arg("upgrade").status_checked()?;
ctx.run_type().execute(&opam).arg("update").check_run()?;
ctx.run_type().execute(&opam).arg("upgrade").check_run()?;
if ctx.config().cleanup() {
ctx.run_type().execute(&opam).arg("clean").status_checked()?;
ctx.run_type().execute(&opam).arg("clean").check_run()?;
}
Ok(())
}
pub fn run_vcpkg_update(ctx: &ExecutionContext) -> Result<()> {
let vcpkg = require("vcpkg")?;
pub fn run_vcpkg_update(run_type: RunType) -> Result<()> {
let vcpkg = utils::require("vcpkg")?;
print_separator("vcpkg");
#[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 sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let mut c = ctx.run_type().execute(sudo);
c.arg(&vcpkg);
c
};
command.args(["upgrade", "--no-dry-run"]).status_checked()
run_type.execute(&vcpkg).args(["upgrade", "--no-dry-run"]).check_run()
}
pub fn run_pipx_update(ctx: &ExecutionContext) -> Result<()> {
let pipx = require("pipx")?;
pub fn run_pipx_update(run_type: RunType) -> Result<()> {
let pipx = utils::require("pipx")?;
print_separator("pipx");
ctx.run_type().execute(pipx).arg("upgrade-all").status_checked()
run_type.execute(&pipx).arg("upgrade-all").check_run()
}
pub fn run_conda_update(ctx: &ExecutionContext) -> Result<()> {
let conda = require("conda")?;
let conda = utils::require("conda")?;
let output = Command::new("conda")
.args(["config", "--show", "auto_activate_base"])
.output_checked_utf8()?;
debug!("Conda output: {}", output.stdout);
if output.stdout.contains("False") {
.output()?;
let string_output = String::from_utf8(output.stdout)?;
debug!("Conda output: {}", string_output);
if string_output.contains("False") {
return Err(SkipStep("auto_activate_base is set to False".to_string()).into());
}
print_separator("Conda");
let mut command = ctx.run_type().execute(conda);
command.args(["update", "--all"]);
if ctx.config().yes(Step::Conda) {
command.arg("--yes");
}
command.status_checked()
ctx.run_type()
.execute(&conda)
.args(["update", "--all", "-y"])
.check_run()
}
pub fn run_mamba_update(ctx: &ExecutionContext) -> Result<()> {
let mamba = require("mamba")?;
let output = Command::new("mamba")
.args(["config", "--show", "auto_activate_base"])
.output_checked_utf8()?;
debug!("Mamba output: {}", output.stdout);
if output.stdout.contains("False") {
return Err(SkipStep("auto_activate_base is set to False".to_string()).into());
}
print_separator("Mamba");
let mut command = ctx.run_type().execute(mamba);
command.args(["update", "--all"]);
if ctx.config().yes(Step::Mamba) {
command.arg("--yes");
}
command.status_checked()
}
pub fn run_pip3_update(ctx: &ExecutionContext) -> Result<()> {
let py = require("python").and_then(check_is_python_2_or_shim);
let py3 = require("python3").and_then(check_is_python_2_or_shim);
let python3 = match (py, py3) {
// prefer `python` if it is available and is a valid Python 3.
(Ok(py), _) => py,
(Err(_), Ok(py3)) => py3,
(Err(py_err), Err(py3_err)) => {
return Err(SkipStep(format!("Skip due to following reasons: {} {}", py_err, py3_err)).into());
}
};
pub fn run_pip3_update(run_type: RunType) -> Result<()> {
let python3 = utils::require("python3")?;
Command::new(&python3)
.args(["-m", "pip"])
.output_checked_utf8()
.check_output()
.map_err(|_| SkipStep("pip does not exists".to_string()))?;
let check_externally_managed = "import sysconfig; from os import path; print('Y') if path.isfile(path.join(sysconfig.get_path('stdlib'), 'EXTERNALLY-MANAGED')) else print('N')";
Command::new(&python3)
.args(["-c", check_externally_managed])
.output_checked_utf8()
.map_err(|_| SkipStep("pip may be externally managed".to_string()))
.and_then(|output| match output.stdout.trim() {
"N" => Ok(()),
"Y" => Err(SkipStep("pip is externally managed".to_string())),
_ => {
print_warning("Unexpected output when checking EXTERNALLY-MANAGED");
print_warning(output.stdout.trim());
Err(SkipStep("pip may be externally managed".to_string()))
}
})?;
print_separator("pip3");
if env::var("VIRTUAL_ENV").is_ok() {
if std::env::var("VIRTUAL_ENV").is_ok() {
print_warning("This step is will be skipped when running inside a virtual environment");
return Err(SkipStep("Does not run inside a virtual environment".to_string()).into());
}
ctx.run_type()
run_type
.execute(&python3)
.args(["-m", "pip", "install", "--upgrade", "--user", "pip"])
.status_checked()
.check_run()
}
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_pip_review_local_update(ctx: &ExecutionContext) -> Result<()> {
let pip_review = require("pip-review")?;
print_separator("pip-review (local)");
if !ctx.config().enable_pip_review_local() {
print_warning(
"Pip-review (local) is disabled by default. Enable it by setting enable_pip_review_local=true in the configuration.",
);
return Err(SkipStep(String::from("Pip-review (local) is disabled by default")).into());
}
ctx.run_type()
.execute(pip_review)
.arg("--local")
.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_pipupgrade() {
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)
.args(ctx.config().pipupgrade_arguments().split_whitespace())
.status_checked()?;
Ok(())
}
pub fn run_stack_update(ctx: &ExecutionContext) -> Result<()> {
if require("ghcup").is_ok() {
// `ghcup` is present and probably(?) being used to install `stack`.
// Don't upgrade `stack`, let `ghcup` handle it. Per `ghcup install stack`:
// !!! Additionally, you should upgrade stack only through ghcup and not use 'stack upgrade' !!!
return Ok(());
}
let stack = require("stack")?;
pub fn run_stack_update(run_type: RunType) -> Result<()> {
let stack = utils::require("stack")?;
print_separator("stack");
ctx.run_type().execute(stack).arg("upgrade").status_checked()
}
pub fn run_ghcup_update(ctx: &ExecutionContext) -> Result<()> {
let ghcup = require("ghcup")?;
print_separator("ghcup");
ctx.run_type().execute(ghcup).arg("upgrade").status_checked()
run_type.execute(&stack).arg("upgrade").check_run()
}
pub fn run_tlmgr_update(ctx: &ExecutionContext) -> Result<()> {
@@ -496,14 +304,16 @@ pub fn run_tlmgr_update(ctx: &ExecutionContext) -> Result<()> {
}
}
let tlmgr = require("tlmgr")?;
let kpsewhich = require("kpsewhich")?;
let tlmgr = utils::require("tlmgr")?;
let kpsewhich = utils::require("kpsewhich")?;
let tlmgr_directory = {
let mut d = PathBuf::from(
&Command::new(kpsewhich)
std::str::from_utf8(
&Command::new(&kpsewhich)
.arg("-var-value=SELFAUTOPARENT")
.output_checked_utf8()?
.stdout
.output()?
.stdout,
)?
.trim(),
);
d.push("tlpkg");
@@ -519,68 +329,61 @@ pub fn run_tlmgr_update(ctx: &ExecutionContext) -> Result<()> {
let mut command = if directory_writable {
ctx.run_type().execute(&tlmgr)
} else {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let mut c = ctx.run_type().execute(sudo);
let mut c = ctx
.run_type()
.execute(ctx.sudo().as_ref().ok_or(TopgradeError::SudoRequired)?);
c.arg(&tlmgr);
c
};
command.args(["update", "--self", "--all"]);
command.status_checked()
command.check_run()
}
pub fn run_chezmoi_update(ctx: &ExecutionContext) -> Result<()> {
let chezmoi = require("chezmoi")?;
HOME_DIR.join(".local/share/chezmoi").require()?;
pub fn run_chezmoi_update(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let chezmoi = utils::require("chezmoi")?;
base_dirs.home_dir().join(".local/share/chezmoi").require()?;
print_separator("chezmoi");
ctx.run_type().execute(chezmoi).arg("update").status_checked()
run_type.execute(&chezmoi).arg("update").check_run()
}
pub fn run_myrepos_update(ctx: &ExecutionContext) -> Result<()> {
let myrepos = require("mr")?;
HOME_DIR.join(".mrconfig").require()?;
pub fn run_myrepos_update(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let myrepos = utils::require("mr")?;
base_dirs.home_dir().join(".mrconfig").require()?;
print_separator("myrepos");
ctx.run_type()
run_type
.execute(&myrepos)
.arg("--directory")
.arg(&*HOME_DIR)
.arg(base_dirs.home_dir())
.arg("checkout")
.status_checked()?;
ctx.run_type()
.check_run()?;
run_type
.execute(&myrepos)
.arg("--directory")
.arg(&*HOME_DIR)
.arg(base_dirs.home_dir())
.arg("update")
.status_checked()
.check_run()
}
pub fn run_custom_command(name: &str, command: &str, ctx: &ExecutionContext) -> Result<()> {
print_separator(name);
let mut exec = ctx.run_type().execute(shell());
#[cfg(unix)]
let command = if let Some(command) = command.strip_prefix("-i ") {
exec.arg("-i");
command
} else {
command
};
exec.arg("-c").arg(command).status_checked()
ctx.run_type().execute(shell()).arg("-c").arg(command).check_run()
}
pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> {
let composer = require("composer")?;
let composer = utils::require("composer")?;
let composer_home = Command::new(&composer)
.args(["global", "config", "--absolute", "--quiet", "home"])
.output_checked_utf8()
.map_err(|e| (SkipStep(format!("Error getting the composer directory: {e}"))))
.map(|s| PathBuf::from(s.stdout.trim()))?
.check_output()
.map_err(|e| (SkipStep(format!("Error getting the composer directory: {}", e))))
.map(|s| PathBuf::from(s.trim()))?
.require()?;
if !composer_home.is_descendant_of(&HOME_DIR) {
if !composer_home.is_descendant_of(ctx.base_dirs().home_dir()) {
return Err(SkipStep(format!(
"Composer directory {} isn't a decandent of the user's home directory",
composer_home.display()
@@ -600,27 +403,30 @@ pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> {
};
if has_update {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
ctx.run_type()
.execute(sudo)
.execute(ctx.sudo().as_ref().unwrap())
.arg(&composer)
.arg("self-update")
.status_checked()?;
.check_run()?;
}
} else {
ctx.run_type().execute(&composer).arg("self-update").status_checked()?;
ctx.run_type().execute(&composer).arg("self-update").check_run()?;
}
}
}
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) = which("valet") {
ctx.run_type().execute(valet).arg("install").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") {
if let Some(valet) = utils::which("valet") {
ctx.run_type().execute(&valet).arg("install").check_run()?;
}
}
@@ -628,39 +434,20 @@ pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> {
}
pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> {
let dotnet = require("dotnet")?;
let dotnet = utils::require("dotnet")?;
// 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 = Command::new(dotnet).args(["tool", "list", "--global"]).output()?;
if !output.status.success() {
return Err(SkipStep(format!("dotnet failed with exit code {:?}", output.status)).into());
}
};
let mut packages = output
.stdout
.lines()
// Skip the header:
//
// Package Id Version Commands
// -------------------------------------
//
// One thing to note is that .NET SDK respect locale, which means this
// header can be printed in languages other than English, do NOT use it
// to do any check.
.skip(2)
.filter(|line| !line.is_empty())
.peekable();
let output = String::from_utf8(output.stdout)?;
if !output.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();
if packages.peek().is_none() {
return Err(SkipStep(String::from("No dotnet global tools installed")).into());
@@ -671,64 +458,39 @@ 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"])
.status_checked()
.with_context(|| format!("Failed to update .NET package {package_name}"))?;
.check_run()?;
}
Ok(())
}
pub fn run_helix_grammars(ctx: &ExecutionContext) -> Result<()> {
require("helix")?;
print_separator("Helix");
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
ctx.run_type()
.execute(sudo)
.args(["helix", "--grammar", "fetch"])
.status_checked()
.with_context(|| "Failed to download helix grammars!")?;
ctx.run_type()
.execute(sudo)
.args(["helix", "--grammar", "build"])
.status_checked()
.with_context(|| "Failed to build helix grammars!")?;
Ok(())
}
pub fn run_raco_update(ctx: &ExecutionContext) -> Result<()> {
let raco = require("raco")?;
pub fn run_raco_update(run_type: RunType) -> Result<()> {
let raco = utils::require("raco")?;
print_separator("Racket Package Manager");
ctx.run_type()
.execute(raco)
.args(["pkg", "update", "--all"])
.status_checked()
run_type.execute(&raco).args(["pkg", "update", "--all"]).check_run()
}
pub fn bin_update(ctx: &ExecutionContext) -> Result<()> {
let bin = require("bin")?;
let bin = utils::require("bin")?;
print_separator("Bin");
ctx.run_type().execute(bin).arg("update").status_checked()
ctx.run_type().execute(&bin).arg("update").check_run()
}
pub fn spicetify_upgrade(ctx: &ExecutionContext) -> Result<()> {
let spicetify = require("spicetify")?;
let spicetify = utils::require("spicetify")?;
print_separator("Spicetify");
ctx.run_type().execute(spicetify).arg("upgrade").status_checked()
ctx.run_type().execute(&spicetify).arg("upgrade").check_run()
}
pub fn run_ghcli_extensions_upgrade(ctx: &ExecutionContext) -> Result<()> {
let gh = require("gh")?;
let result = Command::new(&gh).args(["extensions", "list"]).output_checked_utf8();
let gh = utils::require("gh")?;
let result = Command::new(&gh).args(["extensions", "list"]).check_output();
if result.is_err() {
debug!("GH result {:?}", result);
return Err(SkipStep(String::from("GH failed")).into());
@@ -738,57 +500,16 @@ pub fn run_ghcli_extensions_upgrade(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type()
.execute(&gh)
.args(["extension", "upgrade", "--all"])
.status_checked()
.check_run()
}
pub fn update_julia_packages(ctx: &ExecutionContext) -> Result<()> {
let julia = require("julia")?;
let julia = utils::require("julia")?;
print_separator("Julia Packages");
ctx.run_type()
.execute(julia)
.execute(&julia)
.args(["-e", "using Pkg; Pkg.update()"])
.status_checked()
}
pub fn run_helm_repo_update(ctx: &ExecutionContext) -> Result<()> {
let helm = require("helm")?;
print_separator("Helm");
let no_repo = "no repositories found";
let mut success = true;
let mut exec = ctx.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))
}
}
pub fn run_stew(ctx: &ExecutionContext) -> Result<()> {
let stew = require("stew")?;
print_separator("stew");
ctx.run_type().execute(stew).args(["upgrade", "--all"]).status_checked()
}
pub fn run_bob(ctx: &ExecutionContext) -> Result<()> {
let bob = require("bob")?;
print_separator("Bob");
ctx.run_type().execute(bob).args(["update", "--all"]).status_checked()
.check_run()
}

View File

@@ -3,17 +3,17 @@ use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use color_eyre::eyre::{eyre, Result};
use anyhow::{anyhow, 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::terminal::print_separator;
use crate::utils::{which, PathExt};
use crate::{error::SkipStep, terminal::print_warning};
@@ -33,10 +33,10 @@ pub struct Repositories<'a> {
bad_patterns: Vec<String>,
}
fn output_checked_utf8(output: Output) -> Result<()> {
fn check_output(output: Output) -> Result<()> {
if !(output.status.success()) {
let stderr = String::from_utf8(output.stderr).unwrap();
Err(eyre!(stderr))
Err(anyhow!(stderr))
} else {
Ok(())
}
@@ -66,11 +66,11 @@ async fn pull_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) -
.stdin(Stdio::null())
.output()
.await?;
let result = output_checked_utf8(pull_output).and_then(|_| output_checked_utf8(submodule_output));
let result = check_output(pull_output).and_then(|_| check_output(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,9 +86,12 @@ async fn pull_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) -
"log",
"--no-decorate",
"--oneline",
&format!("{before}..{after}"),
&format!("{}..{}", before, after),
])
.status_checked()?;
.spawn()
.unwrap()
.wait()
.unwrap();
println!();
}
_ => {
@@ -105,8 +108,8 @@ fn get_head_revision(git: &Path, repo: &str) -> Option<String> {
.stdin(Stdio::null())
.current_dir(repo)
.args(["rev-parse", "HEAD"])
.output_checked_utf8()
.map(|output| output.stdout.trim().to_string())
.check_output()
.map(|output| output.trim().to_string())
.map_err(|e| {
error!("Error getting revision for {}: {}", repo, e);
@@ -120,8 +123,8 @@ fn has_remotes(git: &Path, repo: &str) -> Option<bool> {
.stdin(Stdio::null())
.current_dir(repo)
.args(["remote", "show"])
.output_checked_utf8()
.map(|output| output.stdout.lines().count() > 0)
.check_output()
.map(|output| output.lines().count() > 0)
.map_err(|e| {
error!("Error getting remotes for {}: {}", repo, e);
e
@@ -163,9 +166,9 @@ impl Git {
.stdin(Stdio::null())
.current_dir(path)
.args(["rev-parse", "--show-toplevel"])
.output_checked_utf8()
.check_output()
.ok()
.map(|output| output.stdout.trim().to_string());
.map(|output| output.trim().to_string());
return output;
}
}
@@ -178,28 +181,22 @@ impl Git {
None
}
pub fn multi_pull_step(&self, repositories: &Repositories, ctx: &ExecutionContext) -> Result<()> {
// Warn the user about the bad patterns.
//
// NOTE: this should be executed **before** skipping the Git step or the
// user won't receive this warning in the cases where all the paths configured
// are bad patterns.
repositories
.bad_patterns
.iter()
.for_each(|pattern| print_warning(format!("Path {pattern} did not contain any git repositories")));
if repositories.repositories.is_empty() {
return Err(SkipStep(String::from("No repositories to pull")).into());
}
print_separator("Git repositories");
repositories
.bad_patterns
.iter()
.for_each(|pattern| print_warning(format!("Path {} did not contain any git repositories", pattern)));
self.multi_pull(repositories, ctx)
}
pub fn multi_pull(&self, repositories: &Repositories, ctx: &ExecutionContext) -> Result<()> {
let git = self.git.as_ref().unwrap();
if ctx.run_type().dry() {
if let RunType::Dry = ctx.run_type() {
repositories
.repositories
.iter()

View File

@@ -1,45 +0,0 @@
use std::path::PathBuf;
use std::process::Command;
use color_eyre::eyre::Result;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
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(ctx: &ExecutionContext) -> Result<()> {
let go_global_update = require_go_bin("go-global-update")?;
print_separator("go-global-update");
ctx.run_type().execute(go_global_update).status_checked()
}
/// <https://github.com/nao1215/gup>
pub fn run_go_gup(ctx: &ExecutionContext) -> Result<()> {
let gup = require_go_bin("gup")?;
print_separator("gup");
ctx.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,8 +1,10 @@
use crate::error::TopgradeError;
use crate::terminal::print_separator;
use crate::utils::require;
use color_eyre::eyre::Result;
use anyhow::Result;
use crate::execution_context::ExecutionContext;
use crate::executor::ExecutorOutput;
const UPGRADE_KAK: &str = include_str!("upgrade.kak");
@@ -11,13 +13,19 @@ pub fn upgrade_kak_plug(ctx: &ExecutionContext) -> Result<()> {
print_separator("Kakoune");
// TODO: Why supress output for this command?
ctx.run_type()
.execute(kak)
.args(["-ui", "dummy", "-e", UPGRADE_KAK])
.output()?;
let mut command = ctx.run_type().execute(&kak);
command.args(["-ui", "dummy", "-e", UPGRADE_KAK]);
println!("Plugins upgraded");
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")
}
}
Ok(())
}

View File

@@ -2,7 +2,6 @@ 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,105 +1,66 @@
use std::fmt::Display;
#[cfg(target_os = "linux")]
use std::os::unix::fs::MetadataExt;
#![allow(unused_imports)]
#[cfg(unix)]
use std::os::unix::prelude::MetadataExt;
use std::path::PathBuf;
use std::process::Command;
use crate::utils::{require_option, REQUIRE_SUDO};
use crate::HOME_DIR;
use color_eyre::eyre::Result;
#[cfg(target_os = "linux")]
use anyhow::Result;
use directories::BaseDirs;
use log::debug;
#[cfg(unix)]
use nix::unistd::Uid;
use semver::Version;
use tracing::debug;
use crate::command::CommandExt;
use crate::executor::{CommandExt, RunType};
use crate::terminal::print_separator;
use crate::utils::{require, PathExt};
use crate::{error::SkipStep, execution_context::ExecutionContext};
enum NPMVariant {
Npm,
Pnpm,
}
impl NPMVariant {
const fn short_name(&self) -> &str {
match self {
NPMVariant::Npm => "npm",
NPMVariant::Pnpm => "pnpm",
}
}
const fn is_npm(&self) -> bool {
matches!(self, NPMVariant::Npm)
}
}
impl Display for NPMVariant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.short_name())
}
}
#[allow(clippy::upper_case_acronyms)]
struct NPM {
command: PathBuf,
variant: NPMVariant,
}
impl NPM {
fn new(command: PathBuf, variant: NPMVariant) -> Self {
Self { command, variant }
}
/// Is the “NPM” version larger than 8.11.0?
fn is_npm_8(&self) -> bool {
let v = self.version();
self.variant.is_npm() && matches!(v, Ok(v) if v >= Version::new(8, 11, 0))
}
/// Get the most suitable “global location” argument
/// of this NPM instance.
///
/// If the “NPM” version is larger than 8.11.0, we use
/// `--location=global`; otherwise, use `-g`.
fn global_location_arg(&self) -> &str {
if self.is_npm_8() {
"--location=global"
} else {
"-g"
}
fn new(command: PathBuf) -> Self {
Self { command }
}
#[cfg(target_os = "linux")]
fn root(&self) -> Result<PathBuf> {
let args = ["root", self.global_location_arg()];
let version = self.version()?;
let args = if version < Version::new(8, 11, 0) {
["root", "-g"]
} else {
["root", "--location=global"]
};
Command::new(&self.command)
.args(args)
.output_checked_utf8()
.map(|s| PathBuf::from(s.stdout.trim()))
.check_output()
.map(|s| PathBuf::from(s.trim()))
}
fn version(&self) -> Result<Version> {
let version_str = Command::new(&self.command)
.args(["--version"])
.output_checked_utf8()
.map(|s| s.stdout.trim().to_owned());
.check_output()
.map(|s| s.trim().to_owned());
Version::parse(&version_str?).map_err(|err| err.into())
}
fn upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> {
let args = ["update", self.global_location_arg()];
if use_sudo {
let sudo = require_option(ctx.sudo().clone(), REQUIRE_SUDO.to_string())?;
ctx.run_type()
.execute(sudo)
.arg(&self.command)
.args(args)
.status_checked()?;
fn upgrade(&self, run_type: RunType, use_sudo: bool) -> Result<()> {
print_separator("Node Package Manager");
let version = self.version()?;
let args = if version < Version::new(8, 11, 0) {
["update", "-g"]
} else {
ctx.run_type().execute(&self.command).args(args).status_checked()?;
["update", "--location=global"]
};
if use_sudo {
run_type.execute("sudo").args(args).check_run()?;
} else {
run_type.execute(&self.command).args(args).check_run()?;
}
Ok(())
@@ -109,7 +70,7 @@ impl NPM {
pub fn should_use_sudo(&self) -> Result<bool> {
let npm_root = self.root()?;
if !npm_root.exists() {
return Err(SkipStep(format!("{} root at {} doesn't exist", self.variant, npm_root.display())).into());
return Err(SkipStep(format!("NPM root at {} doesn't exist", npm_root.display(),)).into());
}
let metadata = std::fs::metadata(&npm_root)?;
@@ -132,38 +93,27 @@ impl Yarn {
}
}
fn has_global_subcmd(&self) -> bool {
// Get the version of Yarn. After Yarn 2.x (berry),
// “yarn global” has been replaced with “yarn dlx”.
//
// 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"]).output_checked_utf8();
matches!(version, Ok(ver) if ver.stdout.starts_with('1') || ver.stdout.starts_with('0'))
}
#[cfg(target_os = "linux")]
fn root(&self) -> Result<PathBuf> {
let args = ["global", "dir"];
Command::new(&self.command)
.args(args)
.output_checked_utf8()
.map(|s| PathBuf::from(s.stdout.trim()))
.check_output()
.map(|s| PathBuf::from(s.trim()))
}
fn upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> {
fn upgrade(&self, run_type: RunType, use_sudo: bool) -> Result<()> {
print_separator("Yarn Package Manager");
let args = ["global", "upgrade"];
if use_sudo {
let sudo = require_option(ctx.sudo().clone(), REQUIRE_SUDO.to_string())?;
ctx.run_type()
.execute(sudo)
run_type
.execute("sudo")
.arg(self.yarn.as_ref().unwrap_or(&self.command))
.args(args)
.status_checked()?;
.check_run()?;
} else {
ctx.run_type().execute(&self.command).args(args).status_checked()?;
run_type.execute(&self.command).args(args).check_run()?;
}
Ok(())
@@ -173,7 +123,7 @@ impl Yarn {
pub fn should_use_sudo(&self) -> Result<bool> {
let yarn_root = self.root()?;
if !yarn_root.exists() {
return Err(SkipStep(format!("Yarn root at {} doesn't exist", yarn_root.display(),)).into());
return Err(SkipStep(format!("NPM root at {} doesn't exist", yarn_root.display(),)).into());
}
let metadata = std::fs::metadata(&yarn_root)?;
@@ -212,61 +162,36 @@ 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");
let npm = require("pnpm").or_else(|_| require("npm")).map(NPM::new)?;
#[cfg(target_os = "linux")]
{
npm.upgrade(ctx, should_use_sudo(&npm, ctx)?)
npm.upgrade(ctx.run_type(), should_use_sudo(&npm, ctx)?)
}
#[cfg(not(target_os = "linux"))]
{
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("Performant Node Package Manager");
#[cfg(target_os = "linux")]
{
pnpm.upgrade(ctx, should_use_sudo(&pnpm, ctx)?)
}
#[cfg(not(target_os = "linux"))]
{
pnpm.upgrade(ctx, false)
npm.upgrade(ctx.run_type(), false)
}
}
pub fn run_yarn_upgrade(ctx: &ExecutionContext) -> Result<()> {
let yarn = require("yarn").map(Yarn::new)?;
if !yarn.has_global_subcmd() {
debug!("Yarn is 2.x or above, skipping global upgrade");
return Ok(());
}
print_separator("Yarn Package Manager");
#[cfg(target_os = "linux")]
{
yarn.upgrade(ctx, should_use_sudo_yarn(&yarn, ctx)?)
yarn.upgrade(ctx.run_type(), should_use_sudo_yarn(&yarn, ctx)?)
}
#[cfg(not(target_os = "linux"))]
{
yarn.upgrade(ctx, false)
yarn.upgrade(ctx.run_type(), false)
}
}
pub fn deno_upgrade(ctx: &ExecutionContext) -> Result<()> {
let deno = require("deno")?;
let deno_dir = HOME_DIR.join(".deno");
let deno_dir = ctx.base_dirs().home_dir().join(".deno");
if !deno.canonicalize()?.is_descendant_of(&deno_dir) {
let skip_reason = SkipStep("Deno installed outside of .deno directory".to_string());
@@ -274,5 +199,5 @@ pub fn deno_upgrade(ctx: &ExecutionContext) -> Result<()> {
}
print_separator("Deno");
ctx.run_type().execute(&deno).arg("upgrade").status_checked()
ctx.run_type().execute(&deno).arg("upgrade").check_run()
}

View File

@@ -1,37 +1,31 @@
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 color_eyre::eyre::Result;
use anyhow::Result;
pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
//let pkg = require("pkg")?;
let pkg = which("nala").or_else(|| which("pkg")).unwrap();
let pkg = require("pkg")?;
print_separator("Termux Packages");
let is_nala = pkg.ends_with("nala");
let mut command = ctx.run_type().execute(&pkg);
command.arg("upgrade");
if ctx.config().yes(Step::System) {
command.arg("-y");
}
command.status_checked()?;
command.check_run()?;
if !is_nala && ctx.config().cleanup() {
ctx.run_type().execute(&pkg).arg("clean").status_checked()?;
if ctx.config().cleanup() {
ctx.run_type().execute(&pkg).arg("clean").check_run()?;
let apt = require("apt")?;
let mut command = ctx.run_type().execute(apt);
let mut command = ctx.run_type().execute(&apt);
command.arg("autoremove");
if ctx.config().yes(Step::System) {
command.arg("-y");
}
command.status_checked()?;
command.check_run()?;
}
Ok(())

View File

@@ -1,15 +1,13 @@
use std::env::var_os;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
use color_eyre::eyre;
use color_eyre::eyre::Result;
use anyhow::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};
@@ -31,10 +29,11 @@ pub struct YayParu {
impl ArchPackageManager for YayParu {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
if ctx.config().show_arch_news() {
ctx.run_type()
.execute(&self.executable)
Command::new(&self.executable)
.arg("-Pw")
.status_checked_with_codes(&[1, 0])?;
.spawn()
.and_then(|mut p| p.wait())
.ok();
}
let mut command = ctx.run_type().execute(&self.executable);
@@ -49,7 +48,7 @@ impl ArchPackageManager for YayParu {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.status_checked()?;
command.check_run()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable);
@@ -57,7 +56,7 @@ impl ArchPackageManager for YayParu {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.status_checked()?;
command.check_run()?;
}
Ok(())
@@ -73,37 +72,6 @@ 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,
}
@@ -120,7 +88,7 @@ impl ArchPackageManager for Trizen {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.status_checked()?;
command.check_run()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable);
@@ -128,7 +96,7 @@ impl ArchPackageManager for Trizen {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.status_checked()?;
command.check_run()?;
}
Ok(())
@@ -144,7 +112,7 @@ impl Trizen {
}
pub struct Pacman {
sudo: Sudo,
sudo: PathBuf,
executable: PathBuf,
}
@@ -158,7 +126,7 @@ impl ArchPackageManager for Pacman {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.status_checked()?;
command.check_run()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.sudo);
@@ -166,7 +134,7 @@ impl ArchPackageManager for Pacman {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.status_checked()?;
command.check_run()?;
}
Ok(())
@@ -207,7 +175,7 @@ impl ArchPackageManager for Pikaur {
command.arg("--noconfirm");
}
command.status_checked()?;
command.check_run()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable);
@@ -215,7 +183,7 @@ impl ArchPackageManager for Pikaur {
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.status_checked()?;
command.check_run()?;
}
Ok(())
@@ -246,7 +214,7 @@ impl ArchPackageManager for Pamac {
command.arg("--no-confirm");
}
command.status_checked()?;
command.check_run()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable);
@@ -254,7 +222,7 @@ impl ArchPackageManager for Pamac {
if ctx.config().yes(Step::System) {
command.arg("--no-confirm");
}
command.status_checked()?;
command.check_run()?;
}
Ok(())
@@ -263,7 +231,7 @@ impl ArchPackageManager for Pamac {
pub struct Aura {
executable: PathBuf,
sudo: Sudo,
sudo: PathBuf,
}
impl Aura {
@@ -277,7 +245,7 @@ impl Aura {
impl ArchPackageManager for Aura {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let sudo = which("sudo").unwrap_or_else(PathBuf::new);
let sudo = which("sudo").unwrap_or(PathBuf::new());
let mut aur_update = ctx.run_type().execute(&sudo);
if sudo.ends_with("sudo") {
@@ -289,7 +257,7 @@ impl ArchPackageManager for Aura {
aur_update.arg("--noconfirm");
}
aur_update.status_checked()?;
aur_update.check_run()?;
} else {
println!("Aura requires sudo installed to work with AUR packages")
}
@@ -302,7 +270,7 @@ impl ArchPackageManager for Aura {
if ctx.config().yes(Step::System) {
pacman_update.arg("--noconfirm");
}
pacman_update.status_checked()?;
pacman_update.check_run()?;
Ok(())
}
@@ -316,16 +284,14 @@ 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 => GarudaUpdate::get()
config::ArchPackageManager::Autodetect => YayParu::get("paru", &pacman)
.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),
@@ -338,7 +304,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(|| eyre::Report::from(TopgradeError::FailedGettingPackageManager))?;
get_arch_package_manager(ctx).ok_or_else(|| anyhow::Error::from(TopgradeError::FailedGettingPackageManager))?;
package_manager.upgrade(ctx)
}

View File

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

View File

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

View File

@@ -1,17 +1,17 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use color_eyre::eyre::Result;
use anyhow::Result;
use ini::Ini;
use tracing::{debug, warn};
use log::{debug, warn};
use crate::command::CommandExt;
use crate::error::{SkipStep, TopgradeError};
use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, RunType};
use crate::steps::os::archlinux;
use crate::terminal::print_separator;
use crate::utils::{require, require_option, which, PathExt, REQUIRE_SUDO};
use crate::{Step, HOME_DIR};
use crate::terminal::{print_separator, print_warning};
use crate::utils::{require, require_option, which, PathExt};
use crate::Step;
static OS_RELEASE_PATH: &str = "/etc/os-release";
@@ -24,15 +24,10 @@ pub enum Distribution {
CentOS,
ClearLinux,
Fedora,
FedoraSilverblue,
Debian,
Gentoo,
OpenMandriva,
OpenSuseTumbleweed,
PCLinuxOS,
Suse,
SuseMicro,
Vanilla,
Void,
Solus,
Exherbo,
@@ -41,58 +36,33 @@ pub enum Distribution {
}
impl Distribution {
fn parse_os_release(os_release: &Ini) -> Result<Self> {
fn parse_os_release(os_release: &ini::Ini) -> Result<Self> {
let section = os_release.general_section();
let id = section.get("ID");
let name = section.get("NAME");
let variant: Option<Vec<&str>> = section.get("VARIANT").map(|s| s.split_whitespace().collect());
let id_like: Option<Vec<&str>> = section.get("ID_LIKE").map(|s| s.split_whitespace().collect());
Ok(match id {
Some("alpine") => Distribution::Alpine,
Some("centos") | Some("rhel") | Some("ol") => Distribution::CentOS,
Some("clear-linux-os") => Distribution::ClearLinux,
Some("fedora") | Some("nobara") => {
return if let Some(variant) = variant {
if variant.contains(&"Silverblue") {
Ok(Distribution::FedoraSilverblue)
} else {
Ok(Distribution::Fedora)
}
} else {
Ok(Distribution::Fedora)
}
}
Some("fedora") | Some("nobara") => Distribution::Fedora,
Some("void") => Distribution::Void,
Some("debian") | Some("pureos") | Some("Deepin") => Distribution::Debian,
Some("arch") | Some("manjaro-arm") | Some("garuda") | Some("artix") => Distribution::Arch,
Some("debian") | Some("pureos") => Distribution::Debian,
Some("arch") | Some("anarchy") | Some("manjaro-arm") | Some("garuda") | Some("artix") => Distribution::Arch,
Some("solus") => Distribution::Solus,
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(name) = name {
if name.contains("Vanilla") {
return Ok(Distribution::Vanilla);
}
}
if let Some(id_like) = id_like {
if id_like.contains(&"debian") || id_like.contains(&"ubuntu") {
return Ok(Distribution::Debian);
} else if id_like.contains(&"centos") {
return Ok(Distribution::CentOS);
} else if id_like.contains(&"suse") {
let id_variant = id.unwrap_or_default();
return if id_variant.contains("tumbleweed") {
Ok(Distribution::OpenSuseTumbleweed)
} else {
Ok(Distribution::Suse)
};
return Ok(Distribution::Suse);
} else if id_like.contains(&"arch") || id_like.contains(&"archlinux") {
return Ok(Distribution::Arch);
} else if id_like.contains(&"alpine") {
@@ -127,14 +97,10 @@ impl Distribution {
Distribution::Alpine => upgrade_alpine_linux(ctx),
Distribution::Arch => archlinux::upgrade_arch_linux(ctx),
Distribution::CentOS | Distribution::Fedora => upgrade_redhat(ctx),
Distribution::FedoraSilverblue => upgrade_fedora_silverblue(ctx),
Distribution::ClearLinux => upgrade_clearlinux(ctx),
Distribution::Debian => upgrade_debian(ctx),
Distribution::Gentoo => upgrade_gentoo(ctx),
Distribution::Suse => upgrade_suse(ctx),
Distribution::SuseMicro => upgrade_suse_micro(ctx),
Distribution::OpenSuseTumbleweed => upgrade_opensuse_tumbleweed(ctx),
Distribution::Vanilla => upgrade_vanilla(ctx),
Distribution::Void => upgrade_void(ctx),
Distribution::Solus => upgrade_solus(ctx),
Distribution::Exherbo => upgrade_exherbo(ctx),
@@ -142,7 +108,6 @@ impl Distribution {
Distribution::KDENeon => upgrade_neon(ctx),
Distribution::Bedrock => update_bedrock(ctx),
Distribution::OpenMandriva => upgrade_openmandriva(ctx),
Distribution::PCLinuxOS => upgrade_pclinuxos(ctx),
}
}
@@ -158,14 +123,15 @@ impl Distribution {
}
fn update_bedrock(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), String::from("Sudo required"))?;
ctx.run_type().execute(sudo).args(["brl", "update"]);
let output = Command::new("brl").arg("list").output_checked_utf8()?;
let output = Command::new("brl").arg("list").output()?;
debug!("brl list: {:?} {:?}", output.stdout, output.stderr);
for distribution in output.stdout.trim().lines() {
let parsed_output = String::from_utf8(output.stdout).unwrap();
for distribution in parsed_output.trim().split('\n') {
debug!("Bedrock distribution {}", distribution);
match distribution {
"arch" => archlinux::upgrade_arch_linux(ctx)?,
@@ -182,17 +148,17 @@ fn update_bedrock(ctx: &ExecutionContext) -> Result<()> {
}
fn is_wsl() -> Result<bool> {
let output = Command::new("uname").arg("-r").output_checked_utf8()?.stdout;
let output = Command::new("uname").arg("-r").check_output()?;
debug!("Uname output: {}", output);
Ok(output.contains("microsoft"))
}
fn upgrade_alpine_linux(ctx: &ExecutionContext) -> Result<()> {
let apk = require("apk")?;
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = ctx.sudo().as_ref().unwrap();
ctx.run_type().execute(sudo).arg(&apk).arg("update").status_checked()?;
ctx.run_type().execute(sudo).arg(&apk).arg("upgrade").status_checked()
ctx.run_type().execute(sudo).arg(&apk).arg("update").check_run()?;
ctx.run_type().execute(sudo).arg(&apk).arg("upgrade").check_run()
}
fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> {
@@ -200,11 +166,11 @@ fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> {
if ctx.config().rpm_ostree() {
let mut command = ctx.run_type().execute(ostree);
command.arg("upgrade");
return command.status_checked();
return command.check_run();
}
};
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
if let Some(sudo) = &ctx.sudo() {
let mut command = ctx.run_type().execute(sudo);
command
.arg(which("dnf").unwrap_or_else(|| Path::new("yum").to_path_buf()))
@@ -222,73 +188,41 @@ fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> {
command.arg("-y");
}
command.status_checked()?;
Ok(())
}
command.check_run()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
fn upgrade_fedora_silverblue(ctx: &ExecutionContext) -> Result<()> {
let ostree = require("rpm-ostree")?;
let mut command = ctx.run_type().execute(ostree);
command.arg("upgrade");
command.status_checked()?;
Ok(())
}
fn upgrade_bedrock_strata(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
ctx.run_type().execute(sudo).args(["brl", "update"]).status_checked()?;
if let Some(sudo) = ctx.sudo() {
ctx.run_type().execute(sudo).args(["brl", "update"]).check_run()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_suse(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
ctx.run_type()
.execute(sudo)
.args(["zypper", "refresh"])
.status_checked()?;
if let Some(sudo) = ctx.sudo() {
ctx.run_type().execute(sudo).args(["zypper", "refresh"]).check_run()?;
ctx.run_type()
.execute(sudo)
.arg("zypper")
.arg(if ctx.config().suse_dup() {
"dist-upgrade"
.args(["zypper", "dist-upgrade"])
.check_run()?;
} else {
"update"
})
.status_checked()?;
Ok(())
}
fn upgrade_opensuse_tumbleweed(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
ctx.run_type()
.execute(sudo)
.args(["zypper", "refresh"])
.status_checked()?;
ctx.run_type()
.execute(sudo)
.arg("zypper")
.arg("dist-upgrade")
.status_checked()?;
Ok(())
}
fn upgrade_suse_micro(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
ctx.run_type()
.execute(sudo)
.args(["transactional-update", "dup"])
.status_checked()?;
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_openmandriva(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
if let Some(sudo) = &ctx.sudo() {
let mut command = ctx.run_type().execute(sudo);
command.arg(&which("dnf").unwrap()).arg("upgrade");
@@ -301,70 +235,32 @@ fn upgrade_openmandriva(ctx: &ExecutionContext) -> Result<()> {
command.arg("-y");
}
command.status_checked()?;
Ok(())
}
fn upgrade_pclinuxos(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
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());
command.check_run()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
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()?;
Ok(())
}
fn upgrade_vanilla(ctx: &ExecutionContext) -> Result<()> {
let apx = require("apx")?;
let mut update = ctx.run_type().execute(&apx);
update.args(["update", "--all"]);
if ctx.config().yes(Step::System) {
update.arg("-y");
}
update.status_checked()?;
let mut upgrade = ctx.run_type().execute(&apx);
update.args(["upgrade", "--all"]);
if ctx.config().yes(Step::System) {
upgrade.arg("-y");
}
upgrade.status_checked()?;
Ok(())
}
fn upgrade_void(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
if let Some(sudo) = ctx.sudo() {
let mut command = ctx.run_type().execute(sudo);
command.args(["xbps-install", "-Su", "xbps"]);
if ctx.config().yes(Step::System) {
command.arg("-y");
}
command.status_checked()?;
command.check_run()?;
let mut command = ctx.run_type().execute(sudo);
command.args(["xbps-install", "-u"]);
if ctx.config().yes(Step::System) {
command.arg("-y");
}
command.status_checked()?;
command.check_run()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
@@ -372,13 +268,9 @@ fn upgrade_void(ctx: &ExecutionContext) -> Result<()> {
fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> {
let run_type = ctx.run_type();
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
if let Some(sudo) = &ctx.sudo() {
if let Some(layman) = which("layman") {
run_type
.execute(sudo)
.arg(layman)
.args(["-s", "ALL"])
.status_checked()?;
run_type.execute(sudo).arg(layman).args(["-s", "ALL"]).check_run()?;
}
println!("Syncing portage");
@@ -391,10 +283,10 @@ fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> {
.map(|s| s.split_whitespace().collect())
.unwrap_or_else(|| vec!["-q"]),
)
.status_checked()?;
.check_run()?;
if let Some(eix_update) = which("eix-update") {
run_type.execute(sudo).arg(eix_update).status_checked()?;
run_type.execute(sudo).arg(eix_update).check_run()?;
}
run_type
@@ -406,49 +298,23 @@ fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> {
.map(|s| s.split_whitespace().collect())
.unwrap_or_else(|| vec!["-uDNa", "--with-bdeps=y", "world"]),
)
.status_checked()?;
.check_run()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_debian(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = &ctx.sudo() {
let apt = which("apt-fast")
.or_else(|| {
if which("mist").is_some() {
Some(PathBuf::from("mist"))
} else {
None
}
})
.or_else(|| {
if Path::new("/usr/bin/nala").exists() {
Some(Path::new("/usr/bin/nala").to_path_buf())
} else {
None
}
})
.or_else(|| which("nala"))
.unwrap_or_else(|| PathBuf::from("apt-get"));
let is_mist = apt.ends_with("mist");
let is_nala = apt.ends_with("nala");
// MIST does not require `sudo`
if is_mist {
ctx.run_type().execute(&apt).arg("update").status_checked()?;
ctx.run_type().execute(&apt).arg("upgrade").status_checked()?;
// Simply return as MIST does not have `clean` and `autoremove`
// subcommands, neither the `-y` option (for now maybe?).
return Ok(());
}
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
if !is_nala {
ctx.run_type()
.execute(sudo)
.arg(&apt)
.arg("update")
.status_checked_with_codes(&[0, 100])?;
ctx.run_type().execute(sudo).arg(&apt).arg("update").check_run()?;
}
let mut command = ctx.run_type().execute(sudo);
@@ -464,17 +330,20 @@ fn upgrade_debian(ctx: &ExecutionContext) -> Result<()> {
if let Some(args) = ctx.config().apt_arguments() {
command.args(args.split_whitespace());
}
command.status_checked()?;
command.check_run()?;
if ctx.config().cleanup() {
ctx.run_type().execute(sudo).arg(&apt).arg("clean").status_checked()?;
ctx.run_type().execute(sudo).arg(&apt).arg("clean").check_run()?;
let mut command = ctx.run_type().execute(sudo);
command.arg(&apt).arg("autoremove");
if ctx.config().yes(Step::System) {
command.arg("-y");
}
command.status_checked()?;
command.check_run()?;
}
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
@@ -485,48 +354,24 @@ pub fn run_deb_get(ctx: &ExecutionContext) -> Result<()> {
print_separator("deb-get");
ctx.run_type().execute(&deb_get).arg("update").status_checked()?;
ctx.run_type().execute(&deb_get).arg("upgrade").status_checked()?;
ctx.execute_elevated(&deb_get, false)?.arg("update").check_run()?;
ctx.execute_elevated(&deb_get, false)?.arg("upgrade").check_run()?;
if ctx.config().cleanup() {
ctx.run_type().execute(&deb_get).arg("clean").status_checked()?;
ctx.execute_elevated(&deb_get, false)?.arg("clean").check_run()?;
}
Ok(())
}
fn upgrade_solus(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
ctx.run_type()
.execute(sudo)
.args(["eopkg", "upgrade"])
.status_checked()?;
Ok(())
}
pub fn run_am(ctx: &ExecutionContext) -> Result<()> {
let am = require("am")?;
print_separator("AM");
let mut am = ctx.run_type().execute(am);
if ctx.config().yes(Step::AM) {
am.arg("-U");
if let Some(sudo) = ctx.sudo() {
ctx.run_type().execute(sudo).args(["eopkg", "upgrade"]).check_run()?;
} else {
am.arg("-u");
print_warning("No sudo detected. Skipping system upgrade");
}
am.status_checked()
}
pub fn run_appman(ctx: &ExecutionContext) -> Result<()> {
let appman = require("appman")?;
print_separator("appman");
ctx.run_type().execute(appman).arg("-u").status_checked()
Ok(())
}
pub fn run_pacdef(ctx: &ExecutionContext) -> Result<()> {
@@ -534,28 +379,10 @@ pub fn run_pacdef(ctx: &ExecutionContext) -> Result<()> {
print_separator("pacdef");
let output = ctx.run_type().execute(&pacdef).arg("version").output_checked()?;
let string = String::from_utf8(output.stdout)?;
let new_version = string.contains("version: 1");
if new_version {
ctx.run_type()
.execute(&pacdef)
.args(["package", "sync"])
.status_checked()?;
ctx.run_type().execute(&pacdef).arg("sync").check_run()?;
println!();
ctx.run_type()
.execute(&pacdef)
.args(["package", "review"])
.status_checked()?;
} else {
ctx.run_type().execute(&pacdef).arg("sync").status_checked()?;
println!();
ctx.run_type().execute(&pacdef).arg("review").status_checked()?;
}
Ok(())
ctx.run_type().execute(&pacdef).arg("review").check_run()
}
pub fn run_pacstall(ctx: &ExecutionContext) -> Result<()> {
@@ -563,91 +390,64 @@ pub fn run_pacstall(ctx: &ExecutionContext) -> Result<()> {
print_separator("Pacstall");
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()
}
pub fn run_packer_nu(ctx: &ExecutionContext) -> Result<()> {
let nu = require("nu")?;
let packer_home = HOME_DIR.join(".local/share/nushell/packer");
packer_home.clone().require()?;
print_separator("packer.nu");
ctx.run_type()
.execute(nu)
.env("PWD", "/")
.env("NU_PACKER_HOME", packer_home)
.args([
"-c",
"use ~/.local/share/nushell/packer/start/packer.nu/api_layer/packer.nu; packer update",
])
.status_checked()
ctx.run_type().execute(&pacstall).arg("-U").check_run()?;
ctx.run_type().execute(pacstall).arg("-Up").check_run()
}
fn upgrade_clearlinux(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
ctx.run_type()
.execute(sudo)
.args(["swupd", "update"])
.status_checked()?;
if let Some(sudo) = &ctx.sudo() {
ctx.run_type().execute(sudo).args(["swupd", "update"]).check_run()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_exherbo(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
ctx.run_type().execute(sudo).args(["cave", "sync"]).status_checked()?;
if let Some(sudo) = ctx.sudo() {
ctx.run_type().execute(sudo).args(["cave", "sync"]).check_run()?;
ctx.run_type()
.execute(sudo)
.args(["cave", "resolve", "world", "-c1", "-Cs", "-km", "-Km", "-x"])
.status_checked()?;
.check_run()?;
if ctx.config().cleanup() {
ctx.run_type()
.execute(sudo)
.args(["cave", "purge", "-x"])
.status_checked()?;
ctx.run_type().execute(sudo).args(["cave", "purge", "-x"]).check_run()?;
}
ctx.run_type()
.execute(sudo)
.args(["cave", "fix-linkage", "-x", "--", "-Cs"])
.status_checked()?;
.check_run()?;
ctx.run_type()
.execute(sudo)
.args(["eclectic", "config", "interactive"])
.status_checked()?;
.check_run()?;
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
}
fn upgrade_nixos(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
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 let Some(sudo) = ctx.sudo() {
ctx.run_type()
.execute(sudo)
.args(["/run/current-system/sw/bin/nixos-rebuild", "switch", "--upgrade"])
.check_run()?;
if ctx.config().cleanup() {
ctx.run_type()
.execute(sudo)
.args(["/run/current-system/sw/bin/nix-collect-garbage", "-d"])
.status_checked()?;
.check_run()?;
}
} else {
print_warning("No sudo detected. Skipping system upgrade");
}
Ok(())
@@ -659,15 +459,10 @@ fn upgrade_neon(ctx: &ExecutionContext) -> Result<()> {
// in theory rpm based distributions use pkcon as well, though that
// seems rare
// if that comes up we need to create a Distribution::PackageKit or some such
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
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")
.status_checked()?;
ctx.run_type().execute(sudo).arg(&pkcon).arg("refresh").check_run()?;
let mut exe = ctx.run_type().execute(sudo);
let cmd = exe.arg(&pkcon).arg("update");
if ctx.config().yes(Step::System) {
@@ -677,13 +472,14 @@ fn upgrade_neon(ctx: &ExecutionContext) -> Result<()> {
cmd.arg("--autoremove");
}
// from pkcon man, exit code 5 is 'Nothing useful was done.'
cmd.status_checked_with_codes(&[5])?;
cmd.check_run_with_codes(&[5])?;
}
Ok(())
}
pub fn run_needrestart(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
pub fn run_needrestart(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> {
let sudo = require_option(sudo, String::from("sudo is not installed"))?;
let needrestart = require("needrestart")?;
let distribution = Distribution::detect()?;
@@ -693,7 +489,7 @@ pub fn run_needrestart(ctx: &ExecutionContext) -> Result<()> {
print_separator("Check for needed restarts");
ctx.run_type().execute(sudo).arg(needrestart).status_checked()?;
run_type.execute(sudo).arg(needrestart).check_run()?;
Ok(())
}
@@ -710,7 +506,7 @@ pub fn run_fwupdmgr(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type()
.execute(&fwupdmgr)
.arg("refresh")
.status_checked_with_codes(&[2])?;
.check_run_with_codes(&[2])?;
let mut updmgr = ctx.run_type().execute(&fwupdmgr);
@@ -722,12 +518,12 @@ pub fn run_fwupdmgr(ctx: &ExecutionContext) -> Result<()> {
} else {
updmgr.arg("get-updates");
}
updmgr.status_checked_with_codes(&[2])
updmgr.check_run_with_codes(&[2])
}
pub fn run_flatpak(ctx: &ExecutionContext) -> Result<()> {
pub fn flatpak_update(ctx: &ExecutionContext) -> Result<()> {
let flatpak = require("flatpak")?;
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), String::from("sudo is not installed"))?;
let cleanup = ctx.config().cleanup();
let yes = ctx.config().yes(Step::Flatpak);
let run_type = ctx.run_type();
@@ -737,14 +533,14 @@ pub fn run_flatpak(ctx: &ExecutionContext) -> Result<()> {
if yes {
update_args.push("-y");
}
run_type.execute(&flatpak).args(&update_args).status_checked()?;
run_type.execute(&flatpak).args(&update_args).check_run()?;
if cleanup {
let mut cleanup_args = vec!["uninstall", "--user", "--unused"];
if yes {
cleanup_args.push("-y");
}
run_type.execute(&flatpak).args(&cleanup_args).status_checked()?;
run_type.execute(&flatpak).args(&cleanup_args).check_run()?;
}
print_separator("Flatpak System Packages");
@@ -753,42 +549,34 @@ pub fn run_flatpak(ctx: &ExecutionContext) -> Result<()> {
if yes {
update_args.push("-y");
}
run_type
.execute(sudo)
.arg(&flatpak)
.args(&update_args)
.status_checked()?;
run_type.execute(sudo).arg(&flatpak).args(&update_args).check_run()?;
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)
.status_checked()?;
run_type.execute(sudo).arg(flatpak).args(&cleanup_args).check_run()?;
}
} else {
let mut update_args = vec!["update", "--system"];
if yes {
update_args.push("-y");
}
run_type.execute(&flatpak).args(&update_args).status_checked()?;
run_type.execute(&flatpak).args(&update_args).check_run()?;
if cleanup {
let mut cleanup_args = vec!["uninstall", "--system", "--unused"];
if yes {
cleanup_args.push("-y");
}
run_type.execute(flatpak).args(&cleanup_args).status_checked()?;
run_type.execute(flatpak).args(&cleanup_args).check_run()?;
}
}
Ok(())
}
pub fn run_snap(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
pub fn run_snap(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> {
let sudo = require_option(sudo, String::from("sudo is not installed"))?;
let snap = require("snap")?;
if !PathBuf::from("/var/snapd.socket").exists() && !PathBuf::from("/run/snapd.socket").exists() {
@@ -796,17 +584,17 @@ pub fn run_snap(ctx: &ExecutionContext) -> Result<()> {
}
print_separator("snap");
ctx.run_type().execute(sudo).arg(snap).arg("refresh").status_checked()
run_type.execute(sudo).arg(snap).arg("refresh").check_run()
}
pub fn run_pihole_update(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
pub fn run_pihole_update(sudo: Option<&PathBuf>, 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");
ctx.run_type().execute(sudo).arg(pihole).arg("-up").status_checked()
run_type.execute(sudo).arg(pihole).arg("-up").check_run()
}
pub fn run_protonup_update(ctx: &ExecutionContext) -> Result<()> {
@@ -814,74 +602,26 @@ pub fn run_protonup_update(ctx: &ExecutionContext) -> Result<()> {
print_separator("protonup");
ctx.run_type().execute(protonup).status_checked()?;
Ok(())
}
pub fn run_distrobox_update(ctx: &ExecutionContext) -> Result<()> {
let distrobox = require("distrobox")?;
print_separator("Distrobox");
match (
match (
ctx.run_type().execute(distrobox).arg("upgrade"),
ctx.config().distrobox_containers(),
) {
(r, Some(c)) => {
if c.is_empty() {
return Err(SkipStep("You need to specify at least one container".to_string()).into());
}
r.args(c)
}
(r, None) => r.arg("--all"),
},
ctx.config().distrobox_root(),
) {
(r, true) => r.arg("--root"),
(r, false) => r,
}
.status_checked()
}
pub fn run_dkp_pacman_update(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
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()?;
}
ctx.run_type().execute(protonup).check_run()?;
Ok(())
}
pub fn run_config_update(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), String::from("sudo is not installed"))?;
if ctx.config().yes(Step::ConfigUpdate) {
return Err(SkipStep("Skipped in --yes".to_string()).into());
}
if let Ok(etc_update) = require("etc-update") {
print_separator("Configuration update");
ctx.run_type().execute(sudo).arg(etc_update).status_checked()?;
ctx.run_type().execute(sudo).arg(etc_update).check_run()?;
} 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)?.status_checked()?;
ctx.execute_elevated(&pacdiff, false)?.check_run()?;
}
Ok(())
@@ -950,6 +690,11 @@ mod tests {
test_template(include_str!("os_release/fedora"), Distribution::Fedora);
}
#[test]
fn test_antergos() {
test_template(include_str!("os_release/antergos"), Distribution::Arch);
}
#[test]
fn test_manjaro() {
test_template(include_str!("os_release/manjaro"), Distribution::Arch);
@@ -960,6 +705,11 @@ mod tests {
test_template(include_str!("os_release/manjaro-arm"), Distribution::Arch);
}
#[test]
fn test_anarchy() {
test_template(include_str!("os_release/anarchy"), Distribution::Arch);
}
#[test]
fn test_gentoo() {
test_template(include_str!("os_release/gentoo"), Distribution::Gentoo);
@@ -1004,14 +754,4 @@ mod tests {
fn test_pureos() {
test_template(include_str!("os_release/pureos"), Distribution::Debian);
}
#[test]
fn test_deepin() {
test_template(include_str!("os_release/deepin"), Distribution::Debian);
}
#[test]
fn test_vanilla() {
test_template(include_str!("os_release/vanilla"), Distribution::Vanilla);
}
}

View File

@@ -1,41 +1,36 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, RunType};
use crate::terminal::{print_separator, prompt_yesno};
use crate::utils::{require_option, REQUIRE_SUDO};
use crate::{utils::require, Step};
use color_eyre::eyre::Result;
use crate::{error::TopgradeError, utils::require, Step};
use anyhow::Result;
use log::debug;
use std::fs;
use std::process::Command;
use tracing::debug;
pub fn run_macports(ctx: &ExecutionContext) -> Result<()> {
require("port")?;
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
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"])
.status_checked()?;
.args(&["port", "-u", "upgrade", "outdated"])
.check_run()?;
if ctx.config().cleanup() {
ctx.run_type()
.execute(sudo)
.args(["port", "-N", "reclaim"])
.status_checked()?;
.args(&["port", "-N", "reclaim"])
.check_run()?;
}
Ok(())
}
pub fn run_mas(ctx: &ExecutionContext) -> Result<()> {
pub fn run_mas(run_type: RunType) -> Result<()> {
let mas = require("mas")?;
print_separator("macOS App Store");
ctx.run_type().execute(mas).arg("upgrade").status_checked()
run_type.execute(mas).arg("upgrade").check_run()
}
pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> {
@@ -57,21 +52,26 @@ pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> {
}
let mut command = ctx.run_type().execute("softwareupdate");
command.args(["--install", "--all"]);
command.args(&["--install", "--all"]);
if should_ask {
command.arg("--no-scan");
}
command.status_checked()
command.check_run()
}
fn system_update_available() -> Result<bool> {
let output = Command::new("softwareupdate").arg("--list").output_checked_utf8()?;
let output = Command::new("softwareupdate").arg("--list").output()?;
debug!("{:?}", output);
Ok(!output.stderr.contains("No new software available"))
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"))
}
pub fn run_sparkle(ctx: &ExecutionContext) -> Result<()> {
@@ -81,14 +81,14 @@ pub fn run_sparkle(ctx: &ExecutionContext) -> Result<()> {
for application in (fs::read_dir("/Applications")?).flatten() {
let probe = Command::new(&sparkle)
.args(["--probe", "--application"])
.args(&["--probe", "--application"])
.arg(application.path())
.output_checked_utf8();
.check_output();
if probe.is_ok() {
let mut command = ctx.run_type().execute(&sparkle);
command.args(["bundle", "--check-immediately", "--application"]);
command.args(&["bundle", "--check-immediately", "--application"]);
command.arg(application.path());
command.status_checked()?;
command.spawn()?.wait()?;
}
}
Ok(())

View File

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

View File

@@ -1,23 +0,0 @@
use crate::execution_context::ExecutionContext;
use crate::terminal::print_separator;
use crate::utils::{require_option, REQUIRE_SUDO};
use color_eyre::eyre::Result;
use std::path::PathBuf;
pub fn upgrade_openbsd(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("OpenBSD Update");
ctx.run_type()
.execute(sudo)
.args(&["/usr/sbin/sysupgrade", "-n"])
.status_checked()
}
pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("OpenBSD Packages");
ctx.run_type()
.execute(sudo)
.args(&["/usr/sbin/pkg_add", "-u"])
.status_checked()
}

View File

@@ -0,0 +1,6 @@
NAME="Anarchy Linux"
PRETTY_NAME="Anarchy Linux"
ID=anarchy
ID_LIKE=anarchylinux
ANSI_COLOR="0;36"
HOME_URL="https://anarchylinux.org/"

View File

@@ -0,0 +1,10 @@
NAME="Antergos Linux"
VERSION="18.7-ISO-Rolling"
ID="antergos"
ID_LIKE="arch"
PRETTY_NAME="Antergos Linux"
CPE_NAME="cpe:/o:antergosproject:antergos:18.7"
ANSI_COLOR="1;34;40"
HOME_URL="antergos.com"
SUPPORT_URL="forum.antergos.com"
BUG_REPORT_URL="@antergos"

View File

@@ -1,9 +1,8 @@
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
PRETTY_NAME="Debian GNU/Linux 8 (jessie)"
NAME="Debian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye
VERSION_ID="8"
VERSION="8 (jessie)"
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
HOME_URL="http://www.debian.org/"
SUPPORT_URL="http://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

View File

@@ -1,8 +0,0 @@
PRETTY_NAME="Deepin 20.9"
NAME="Deepin"
VERSION_ID="20.9"
VERSION="20.9"
VERSION_CODENAME="apricot"
ID=Deepin
HOME_URL="https://www.deepin.org/"
BUG_REPORT_URL="https://bbs.deepin.org/"

View File

@@ -1,9 +0,0 @@
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,12 +0,0 @@
PRETTY_NAME="VanillaOS 22.10 all"
NAME="VanillaOS"
VERSION_ID="22.10"
VERSION="22.10 all"
VERSION_CODENAME="kinetic"
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://github.com/vanilla-os"
SUPPORT_URL="https://github.com/vanilla-os"
BUG_REPORT_URL="https://github.com/vanilla-os"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME="kinetic"

View File

@@ -1,41 +1,33 @@
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::var, path::Path};
use std::{env, path::Path};
use crate::command::CommandExt;
use crate::{Step, HOME_DIR};
use color_eyre::eyre::Result;
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;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::executor::RunType;
use crate::terminal::print_separator;
use crate::utils::{require, require_option, PathExt, REQUIRE_SUDO};
#[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 {
@@ -85,147 +77,91 @@ impl BrewVariant {
}
}
pub fn run_fisher(ctx: &ExecutionContext) -> Result<()> {
pub fn run_fisher(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let fish = require("fish")?;
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()))?;
if env::var("fisher_path").is_err() {
base_dirs
.home_dir()
.join(".config/fish/functions/fisher.fish")
.require()?;
}
print_separator("Fisher");
let version_str = ctx
.run_type()
let version_str = run_type
.execute(&fish)
.args(["-c", "fisher --version"])
.output_checked_utf8()?
.stdout;
.check_output()?;
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
ctx.run_type().execute(&fish).args(["-c", "fisher"]).status_checked()
run_type.execute(&fish).args(["-c", "fisher"]).check_run()
} else {
// v4
ctx.run_type()
.execute(&fish)
.args(["-c", "fisher update"])
.status_checked()
run_type.execute(&fish).args(["-c", "fisher update"]).check_run()
}
}
pub fn run_bashit(ctx: &ExecutionContext) -> Result<()> {
HOME_DIR.join(".bash_it").require()?;
ctx.base_dirs().home_dir().join(".bash_it").require()?;
print_separator("Bash-it");
ctx.run_type()
.execute("bash")
.args(["-lic", &format!("bash-it update {}", ctx.config().bashit_branch())])
.status_checked()
}
pub fn run_oh_my_bash(ctx: &ExecutionContext) -> Result<()> {
require("bash")?;
let oh_my_bash = var("OSH")
// default to `~/.oh-my-bash`
.unwrap_or(
HOME_DIR
.join(".oh-my-bash")
.to_str()
.expect("should be UTF-8 encoded")
.to_string(),
)
.require()?;
print_separator("oh-my-bash");
let mut update_script = oh_my_bash;
update_script.push_str("tools/upgrade.sh");
ctx.run_type().execute("bash").arg(update_script).status_checked()
.check_run()
}
pub fn run_oh_my_fish(ctx: &ExecutionContext) -> Result<()> {
let fish = require("fish")?;
HOME_DIR.join(".local/share/omf/pkg/omf/functions/omf.fish").require()?;
ctx.base_dirs()
.home_dir()
.join(".local/share/omf/pkg/omf/functions/omf.fish")
.require()?;
print_separator("oh-my-fish");
ctx.run_type().execute(fish).args(["-c", "omf update"]).status_checked()
ctx.run_type().execute(&fish).args(["-c", "omf update"]).check_run()
}
pub fn run_pkgin(ctx: &ExecutionContext) -> Result<()> {
let pkgin = require("pkgin")?;
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("Pkgin");
let mut command = ctx.run_type().execute(sudo);
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.status_checked()?;
command.check_run()?;
let mut command = ctx.run_type().execute(sudo);
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.status_checked()
command.check_run()
}
pub fn run_fish_plug(ctx: &ExecutionContext) -> Result<()> {
let fish = require("fish")?;
HOME_DIR
ctx.base_dirs()
.home_dir()
.join(".local/share/fish/plug/kidonng/fish-plug/functions/plug.fish")
.require()?;
print_separator("fish-plug");
ctx.run_type()
.execute(fish)
.args(["-c", "plug update"])
.status_checked()
}
/// Upgrades `fundle` and `fundle` plugins.
///
/// `fundle` is a package manager for the Fish shell.
///
/// See: <https://github.com/danhper/fundle>
pub fn run_fundle(ctx: &ExecutionContext) -> Result<()> {
let fish = require("fish")?;
HOME_DIR.join(".config/fish/fundle").require()?;
print_separator("fundle");
ctx.run_type()
.execute(fish)
.args(["-c", "fundle self-update && fundle update"])
.status_checked()
ctx.run_type().execute(&fish).args(["-c", "plug update"]).check_run()
}
#[cfg(not(any(target_os = "android", target_os = "macos")))]
pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
let gdbus = require("gdbus")?;
require_option(
var("XDG_CURRENT_DESKTOP").ok().filter(|p| p.contains("GNOME")),
env::var("XDG_CURRENT_DESKTOP").ok().filter(|p| p.contains("GNOME")),
"Desktop doest not appear to be gnome".to_string(),
)?;
let output = Command::new("gdbus")
@@ -239,10 +175,10 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
"--method",
"org.freedesktop.DBus.ListActivatableNames",
])
.output_checked_utf8()?;
.check_output()?;
debug!("Checking for gnome extensions: {}", output);
if !output.stdout.contains("org.gnome.Shell.Extensions") {
if !output.contains("org.gnome.Shell.Extensions") {
return Err(SkipStep(String::from("Gnome shell extensions are unregistered in DBus")).into());
}
@@ -260,10 +196,9 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
"--method",
"org.gnome.Shell.Extensions.CheckForUpdates",
])
.status_checked()
.check_run()
}
#[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())?;
@@ -278,18 +213,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").status_checked()?;
variant.execute(run_type).arg("update").check_run()?;
variant
.execute(run_type)
.args(["upgrade", "--ignore-pinned", "--formula"])
.status_checked()?;
.check_run()?;
if ctx.config().cleanup() {
variant.execute(run_type).arg("cleanup").status_checked()?;
variant.execute(run_type).arg("cleanup").check_run()?;
}
if ctx.config().brew_autoremove() {
variant.execute(run_type).arg("autoremove").status_checked()?;
variant.execute(run_type).arg("autoremove").check_run()?;
}
Ok(())
@@ -306,28 +241,28 @@ pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()>
let cask_upgrade_exists = variant
.execute(RunType::Wet)
.args(["--repository", "buo/cask-upgrade"])
.output_checked_utf8()
.map(|p| Path::new(p.stdout.trim()).exists())?;
.args(&["--repository", "buo/cask-upgrade"])
.check_output()
.map(|p| Path::new(p.trim()).exists())?;
let mut brew_args = vec![];
if cask_upgrade_exists {
brew_args.extend(["cu", "-y"]);
brew_args.extend(&["cu", "-y"]);
if ctx.config().brew_cask_greedy() {
brew_args.push("-a");
}
} else {
brew_args.extend(["upgrade", "--cask"]);
brew_args.extend(&["upgrade", "--cask"]);
if ctx.config().brew_cask_greedy() {
brew_args.push("--greedy");
}
}
variant.execute(run_type).args(&brew_args).status_checked()?;
variant.execute(run_type).args(&brew_args).check_run()?;
if ctx.config().cleanup() {
variant.execute(run_type).arg("cleanup").status_checked()?;
variant.execute(run_type).arg("cleanup").check_run()?;
}
Ok(())
@@ -338,7 +273,7 @@ pub fn run_guix(ctx: &ExecutionContext) -> Result<()> {
let run_type = ctx.run_type();
let output = Command::new(&guix).arg("pull").output_checked_utf8();
let output = Command::new(&guix).arg("pull").check_output();
debug!("guix pull output: {:?}", output);
let should_upgrade = output.is_ok();
debug!("Can Upgrade Guix: {:?}", should_upgrade);
@@ -346,7 +281,7 @@ pub fn run_guix(ctx: &ExecutionContext) -> Result<()> {
print_separator("Guix");
if should_upgrade {
return run_type.execute(&guix).args(["package", "-u"]).status_checked();
return run_type.execute(&guix).args(["package", "-u"]).check_run();
}
Err(SkipStep(String::from("Guix Pull Failed, Skipping")).into())
}
@@ -355,7 +290,6 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
let nix = require("nix")?;
let nix_channel = require("nix-channel")?;
let nix_env = require("nix-env")?;
// TODO: Is None possible here?
let profile_path = match home::home_dir() {
Some(home) => Path::new(&home).join(".nix-profile"),
None => Path::new("/nix/var/nix/profiles/per-user/default").into(),
@@ -363,7 +297,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"]).output_checked_utf8();
let output = Command::new(&nix_env).args(["--query", "nix"]).check_output();
debug!("nix-env output: {:?}", output);
let should_self_upgrade = output.is_ok();
@@ -395,23 +329,23 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
if should_self_upgrade {
if multi_user {
ctx.execute_elevated(&nix, true)?.arg("upgrade-nix").status_checked()?;
ctx.execute_elevated(&nix, true)?.arg("upgrade-nix").check_run()?;
} else {
run_type.execute(&nix).arg("upgrade-nix").status_checked()?;
run_type.execute(&nix).arg("upgrade-nix").check_run()?;
}
}
run_type.execute(nix_channel).arg("--update").status_checked()?;
run_type.execute(&nix_channel).arg("--update").check_run()?;
if Path::new(&manifest_json_path).exists() {
if std::path::Path::new(&manifest_json_path).exists() {
run_type
.execute(&nix)
.arg("profile")
.arg("upgrade")
.arg(".*")
.status_checked()
.check_run()
} else {
run_type.execute(&nix_env).arg("--upgrade").status_checked()
run_type.execute(&nix_env).arg("--upgrade").check_run()
}
}
@@ -420,51 +354,50 @@ pub fn run_yadm(ctx: &ExecutionContext) -> Result<()> {
print_separator("yadm");
ctx.run_type().execute(yadm).arg("pull").status_checked()
ctx.run_type().execute(&yadm).arg("pull").check_run()
}
pub fn run_asdf(ctx: &ExecutionContext) -> Result<()> {
pub fn run_asdf(run_type: RunType) -> Result<()> {
let asdf = require("asdf")?;
print_separator("asdf");
ctx.run_type()
.execute(&asdf)
.arg("update")
.status_checked_with_codes(&[42])?;
let exit_status = run_type.execute(&asdf).arg("update").spawn()?.wait()?;
ctx.run_type()
.execute(&asdf)
.args(["plugin", "update", "--all"])
.status_checked()
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()
}
pub fn run_home_manager(ctx: &ExecutionContext) -> Result<()> {
pub fn run_home_manager(run_type: RunType) -> Result<()> {
let home_manager = require("home-manager")?;
print_separator("home-manager");
ctx.run_type().execute(home_manager).arg("switch").status_checked()
run_type.execute(&home_manager).arg("switch").check_run()
}
pub fn run_tldr(ctx: &ExecutionContext) -> Result<()> {
pub fn run_tldr(run_type: RunType) -> Result<()> {
let tldr = require("tldr")?;
print_separator("TLDR");
ctx.run_type().execute(tldr).arg("--update").status_checked()
run_type.execute(&tldr).arg("--update").check_run()
}
pub fn run_pearl(ctx: &ExecutionContext) -> Result<()> {
pub fn run_pearl(run_type: RunType) -> Result<()> {
let pearl = require("pearl")?;
print_separator("pearl");
ctx.run_type().execute(pearl).arg("update").status_checked()
run_type.execute(&pearl).arg("update").check_run()
}
pub fn run_sdkman(ctx: &ExecutionContext) -> Result<()> {
pub fn run_sdkman(base_dirs: &BaseDirs, cleanup: bool, run_type: RunType) -> Result<()> {
let bash = require("bash")?;
let sdkman_init_path = var("SDKMAN_DIR")
let sdkman_init_path = env::var("SDKMAN_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".sdkman"))
.unwrap_or_else(|_| base_dirs.home_dir().join(".sdkman"))
.join("bin")
.join("sdkman-init.sh")
.require()
@@ -472,9 +405,9 @@ pub fn run_sdkman(ctx: &ExecutionContext) -> Result<()> {
print_separator("SDKMAN!");
let sdkman_config_path = var("SDKMAN_DIR")
let sdkman_config_path = env::var("SDKMAN_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".sdkman"))
.unwrap_or_else(|_| base_dirs.home_dir().join(".sdkman"))
.join("etc")
.join("config")
.require()?;
@@ -487,36 +420,30 @@ pub fn run_sdkman(ctx: &ExecutionContext) -> Result<()> {
if selfupdate_enabled == "true" {
let cmd_selfupdate = format!("source {} && sdk selfupdate", &sdkman_init_path);
ctx.run_type()
run_type
.execute(&bash)
.args(["-c", cmd_selfupdate.as_str()])
.status_checked()?;
.check_run()?;
}
let cmd_update = format!("source {} && sdk update", &sdkman_init_path);
ctx.run_type()
.execute(&bash)
.args(["-c", cmd_update.as_str()])
.status_checked()?;
run_type.execute(&bash).args(["-c", cmd_update.as_str()]).check_run()?;
let cmd_upgrade = format!("source {} && sdk upgrade", &sdkman_init_path);
ctx.run_type()
.execute(&bash)
.args(["-c", cmd_upgrade.as_str()])
.status_checked()?;
run_type.execute(&bash).args(["-c", cmd_upgrade.as_str()]).check_run()?;
if ctx.config().cleanup() {
if cleanup {
let cmd_flush_archives = format!("source {} && sdk flush archives", &sdkman_init_path);
ctx.run_type()
run_type
.execute(&bash)
.args(["-c", cmd_flush_archives.as_str()])
.status_checked()?;
.check_run()?;
let cmd_flush_temp = format!("source {} && sdk flush temp", &sdkman_init_path);
ctx.run_type()
run_type
.execute(&bash)
.args(["-c", cmd_flush_temp.as_str()])
.status_checked()?;
.check_run()?;
}
Ok(())
@@ -527,27 +454,10 @@ pub fn run_bun(ctx: &ExecutionContext) -> Result<()> {
print_separator("Bun");
ctx.run_type().execute(bun).arg("upgrade").status_checked()
ctx.run_type().execute(&bun).arg("upgrade").check_run()
}
/// Update dotfiles with `rcm(7)`.
///
/// See: <https://github.com/thoughtbot/rcm>
pub fn run_rcm(ctx: &ExecutionContext) -> Result<()> {
let rcup = require("rcup")?;
print_separator("rcm");
ctx.run_type().execute(rcup).arg("-v").status_checked()
}
pub fn run_maza(ctx: &ExecutionContext) -> Result<()> {
let maza = require("maza")?;
print_separator("maza");
ctx.run_type().execute(maza).arg("update").status_checked()
}
pub fn reboot() -> Result<()> {
pub fn reboot() {
print!("Rebooting...");
Command::new("sudo").arg("reboot").status_checked()
Command::new("sudo").arg("reboot").spawn().unwrap().wait().unwrap();
}

View File

@@ -2,12 +2,11 @@ use std::convert::TryFrom;
use std::path::Path;
use std::{ffi::OsStr, process::Command};
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
use tracing::debug;
use anyhow::Result;
use log::debug;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, RunType};
use crate::terminal::{print_separator, print_warning};
use crate::utils::require;
use crate::{error::SkipStep, steps::git::Repositories};
@@ -19,22 +18,23 @@ pub fn run_chocolatey(ctx: &ExecutionContext) -> Result<()> {
print_separator("Chocolatey");
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 cmd = &choco;
let mut args = vec!["upgrade", "all"];
command.args(["upgrade", "all"]);
if let Some(sudo) = ctx.sudo() {
cmd = sudo;
args.insert(0, "choco");
}
let mut command = ctx.run_type().execute(&cmd);
command.args(&args);
if yes {
command.arg("--yes");
}
command.status_checked()
command.check_run()
}
pub fn run_winget(ctx: &ExecutionContext) -> Result<()> {
@@ -47,71 +47,49 @@ 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"])
.status_checked()
ctx.run_type().execute(&winget).args(&["upgrade", "--all"]).check_run()
}
pub fn run_scoop(ctx: &ExecutionContext) -> Result<()> {
pub fn run_scoop(cleanup: bool, run_type: RunType) -> Result<()> {
let scoop = require("scoop")?;
print_separator("Scoop");
ctx.run_type().execute(&scoop).args(["update"]).status_checked()?;
ctx.run_type().execute(&scoop).args(["update", "*"]).status_checked()?;
run_type.execute(&scoop).args(&["update"]).check_run()?;
run_type.execute(&scoop).args(&["update", "*"]).check_run()?;
if ctx.config().cleanup() {
ctx.run_type().execute(&scoop).args(["cleanup", "*"]).status_checked()?;
if cleanup {
run_type.execute(&scoop).args(&["cleanup", "*"]).check_run()?;
}
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"]).output_checked_utf8()?.stdout;
let output = Command::new(wsl).args(&["--list", "-q"]).check_output()?;
Ok(output
.lines()
.filter(|s| !s.is_empty())
.map(|x| x.replace(['\u{0}', '\r'], ""))
.map(|x| x.replace('\u{0}', "").replace('\r', ""))
.collect())
}
fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> Result<()> {
let topgrade = Command::new(wsl)
.args(["-d", dist, "bash", "-lc", "which topgrade"])
.output_checked_utf8()
let topgrade = Command::new(&wsl)
.args(&["-d", dist, "bash", "-lc", "which topgrade"])
.check_output()
.map_err(|_| SkipStep(String::from("Could not find Topgrade installed in WSL")))?;
let mut command = ctx.run_type().execute(wsl);
let mut command = ctx.run_type().execute(&wsl);
command
.args(["-d", dist, "bash", "-c"])
.arg(format!("TOPGRADE_PREFIX={dist} exec {topgrade}"));
.args(&["-d", dist, "bash", "-c"])
.arg(format!("TOPGRADE_PREFIX={} exec {}", dist, topgrade));
if ctx.config().yes(Step::Wsl) {
command.arg("-y");
}
command.status_checked()
command.check_run()
}
pub fn run_wsl_topgrade(ctx: &ExecutionContext) -> Result<()> {
@@ -151,21 +129,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")
.status_checked()?;
ctx.run_type().execute(&usoclient).arg("StartInstall").status_checked()
ctx.run_type().execute(&usoclient).arg("ScanInstallWait").check_run()?;
ctx.run_type().execute(&usoclient).arg("StartInstall").check_run()
}
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 reboot() {
Command::new("shutdown").args(&["/R", "/T", "0"]).spawn().ok();
}
pub fn insert_startup_scripts(git_repos: &mut Repositories) -> Result<()> {
let startup_dir = crate::WINDOWS_DIRS
pub fn insert_startup_scripts(ctx: &ExecutionContext, git_repos: &mut Repositories) -> Result<()> {
let startup_dir = ctx
.base_dirs()
.data_dir()
.join("Microsoft\\Windows\\Start Menu\\Programs\\Startup");
for entry in std::fs::read_dir(&startup_dir)?.flatten() {

View File

@@ -3,10 +3,10 @@ use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use color_eyre::eyre::Result;
use anyhow::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"])
.output_checked_utf8()
.map(|output| PathBuf::from(output.stdout.trim()))
.check_output()
.map(|output| PathBuf::from(output.trim()))
.and_then(|p| p.require())
.ok()
});
@@ -46,14 +46,14 @@ impl Powershell {
#[cfg(windows)]
pub fn has_module(powershell: &Path, command: &str) -> bool {
Command::new(powershell)
.args([
Command::new(&powershell)
.args(&[
"-NoProfile",
"-Command",
&format!("Get-Module -ListAvailable {command}"),
&format!("Get-Module -ListAvailable {}", command),
])
.output_checked_utf8()
.map(|result| !result.stdout.is_empty())
.check_output()
.map(|result| !result.is_empty())
.unwrap_or(false)
}
@@ -79,9 +79,8 @@ impl Powershell {
println!("Updating modules...");
ctx.run_type()
.execute(powershell)
// This probably doesn't need `shell_words::join`.
.args(["-NoProfile", "-Command", &cmd.join(" ")])
.status_checked()
.check_run()
}
#[cfg(windows)]
@@ -100,14 +99,14 @@ impl Powershell {
let mut command = if let Some(sudo) = ctx.sudo() {
let mut command = ctx.run_type().execute(sudo);
command.arg(powershell);
command.arg(&powershell);
command
} else {
ctx.run_type().execute(powershell)
ctx.run_type().execute(&powershell)
};
command
.args([
.args(&[
"-NoProfile",
"-Command",
&format!(
@@ -119,6 +118,6 @@ impl Powershell {
}
),
])
.status_checked()
.check_run()
}
}

View File

@@ -1,8 +1,6 @@
use color_eyre::eyre::Result;
use anyhow::Result;
use crate::{
command::CommandExt, error::SkipStep, execution_context::ExecutionContext, terminal::print_separator, utils,
};
use crate::{error::SkipStep, execution_context::ExecutionContext, terminal::print_separator, utils};
fn prepare_async_ssh_command(args: &mut Vec<&str>) {
args.insert(0, "ssh");
@@ -19,14 +17,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, hostname, &shell_words::join(args))?;
crate::tmux::run_command(ctx, &args.join(" "))?;
Err(SkipStep(String::from("Remote Topgrade launched in Tmux")).into())
}
@@ -43,12 +41,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).status_checked()
ctx.run_type().execute(&ssh).args(&args).check_run()
}
}

View File

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

View File

@@ -1,28 +1,26 @@
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 crate::command::CommandExt;
use crate::executor::RunType;
use crate::terminal::print_separator;
use crate::HOME_DIR;
use crate::{
execution_context::ExecutionContext,
utils::{which, PathExt},
utils::{which, Check, 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(ctx: &ExecutionContext) -> Result<()> {
let tpm = HOME_DIR.join(".tmux/plugins/tpm/bin/update_plugins").require()?;
pub fn run_tpm(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let tpm = base_dirs
.home_dir()
.join(".tmux/plugins/tpm/bin/update_plugins")
.require()?;
print_separator("tmux plugins");
ctx.run_type().execute(tpm).arg("all").status_checked()
run_type.execute(&tpm).arg("all").check_run()
}
struct Tmux {
@@ -31,10 +29,12 @@ struct Tmux {
}
impl Tmux {
fn new(args: Vec<String>) -> Self {
fn new(args: &Option<String>) -> Self {
Self {
tmux: which("tmux").expect("Could not find tmux"),
args: if args.is_empty() { None } else { Some(args) },
args: args
.as_ref()
.map(|args| args.split_whitespace().map(String::from).collect()),
}
}
@@ -46,130 +46,73 @@ impl Tmux {
command
}
fn has_session(&self, session_name: &str) -> Result<bool> {
fn has_session(&self, session_name: &str) -> Result<bool, io::Error> {
Ok(self
.build()
.args(["has-session", "-t", session_name])
.output_checked_with(|_| Ok(()))?
.output()?
.status
.success())
}
/// 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
fn new_session(&self, session_name: &str) -> Result<bool, io::Error> {
Ok(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 (always `topgrade`)
.args(["new-session", "-d", "-s", session_name, "-n", window_name, command])
.output_checked()?;
.args(["new-session", "-d", "-s", session_name, "-n", "dummy"])
.spawn()?
.wait()?
.success())
}
fn run_in_session(&self, command: &str) -> Result<()> {
self.build()
.args(["new-window", "-t", "topgrade", command])
.spawn()?
.wait()?
.check()?;
Ok(())
}
/// 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!()
}
/// 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>) -> Result<()> {
pub fn run_in_tmux(args: &Option<String>) -> ! {
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)
command.join(" ")
};
let tmux = Tmux::new(args);
// 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)?;
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");
// 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-session", "-t", &session]).exec();
Err(eyre!("{err}")).context("Failed to `execvp(3)` tmux")
let err = tmux.build().args(["attach", "-t", "topgrade"]).exec();
panic!("{:?}", err);
} else {
println!("Topgrade launched in a new tmux session");
Ok(())
exit(0);
}
}
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(())
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()
}

View File

@@ -1,20 +1,17 @@
use color_eyre::eyre::Result;
use anyhow::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_checked_utf8()?;
let output = Command::new(toolbx).args(["list", "--containers"]).output()?;
let output_str = String::from_utf8(output.stdout)?;
let proc: Vec<String> = output
.stdout
let proc: Vec<String> = output_str
.lines()
// Skip the first line since that contains only status information
.skip(1)
@@ -36,13 +33,13 @@ pub fn run_toolbx(ctx: &ExecutionContext) -> Result<()> {
debug!("Toolboxes to inspect: {:?}", toolboxes);
let mut topgrade_path = PathBuf::from("/run/host");
// Path of the running Topgrade executable
// Path of the running topgrade executable
// Skip 1 to eliminate the path root, otherwise push overwrites the path
topgrade_path.push(std::env::current_exe()?.components().skip(1).collect::<PathBuf>());
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",
@@ -57,7 +54,7 @@ pub fn run_toolbx(ctx: &ExecutionContext) -> Result<()> {
args.push("--yes");
}
ctx.run_type().execute(&toolbx).args(&args).status_checked()?;
let _output = ctx.run_type().execute(&toolbx).args(&args).check_run();
}
Ok(())

View File

@@ -1,14 +1,3 @@
" AstroUpdate calls a plugin manager - Lazy as of this writing. So we check for it before
" others. Add to init.lua:
" updater = {
" skip_prompts = true,
" },
if exists(":AstroUpdate")
echo "AstroUpdate"
AstroUpdate
quitall
endif
if exists(":NeoBundleUpdate")
echo "NeoBundle"
NeoBundleUpdate
@@ -29,6 +18,11 @@ if exists(":PlugUpgrade")
endif
endif
if exists(":PackerUpdate")
echo "Packer"
PackerSync
endif
if exists("*dein#update()")
echo "dein#update()"
call dein#update()
@@ -44,15 +38,9 @@ if exists(":PaqUpdate")
PaqUpdate
endif
if exists(":Lazy")
echo "Lazy Update"
Lazy! sync | qa
if exists(":CocUpdateSync")
echo "CocUpdateSync"
CocUpdateSync
endif
if exists(':PackerSync')
echo "Packer"
autocmd User PackerComplete quitall
PackerSync
else
quitall
endif
quitall

View File

@@ -1,37 +1,38 @@
use crate::command::CommandExt;
use crate::error::{SkipStep, TopgradeError};
use crate::HOME_DIR;
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
use anyhow::Result;
use crate::executor::{Executor, ExecutorOutput};
use crate::executor::{CommandExt, ExecutorOutput, RunType};
use crate::terminal::print_separator;
use crate::{
execution_context::ExecutionContext,
utils::{require, PathExt},
};
use std::path::PathBuf;
use directories::BaseDirs;
use log::debug;
use std::path::{Path, PathBuf};
use std::{
io::{self, Write},
process::Command,
};
use tracing::debug;
const UPGRADE_VIM: &str = include_str!("upgrade.vim");
pub fn vimrc() -> Result<PathBuf> {
HOME_DIR
pub fn vimrc(base_dirs: &BaseDirs) -> Result<PathBuf> {
base_dirs
.home_dir()
.join(".vimrc")
.require()
.or_else(|_| HOME_DIR.join(".vim/vimrc").require())
.or_else(|_| base_dirs.home_dir().join(".vim/vimrc").require())
}
fn nvimrc() -> Result<PathBuf> {
fn nvimrc(base_dirs: &BaseDirs) -> Result<PathBuf> {
#[cfg(unix)]
let base_dir = crate::XDG_DIRS.config_dir();
let base_dir =
// Bypass directories crate as nvim doesn't use the macOS-specific directories.
std::env::var_os("XDG_CONFIG_HOME").map_or_else(|| base_dirs.home_dir().join(".config"), PathBuf::from);
#[cfg(windows)]
let base_dir = crate::WINDOWS_DIRS.cache_dir();
let base_dir = base_dirs.cache_dir();
base_dir
.join("nvim/init.vim")
@@ -39,14 +40,19 @@ fn nvimrc() -> Result<PathBuf> {
.or_else(|_| base_dir.join("nvim/init.lua").require())
}
fn upgrade_script() -> Result<tempfile::NamedTempFile> {
fn upgrade(vim: &Path, vimrc: &Path, ctx: &ExecutionContext) -> Result<()> {
let mut tempfile = tempfile::NamedTempFile::new()?;
tempfile.write_all(UPGRADE_VIM.replace('\r', "").as_bytes())?;
debug!("Wrote vim script to {:?}", tempfile.path());
Ok(tempfile)
}
fn upgrade(command: &mut Executor, ctx: &ExecutionContext) -> Result<()> {
let mut command = ctx.run_type().execute(vim);
command
.args(["-u"])
.arg(vimrc)
.args(["-U", "NONE", "-V1", "-nNesS"])
.arg(tempfile.path());
if ctx.config().force_vim_plug_update() {
command.env("TOPGRADE_FORCE_PLUGUPDATE", "true");
}
@@ -62,7 +68,7 @@ fn upgrade(command: &mut Executor, ctx: &ExecutionContext) -> Result<()> {
}
if !status.success() {
return Err(TopgradeError::ProcessFailed(command.get_program(), status).into());
return Err(TopgradeError::ProcessFailed(status).into());
} else {
println!("Plugins upgraded")
}
@@ -72,7 +78,7 @@ fn upgrade(command: &mut Executor, ctx: &ExecutionContext) -> Result<()> {
}
pub fn upgrade_ultimate_vimrc(ctx: &ExecutionContext) -> Result<()> {
let config_dir = HOME_DIR.join(".vim_runtime").require()?;
let config_dir = ctx.base_dirs().home_dir().join(".vim_runtime").require()?;
let git = require("git")?;
let python = require("python3")?;
let update_plugins = config_dir.join("update_plugins.py").require()?;
@@ -83,68 +89,52 @@ pub fn upgrade_ultimate_vimrc(ctx: &ExecutionContext) -> Result<()> {
.execute(&git)
.current_dir(&config_dir)
.args(["reset", "--hard"])
.status_checked()?;
.check_run()?;
ctx.run_type()
.execute(&git)
.current_dir(&config_dir)
.args(["clean", "-d", "--force"])
.status_checked()?;
.check_run()?;
ctx.run_type()
.execute(&git)
.current_dir(&config_dir)
.args(["pull", "--rebase"])
.status_checked()?;
.check_run()?;
ctx.run_type()
.execute(python)
.current_dir(config_dir)
.arg(update_plugins)
.status_checked()?;
.check_run()?;
Ok(())
}
pub fn upgrade_vim(ctx: &ExecutionContext) -> Result<()> {
pub fn upgrade_vim(base_dirs: &BaseDirs, ctx: &ExecutionContext) -> Result<()> {
let vim = require("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());
let output = Command::new(&vim).arg("--version").check_output()?;
if !output.starts_with("VIM") {
return Err(SkipStep(String::from("vim binary might by actually nvim")).into());
}
let vimrc = vimrc()?;
let vimrc = vimrc(base_dirs)?;
print_separator("Vim");
upgrade(
ctx.run_type()
.execute(&vim)
.args(["-u"])
.arg(vimrc)
.args(["-U", "NONE", "-V1", "-nNesS"])
.arg(upgrade_script()?.path()),
ctx,
)
upgrade(&vim, &vimrc, ctx)
}
pub fn upgrade_neovim(ctx: &ExecutionContext) -> Result<()> {
pub fn upgrade_neovim(base_dirs: &BaseDirs, ctx: &ExecutionContext) -> Result<()> {
let nvim = require("nvim")?;
let nvimrc = nvimrc()?;
let nvimrc = nvimrc(base_dirs)?;
print_separator("Neovim");
upgrade(
ctx.run_type()
.execute(nvim)
.args(["-u"])
.arg(nvimrc)
.args(["--headless", "-V1", "-nS"])
.arg(upgrade_script()?.path()),
ctx,
)
upgrade(&nvim, &nvimrc, ctx)
}
pub fn run_voom(ctx: &ExecutionContext) -> Result<()> {
pub fn run_voom(_base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let voom = require("voom")?;
print_separator("voom");
ctx.run_type().execute(voom).arg("update").status_checked()
run_type.execute(voom).arg("update").check_run()
}

View File

@@ -1,185 +1,142 @@
use std::env;
use std::path::PathBuf;
use std::process::Command;
use color_eyre::eyre::Result;
use tracing::debug;
use walkdir::WalkDir;
use crate::command::CommandExt;
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 crate::HOME_DIR;
use anyhow::Result;
use directories::BaseDirs;
use log::debug;
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
use walkdir::WalkDir;
pub fn run_zr(ctx: &ExecutionContext) -> Result<()> {
pub fn run_zr(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let zsh = require("zsh")?;
require("zr")?;
print_separator("zr");
let cmd = format!("source {} && zr --update", zshrc().display());
ctx.run_type()
.execute(zsh)
.args(["-l", "-c", cmd.as_str()])
.status_checked()
let cmd = format!("source {} && zr --update", zshrc(base_dirs).display());
run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).check_run()
}
fn zdotdir() -> PathBuf {
pub fn zshrc(base_dirs: &BaseDirs) -> PathBuf {
env::var("ZDOTDIR")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.clone())
.map(|p| Path::new(&p).join(".zshrc"))
.unwrap_or_else(|_| base_dirs.home_dir().join(".zshrc"))
}
pub fn zshrc() -> PathBuf {
zdotdir().join(".zshrc")
}
pub fn run_antidote(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?;
let mut antidote = zdotdir().join(".antidote").require()?;
antidote.push("antidote.zsh");
print_separator("antidote");
ctx.run_type()
.execute(zsh)
.arg("-c")
.arg(format!("source {} && antidote update", antidote.display()))
.status_checked()
}
pub fn run_antibody(ctx: &ExecutionContext) -> Result<()> {
pub fn run_antibody(run_type: RunType) -> Result<()> {
require("zsh")?;
let antibody = require("antibody")?;
print_separator("antibody");
ctx.run_type().execute(antibody).arg("update").status_checked()
run_type.execute(antibody).arg("update").check_run()
}
pub fn run_antigen(ctx: &ExecutionContext) -> Result<()> {
pub fn run_antigen(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let zsh = require("zsh")?;
let zshrc = zshrc().require()?;
let zshrc = zshrc(base_dirs).require()?;
env::var("ADOTDIR")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join("antigen.zsh"))
.unwrap_or_else(|_| base_dirs.home_dir().join("antigen.zsh"))
.require()?;
print_separator("antigen");
let cmd = format!("source {} && (antigen selfupdate ; antigen update)", zshrc.display());
ctx.run_type()
.execute(zsh)
.args(["-l", "-c", cmd.as_str()])
.status_checked()
run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).check_run()
}
pub fn run_zgenom(ctx: &ExecutionContext) -> Result<()> {
pub fn run_zgenom(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let zsh = require("zsh")?;
let zshrc = zshrc().require()?;
let zshrc = zshrc(base_dirs).require()?;
env::var("ZGEN_SOURCE")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".zgenom"))
.unwrap_or_else(|_| base_dirs.home_dir().join(".zgenom"))
.require()?;
print_separator("zgenom");
let cmd = format!("source {} && zgenom selfupdate && zgenom update", zshrc.display());
ctx.run_type()
.execute(zsh)
.args(["-l", "-c", cmd.as_str()])
.status_checked()
run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).check_run()
}
pub fn run_zplug(ctx: &ExecutionContext) -> Result<()> {
pub fn run_zplug(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let zsh = require("zsh")?;
zshrc().require()?;
zshrc(base_dirs).require()?;
env::var("ZPLUG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".zplug"))
.unwrap_or_else(|_| base_dirs.home_dir().join(".zplug"))
.require()?;
print_separator("zplug");
ctx.run_type()
.execute(zsh)
.args(["-i", "-c", "zplug update"])
.status_checked()
run_type.execute(zsh).args(["-i", "-c", "zplug update"]).check_run()
}
pub fn run_zinit(ctx: &ExecutionContext) -> Result<()> {
pub fn run_zinit(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let zsh = require("zsh")?;
let zshrc = zshrc().require()?;
let zshrc = zshrc(base_dirs).require()?;
env::var("ZINIT_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".zinit"))
.unwrap_or_else(|_| base_dirs.home_dir().join(".zinit"))
.require()?;
print_separator("zinit");
let cmd = format!("source {} && zinit self-update && zinit update --all", zshrc.display(),);
ctx.run_type()
.execute(zsh)
.args(["-i", "-c", cmd.as_str()])
.status_checked()
run_type.execute(zsh).args(["-i", "-c", cmd.as_str()]).check_run()
}
pub fn run_zi(ctx: &ExecutionContext) -> Result<()> {
pub fn run_zi(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let zsh = require("zsh")?;
let zshrc = zshrc().require()?;
let zshrc = zshrc(base_dirs).require()?;
HOME_DIR.join(".zi").require()?;
base_dirs.home_dir().join(".zi").require()?;
print_separator("zi");
let cmd = format!("source {} && zi self-update && zi update --all", zshrc.display(),);
ctx.run_type().execute(zsh).args(["-i", "-c", &cmd]).status_checked()
run_type.execute(zsh).args(["-i", "-c", &cmd]).check_run()
}
pub fn run_zim(ctx: &ExecutionContext) -> Result<()> {
pub fn run_zim(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> {
let zsh = require("zsh")?;
env::var("ZIM_HOME")
.or_else(|_| {
Command::new("zsh")
// TODO: Should these be quoted?
.args(["-c", "[[ -n ${ZIM_HOME} ]] && print -n ${ZIM_HOME}"])
.output_checked_utf8()
.map(|o| o.stdout)
.check_output()
})
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".zim"))
.unwrap_or_else(|_| base_dirs.home_dir().join(".zim"))
.require()?;
print_separator("zim");
ctx.run_type()
run_type
.execute(zsh)
.args(["-i", "-c", "zimfw upgrade && zimfw update"])
.status_checked()
.check_run()
}
pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
require("zsh")?;
let oh_my_zsh = env::var("ZSH")
.map(PathBuf::from)
// default to `~/.oh-my-zsh`
.unwrap_or(HOME_DIR.join(".oh-my-zsh"))
.require()?;
let oh_my_zsh = ctx.base_dirs().home_dir().join(".oh-my-zsh").require()?;
print_separator("oh-my-zsh");
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"])
.output_checked_utf8()
.map(|o| o.stdout)
.check_output()
})
.map(PathBuf::from)
.unwrap_or_else(|e| {
@@ -209,10 +166,7 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type()
.execute("zsh")
.env("ZSH", &oh_my_zsh)
.arg(&oh_my_zsh.join("tools/upgrade.sh"))
// oh-my-zsh returns 80 when it is already updated and no changes pulled
// in this update.
// See this comment: https://github.com/r-darwish/topgrade/issues/569#issuecomment-736756731
// for more information.
.status_checked_with_codes(&[80])
.check_run_with_codes(&[80])
}

View File

@@ -1,125 +0,0 @@
use std::ffi::OsStr;
use std::path::Path;
use std::path::PathBuf;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use serde::Deserialize;
use strum::AsRefStr;
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("please").map(|p| (p, SudoKind::Please)))
.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 })
}
/// Create Sudo from SudoKind, if found in the system
pub fn new(kind: SudoKind) -> Option<Self> {
which(kind.as_ref()).map(|path| 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::Please => {
// From `man please`
// -w, --warm
// Warm the access token and exit.
cmd.arg("-w");
}
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, Deserialize, AsRefStr)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum SudoKind {
Doas,
Please,
Sudo,
Gsudo,
Pkexec,
}
impl AsRef<OsStr> for Sudo {
fn as_ref(&self) -> &OsStr {
self.path.as_ref()
}
}

View File

@@ -1,22 +1,24 @@
use std::cmp::{max, min};
use std::env;
use std::io::{self, Write};
#[cfg(target_os = "linux")]
use std::path::PathBuf;
use std::process::Command;
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::utils::which;
lazy_static! {
static ref TERMINAL: Mutex<Terminal> = Mutex::new(Terminal::new());
@@ -32,8 +34,13 @@ pub fn shell() -> &'static str {
which("pwsh").map(|_| "pwsh").unwrap_or("powershell")
}
pub fn run_shell() -> eyre::Result<()> {
Command::new(shell()).env("IN_TOPGRADE", "1").status_checked()
pub fn run_shell() {
Command::new(shell())
.env("IN_TOPGRADE", "1")
.spawn()
.unwrap()
.wait()
.unwrap();
}
struct Terminal {
@@ -43,6 +50,8 @@ struct Terminal {
set_title: bool,
display_time: bool,
desktop_notification: bool,
#[cfg(target_os = "linux")]
notify_send: Option<PathBuf>,
}
impl Terminal {
@@ -52,11 +61,13 @@ 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,
desktop_notification: false,
#[cfg(target_os = "linux")]
notify_send: which("notify-send"),
}
}
@@ -72,11 +83,13 @@ impl Terminal {
self.display_time = display_time
}
#[allow(unused_variables)]
fn notify_desktop<P: AsRef<str>>(&self, message: P, timeout: Option<Duration>) {
debug!("Desktop notification: {}", message.as_ref());
cfg_if::cfg_if! {
if #[cfg(target_os = "macos")] {
let mut notification = Notification::new();
notification
.summary("Topgrade")
notification.summary("Topgrade")
.body(message.as_ref())
.appname("topgrade");
@@ -84,6 +97,19 @@ impl Terminal {
notification.timeout(Timeout::Milliseconds(timeout.as_millis() as u32));
}
notification.show().ok();
} else if #[cfg(target_os = "linux")] {
if let Some(ns) = self.notify_send.as_ref() {
let mut command = Command::new(ns);
if let Some(timeout) = timeout {
command.arg("-t");
command.arg(format!("{}", timeout.as_millis()));
}
command.args(["-a", "Topgrade", "Topgrade"]);
command.arg(message.as_ref());
command.output().ok();
}
}
}
}
fn print_separator<P: AsRef<str>>(&mut self, message: P) {
@@ -116,7 +142,7 @@ impl Terminal {
.write_fmt(format_args!(
"{}\n",
style(format_args!(
"\n── {} {:^border$}",
"\n―― {} {:^border$}",
message,
"",
border = max(
@@ -132,24 +158,11 @@ impl Terminal {
.ok();
}
None => {
self.term.write_fmt(format_args!("―― {message} ――\n")).ok();
self.term.write_fmt(format_args!("―― {} ――\n", message)).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();
@@ -188,7 +201,7 @@ impl Terminal {
self.term
.write_fmt(format_args!(
"{}",
style(format!("{question} (y)es/(N)o",)).yellow().bold()
style(format!("{} (y)es/(N)o", question,)).yellow().bold()
))
.ok();
@@ -201,7 +214,7 @@ impl Terminal {
}
}
#[allow(unused_variables)]
fn should_retry(&mut self, interrupted: bool, step_name: &str) -> eyre::Result<bool> {
fn should_retry(&mut self, interrupted: bool, step_name: &str) -> Result<bool, io::Error> {
if self.width.is_none() {
return Ok(false);
}
@@ -210,35 +223,31 @@ impl Terminal {
self.term.set_title("Topgrade - Awaiting user");
}
if self.desktop_notification {
self.notify_desktop(format!("{step_name} failed"), None);
}
self.notify_desktop(&format!("{} failed", step_name), None);
let prompt_inner = style(format!("{}Retry? (y)es/(N)o/(s)hell/(q)uit", self.prefix))
self.term
.write_fmt(format_args!(
"\n{}",
style(format!("{}Retry? (y)es/(N)o/(s)hell/(q)uit", self.prefix))
.yellow()
.bold();
self.term.write_fmt(format_args!("\n{prompt_inner}")).ok();
.bold()
))
.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");
if let Err(err) = run_shell().context("Failed to run shell") {
self.term.write_fmt(format_args!("{err:?}\n{prompt_inner}")).ok();
} else {
run_shell();
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)).context("Quit from user input")
}
Ok(Key::Char('q')) | Ok(Key::Char('Q')) => return Err(io::Error::from(io::ErrorKind::Interrupted)),
_ => (),
}
};
@@ -259,7 +268,7 @@ impl Default for Terminal {
}
}
pub fn should_retry(interrupted: bool, step_name: &str) -> eyre::Result<bool> {
pub fn should_retry(interrupted: bool, step_name: &str) -> Result<bool, io::Error> {
TERMINAL.lock().unwrap().should_retry(interrupted, step_name)
}
@@ -267,11 +276,6 @@ 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,14 +1,46 @@
use crate::error::{SkipStep, TopgradeError};
use anyhow::Result;
use log::{debug, error};
use std::env;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::{ExitStatus, Output};
use color_eyre::eyre::Result;
use tracing::{debug, error};
pub trait Check {
fn check(self) -> Result<()>;
}
use crate::command::CommandExt;
use crate::error::SkipStep;
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())
}
}
}
pub trait PathExt
where
@@ -70,6 +102,13 @@ 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" }))
@@ -103,151 +142,3 @@ pub fn require_option<T>(option: Option<T>, cause: String) -> Result<T> {
Err(SkipStep(cause).into())
}
}
pub fn string_prepend_str(string: &mut String, s: &str) {
let mut new_string = String::with_capacity(string.len() + s.len());
new_string.push_str(s);
new_string.push_str(string);
*string = new_string;
}
/* 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> {
Command::new("hostname")
.output_checked_utf8()
.map_err(|err| SkipStep(format!("Failed to get hostname: {err}")).into())
.map(|output| output.stdout.trim().to_owned())
}
pub mod merge_strategies {
use merge::Merge;
use crate::config::Commands;
/// Prepends right to left (both Option<Vec<T>>)
pub fn vec_prepend_opt<T>(left: &mut Option<Vec<T>>, right: Option<Vec<T>>) {
if let Some(left_vec) = left {
if let Some(mut right_vec) = right {
right_vec.append(left_vec);
let _ = std::mem::replace(left, Some(right_vec));
}
} else {
*left = right;
}
}
/// Appends an Option<String> to another Option<String>
pub fn string_append_opt(left: &mut Option<String>, right: Option<String>) {
if let Some(left_str) = left {
if let Some(right_str) = right {
left_str.push(' ');
left_str.push_str(&right_str);
}
} else {
*left = right;
}
}
pub fn inner_merge_opt<T>(left: &mut Option<T>, right: Option<T>)
where
T: Merge,
{
if let Some(ref mut left_inner) = left {
if let Some(right_inner) = right {
left_inner.merge(right_inner);
}
} else {
*left = right;
}
}
pub fn commands_merge_opt(left: &mut Option<Commands>, right: Option<Commands>) {
if let Some(ref mut left_inner) = left {
if let Some(right_inner) = right {
left_inner.extend(right_inner);
}
} else {
*left = right;
}
}
}
// Skip causes
// TODO: Put them in a better place when we have more of them
pub const REQUIRE_SUDO: &str = "Require sudo or counterpart but not found, skip";
/// Return `Err(SkipStep)` if `python` is a Python 2 or shim.
///
/// # Shim
/// On Windows, if you install `python` through `winget`, an actual `python`
/// is installed as well as a `python3` shim. Shim is invokable, but when you
/// execute it, the Microsoft App Store will be launched instead of a Python
/// shell.
///
/// We do this check through `python -V`, a shim will just give `Python` with
/// no version number.
pub fn check_is_python_2_or_shim(python: PathBuf) -> Result<PathBuf> {
let output = Command::new(&python).arg("-V").output_checked_utf8()?;
// "Python x.x.x\n"
let stdout = output.stdout;
// ["Python"] or ["Python", "x.x.x"], the newline char is trimmed.
let mut split = stdout.split_whitespace();
if let Some(version) = split.nth(1) {
let major_version = version
.split('.')
.next()
.expect("Should have a major version number")
.parse::<u32>()
.expect("Major version should be a valid number");
if major_version == 2 {
return Err(SkipStep(format!("{} is a Python 2, skip.", python.display())).into());
}
} else {
// No version number, is a shim
return Err(SkipStep(format!("{} is a Python shim, skip.", python.display())).into());
}
Ok(python)
}

80
topgrade.8 Normal file
View File

@@ -0,0 +1,80 @@
.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>