Compare commits

...

25 Commits

Author SHA1 Message Date
SteveLauC
ef0a0d69bb chore: release v16.0.4 (#1169) 2025-06-13 19:03:17 +08:00
Gideon
4b3a3e74f8 Fix "Only one instance of <IDE> can be run at a a time." error (#1123) 2025-06-13 18:54:07 +08:00
Gideon
2c4751c7b2 Add CachyOS support (#1153)
* Add CachyOS support

* Fix os-release file

* Re-run CI
2025-06-12 14:36:35 +08:00
Stuart Reilly
30941ed26d Drop lazy_static (#1168)
Co-authored-by: Stuart Reilly <sreilly@scottlogic.com>
2025-06-12 14:32:29 +08:00
Gideon
c7163b63db Run uv cache prune in uv step when cleanup is enabled (#1165)
Add uv cache prune
2025-06-10 18:10:18 +08:00
SteveLauC
6e6b3dcbfe ci: add missing rustup components (#1166) 2025-06-10 15:43:21 +08:00
Gideon
1d136a6635 Fix nix version output changed (#1140)
* Fix nix version output changed

* Format
2025-04-24 09:31:02 +08:00
Gideon
0ee67d78ef Fix conflict between hx (hexdump alternative) and Helix (#1135)
* Fix conflict between hx (hexdump alternative) and Helix

* Remove uneccessary unit parameter

* Use helix-centered detection
2025-04-21 18:46:00 +08:00
Gideon
7356b920d4 Update jetbrains-toolbox-updater (#1128)
* Update jetbrains-toolbox-updater

* Update to 5.0.0
2025-04-21 11:59:25 +08:00
Gideon
ce8a325c1f Add Yazi step (#1134) 2025-04-21 11:45:01 +08:00
Alexandre Franke
a2f57e4769 fix: correct GNOME spelling (#1124)
Before this change, the spelling was inconsistent. Now it is consistent and follows the upstream spelling. GNOME is spelled all caps, because it is a trademark. (As an exception, it is spelled all lowercase in technical strings, such as identifiers and commands)
2025-04-16 13:46:26 +08:00
Matt Thomson
751f41bc5e Handle format change in asdf version (#1127)
As of the latest version, this now has the format 0.16.7 - i.e. without the hash part.  This commit makes it so that both formats work.

Fixes #1096
2025-04-16 13:35:30 +08:00
Gideon
fd406f0f82 Add output_changed_message!, replace some .expects (#1110) 2025-04-13 16:43:08 +08:00
Gideon
801dddacd4 Omit deb-get clean output (#1119) 2025-04-13 16:36:22 +08:00
Gideon
397a537eef Fix uv step (#1121) 2025-04-13 16:07:57 +08:00
Gideon
0423c836eb Fix vscodium skipping silently (#1109)
* Fix vscodium skipping silently

* Deduplicate

* Format

* Fix x.x.0 version crashing

* Error instead of skipping when there is no first version line

* Format
2025-04-13 11:50:27 +08:00
Gideon
3250337e70 Add JetBrains IDE plugin update steps (#1103)
* Add JetBrains IDE plugin update steps

* Improve comment consistency

* Add comments for missing Windows-only IDEs

* Fix typo

Co-authored-by: SteveLauC <stevelauc@outlook.com>

* Fix missing "plugins" in Android Studio step name

Co-authored-by: SteveLauC <stevelauc@outlook.com>

* Add breaking change to BREAKINGCHANGES_dev.md

---------

Co-authored-by: SteveLauC <stevelauc@outlook.com>
2025-04-11 10:56:24 +08:00
Nils
9dcd7fffe2 Enhancement: Update Windows Package Manager Indexes (#1085)
* fix(windows): update winget sources and upgrade all packages

* refactor(windows): streamline winget upgrade command execution
2025-04-10 19:50:35 +08:00
Nils
30b727b138 fix(powershell): update command arguments to include execution policy (#1041)
* fix(powershell): update command arguments to include execution policy

* fix(powershell): format command arguments for better readability

* fix(powershell): refactor command arguments for Windows execution policy and common options

* fix(powershell): improve execution policy handling and refactor argument passing

* refactor(powershell): streamline command execution and improve profile handling

* fix(powershell): improve code formatting for better readability and maintainability

* fix(powershell): enhance argument validation for improved error handling

* refactor(powershell): simplify Powershell struct methods and enhance command preparation

* refactor(powershell): streamline command building and enhance argument handling

* refactor(powershell): change add_common_args to use instance method for better encapsulation

* refactor(powershell): enhance command building by introducing build_command method and improving argument handling

* refactor(powershell): update build_command return type to match executor.execute() output

* refactor(powershell): refactor command building by introducing build_command_internal and adding profile getter

* refactor(powershell): improve profile retrieval and command execution logic

* refactor(powershell): add support for Windows update and Microsoft Store commands

* refactor(powershell): change args method to arg for accepting all Windows updates

* refactor(powershell): change arg method to args for accepting all Windows updates

* refactor(powershell): change args method to arg for accepting all Windows updates

* refactor(powershell): streamline command construction for Windows updates

* refactor(powershell): update args method for has_module function

* refactor(powershell): improve execution policy handling and command construction for Windows updates

* refactor(powershell): update Microsoft Store update command execution and streamline output handling

* refactor(powershell): clean up whitespace and improve code readability in execution policy checks

---------

Co-authored-by: nistee <lo9s4b7qp@mozmail.com>
2025-04-10 19:48:53 +08:00
SteveLauC
b86d6981ab fix: uv self update (#1105)
Fix #942, the impl is based on this comment https://github.com/topgrade-rs/topgrade/issues/942#issuecomment-2785749010
2025-04-10 17:35:55 +08:00
Gideon
2bf6a2b100 Update CONTRIBUTING.md to reflect enum Step sort enforcement (#1111)
* Update CONTRIBUTING.md

* Formatting
2025-04-09 10:03:32 +08:00
Gideon
3dc8d31d57 Sort Step enum and keep it sorted in the ci.yml workflow (#1104)
* Add `step-enum-sorted` to `ci.yml` workflow

* Sort `Step` enum

* Sort `Step` enum
2025-04-08 19:13:35 +08:00
SteveLauC
b308fb92c0 ci: merge create_assets_xxx workflows and let AUR binary release workflow wait for it (#1100) 2025-04-08 14:34:48 +08:00
dependabot[bot]
bc9746455e chore(deps): bump tokio from 1.38.0 to 1.38.2 (#1101)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.38.0 to 1.38.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.38.0...tokio-1.38.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-08 10:25:58 +08:00
Gideon
109a9c76e3 Downgrade create_release_assets.yml workflow (#1098)
Downgrade `create_release_assets.yml` workflow Ubuntu version to `22.04` from `24.04`
2025-04-07 08:39:56 +08:00
20 changed files with 881 additions and 455 deletions

View File

@@ -22,10 +22,28 @@ jobs:
env: env:
TERM: xterm-256color TERM: xterm-256color
run: | run: |
rustup component add rustfmt
cargo fmt --all -- --check cargo fmt --all -- --check
step-enum-sorted:
name: Step enum sorted
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check if `Step` enum is sorted
run: |
ENUM_NAME="Step"
FILE="src/config.rs"
awk "/enum $ENUM_NAME/,/}/" "$FILE" | \
grep -E '^\s*[A-Za-z_][A-Za-z0-9_]*\s*,?$' | \
sed 's/[, ]//g' > original.txt
sort original.txt > sorted.txt
diff original.txt sorted.txt
main: main:
needs: fmt needs: [fmt, step-enum-sorted]
name: ${{ matrix.target_name }} (check, clippy) name: ${{ matrix.target_name }} (check, clippy)
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@@ -79,7 +97,9 @@ jobs:
run: ${{ matrix.use_cross == true && 'cross' || 'cargo' }} check --locked --target ${{ matrix.target }} run: ${{ matrix.use_cross == true && 'cross' || 'cargo' }} check --locked --target ${{ matrix.target }}
- name: Run cargo/cross clippy - name: Run cargo/cross clippy
run: ${{ matrix.use_cross == true && 'cross' || 'cargo' }} clippy --locked --target ${{ matrix.target }} --all-features -- -D warnings run: |
rustup component add clippy
${{ matrix.use_cross == true && 'cross' || 'cargo' }} clippy --locked --target ${{ matrix.target }} --all-features -- -D warnings
- name: Run cargo test - name: Run cargo test
# ONLY run test with cargo # ONLY run test with cargo

View File

@@ -1,26 +1,23 @@
name: Publish release files for CD native environments name: Publish release files for CD native and non-cd-native environments
on: on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
release: release:
types: [ created ] types: [ created ]
jobs: jobs:
build: # Publish release files for CD native environments
native_build:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
platform: [ ubuntu-latest, macos-latest, macos-13, windows-latest ] platform: [ ubuntu-22.04, macos-latest, macos-13, windows-latest ]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install cargo-deb - name: Install cargo-deb
run: cargo install cargo-deb run: cargo install cargo-deb
if: ${{ matrix.platform == 'ubuntu-latest' }} if: ${{ startsWith(matrix.platform, 'ubuntu-') }}
shell: bash shell: bash
- name: Check format - name: Check format
@@ -59,14 +56,14 @@ jobs:
rm -rf target/release rm -rf target/release
cargo build --release cargo build --release
cargo deb --no-build --no-strip cargo deb --no-build --no-strip
if: ${{ matrix.platform == 'ubuntu-latest' }} if: ${{ startsWith(matrix.platform, 'ubuntu-') }}
shell: bash shell: bash
- name: Move Debian-based system package - name: Move Debian-based system package
run: | run: |
mkdir -p assets mkdir -p assets
mv target/debian/*.deb assets mv target/debian/*.deb assets
if: ${{ matrix.platform == 'ubuntu-latest' }} if: ${{ startsWith(matrix.platform, 'ubuntu-') }}
shell: bash shell: bash
- name: Rename Release (Windows) - name: Rename Release (Windows)
@@ -86,3 +83,91 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: assets/* files: assets/*
# Publish release files for non-CD-native environments
cross_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@v4
- name: Install cargo-deb cross compilation dependencies
run: sudo apt-get install libc6-arm64-cross libgcc-s1-arm64-cross
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
shell: bash
- name: Install cargo-deb cross compilation dependencies for armv7
run: sudo apt-get install libc6-armhf-cross libgcc-s1-armhf-cross
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
shell: bash
- name: Install cargo-deb
run: cargo install cargo-deb
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'armv7-unknown-linux-gnueabihf' }}
shell: bash
- name: install targets
run: rustup target add ${{ matrix.target }}
- name: install cross
uses: taiki-e/install-action@v2
with:
tool: cross@0.2.5
- name: Check format
run: cross fmt --all -- --check
- name: Run clippy
run: cross clippy --all-targets --locked --target ${{matrix.target}} -- -D warnings
- name: Run clippy (All features)
run: cross clippy --locked --all-features --target ${{matrix.target}} -- -D warnings
- name: Run tests
run: cross test --target ${{matrix.target}}
- name: Build in Release profile with all features enabled
run: cross build --release --all-features --target ${{matrix.target}}
- name: Rename Release
run: |
mkdir -p 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: Build Debian-based system package without autoupdate feature
# First remove the binary built by previous steps
# because we don't want the auto-update feature,
# then build the new binary without auto-updating.
run: |
rm -rf target/${{matrix.target}}
cross build --release --target ${{matrix.target}}
cargo deb --target=${{matrix.target}} --no-build --no-strip
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'armv7-unknown-linux-gnueabihf' }}
shell: bash
- name: Move Debian-based system package
run: |
mkdir -p assets
mv target/${{matrix.target}}/debian/*.deb assets
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'armv7-unknown-linux-gnueabihf' }}
shell: bash
- name: Release
uses: softprops/action-gh-release@v2
with:
files: assets/*

View File

@@ -1,97 +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@v4
- name: Install cargo-deb cross compilation dependencies
run: sudo apt-get install libc6-arm64-cross libgcc-s1-arm64-cross
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
shell: bash
- name: Install cargo-deb cross compilation dependencies for armv7
run: sudo apt-get install libc6-armhf-cross libgcc-s1-armhf-cross
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
shell: bash
- name: Install cargo-deb
run: cargo install cargo-deb
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'armv7-unknown-linux-gnueabihf' }}
shell: bash
- name: install targets
run: rustup target add ${{ matrix.target }}
- name: install cross
uses: taiki-e/install-action@v2
with:
tool: cross@0.2.5
- name: Check format
run: cross fmt --all -- --check
- name: Run clippy
run: cross clippy --all-targets --locked --target ${{matrix.target}} -- -D warnings
- name: Run clippy (All features)
run: cross clippy --locked --all-features --target ${{matrix.target}} -- -D warnings
- name: Run tests
run: cross test --target ${{matrix.target}}
- name: Build in Release profile with all features enabled
run: cross build --release --all-features --target ${{matrix.target}}
- name: Rename Release
run: |
mkdir -p 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: Build Debian-based system package without autoupdate feature
# First remove the binary built by previous steps
# because we don't want the auto-update feature,
# then build the new binary without auto-updating.
run: |
rm -rf target/${{matrix.target}}
cross build --release --target ${{matrix.target}}
cargo deb --target=${{matrix.target}} --no-build --no-strip
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'armv7-unknown-linux-gnueabihf' }}
shell: bash
- name: Move Debian-based system package
run: |
mkdir -p assets
mv target/${{matrix.target}}/debian/*.deb assets
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'armv7-unknown-linux-gnueabihf' }}
shell: bash
- name: Release
uses: softprops/action-gh-release@v2
with:
files: assets/*

View File

@@ -1,13 +1,12 @@
name: Publish to AUR name: Publish to AUR
on: on:
# workflow_run: # Step "Publish binary AUR package" needs the binaries built by the following
# workflows: ["Check SemVer compliance"] # workflow, so we wait for it to complete.
# types: workflow_run:
# - completed workflows: ["Publish release files for CD native and non-cd-native environments"]
push: types:
tags: - completed
- "v*"
jobs: jobs:
aur-publish: aur-publish:

View File

@@ -0,0 +1,3 @@
1. The `jet_brains_toolbox` step was renamed to `jetbrains_toolbox`. If you're
using the old name in your configuration file in the `disable` or `only`
fields, simply change it to `jetbrains_toolbox`.

View File

@@ -20,11 +20,11 @@ To add a new `step` to `topgrade`:
```rust ```rust
pub enum Step { pub enum Step {
// Existed steps // Existing steps
// ... // ...
// Your new step here! // Your new step here!
// You may want it to be sorted alphabetically because that looks great:) // Make sure it stays sorted alphabetically because that looks great :)
Xxx, Xxx,
} }
``` ```
@@ -78,7 +78,7 @@ To add a new `step` to `topgrade`:
to separate the steps, for example, for steps that are Linux-only, it goes to separate the steps, for example, for steps that are Linux-only, it goes
like this: like this:
``` ```rust
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
// Xxx is Linux-only // Xxx is Linux-only
@@ -86,7 +86,7 @@ To add a new `step` to `topgrade`:
} }
``` ```
Congrats, you just added a new `step`:) Congrats, you just added a new `step` :)
## Modification to the configuration entries ## Modification to the configuration entries

51
Cargo.lock generated
View File

@@ -1449,9 +1449,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "jetbrains-toolbox-updater" name = "jetbrains-toolbox-updater"
version = "1.4.0" version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d86b38fee698b3f63c9772fe2832d03a84e0724e409d499517c8a102949717" checksum = "5c6bb35a4c18ced364ba2a3952bf5ca2b9231451974f5c2a4c8fa14f300a545b"
dependencies = [ dependencies = [
"dirs 6.0.0", "dirs 6.0.0",
"json", "json",
@@ -1481,9 +1481,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.170" version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]] [[package]]
name = "libredox" name = "libredox"
@@ -1730,6 +1730,15 @@ dependencies = [
"objc_id", "objc_id",
] ]
[[package]]
name = "objc2-core-foundation"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925"
dependencies = [
"bitflags 2.5.0",
]
[[package]] [[package]]
name = "objc_id" name = "objc_id"
version = "0.1.1" version = "0.1.1"
@@ -2002,26 +2011,6 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.4.1"
@@ -2638,15 +2627,14 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.33.1" version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2"
dependencies = [ dependencies = [
"core-foundation-sys",
"libc", "libc",
"memchr", "memchr",
"ntapi", "ntapi",
"rayon", "objc2-core-foundation",
"windows", "windows",
] ]
@@ -2786,9 +2774,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.38.0" version = "1.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" checksum = "68722da18b0fc4a05fdc1120b302b82051265792a1e1b399086e9b204b10ad3d"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@@ -2884,7 +2872,7 @@ dependencies = [
[[package]] [[package]]
name = "topgrade" name = "topgrade"
version = "16.0.3" version = "16.0.4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"chrono", "chrono",
@@ -2898,7 +2886,6 @@ dependencies = [
"glob", "glob",
"home", "home",
"jetbrains-toolbox-updater", "jetbrains-toolbox-updater",
"lazy_static",
"merge", "merge",
"nix 0.29.0", "nix 0.29.0",
"notify-rust", "notify-rust",

View File

@@ -6,7 +6,7 @@ keywords = ["upgrade", "update"]
license = "GPL-3.0" license = "GPL-3.0"
repository = "https://github.com/topgrade-rs/topgrade" repository = "https://github.com/topgrade-rs/topgrade"
rust-version = "1.84.1" rust-version = "1.84.1"
version = "16.0.3" version = "16.0.4"
authors = ["Roey Darwish Dror <roey.ghost@gmail.com>", "Thomas Schönauer <t.schoenauer@hgs-wt.at>"] authors = ["Roey Darwish Dror <roey.ghost@gmail.com>", "Thomas Schönauer <t.schoenauer@hgs-wt.at>"]
exclude = ["doc/screenshot.gif", "BREAKINGCHANGES_dev.md"] exclude = ["doc/screenshot.gif", "BREAKINGCHANGES_dev.md"]
edition = "2021" edition = "2021"
@@ -33,7 +33,6 @@ clap_complete = "~4.5"
clap_mangen = "~0.2" clap_mangen = "~0.2"
walkdir = "~2.5" walkdir = "~2.5"
console = "~0.15" console = "~0.15"
lazy_static = "~1.4"
chrono = "~0.4" chrono = "~0.4"
glob = "~0.3" glob = "~0.3"
strum = { version = "~0.26", features = ["derive"] } strum = { version = "~0.26", features = ["derive"] }
@@ -54,7 +53,7 @@ notify-rust = "~4.11"
wildmatch = "2.3.0" wildmatch = "2.3.0"
rust-i18n = "3.0.1" rust-i18n = "3.0.1"
sys-locale = "0.3.1" sys-locale = "0.3.1"
jetbrains-toolbox-updater = "1.1.0" jetbrains-toolbox-updater = "5.0.0"
[package.metadata.generate-rpm] [package.metadata.generate-rpm]
assets = [{ source = "target/release/topgrade", dest = "/usr/bin/topgrade" }] assets = [{ source = "target/release/topgrade", dest = "/usr/bin/topgrade" }]

View File

@@ -654,29 +654,29 @@ _version: 2
zh_CN: "无法使用 `fish_update_completions`" zh_CN: "无法使用 `fish_update_completions`"
zh_TW: "無法使用 `fish_update_completions`" zh_TW: "無法使用 `fish_update_completions`"
de: "`fish_update_completions` ist nicht verfügbar" de: "`fish_update_completions` ist nicht verfügbar"
"Desktop doest not appear to be gnome": "Desktop does not appear to be GNOME":
en: "Desktop doest not appear to be gnome" en: "Desktop does not appear to be GNOME"
lt: "Darbalaukis, matyt, nėra Gnome" lt: "Darbalaukis, matyt, nėra GNOME"
es: "El escritorio no parece ser Gnome" es: "El escritorio no parece ser GNOME"
fr: "Le bureau ne semble pas être Gnome" fr: "Le bureau ne semble pas être GNOME"
zh_CN: "桌面环境不是 Gnome" zh_CN: "桌面环境不是 GNOME"
zh_TW: "桌面環境不是 Gnome" zh_TW: "桌面環境不是 GNOME"
de: "Desktop scheint nicht GNOME zu sein" de: "Desktop scheint nicht GNOME zu sein"
"Gnome shell extensions are unregistered in DBus": "GNOME shell extensions are unregistered in DBus":
en: "Gnome shell extensions are unregistered in DBus" en: "GNOME shell extensions are unregistered in DBus"
lt: "Gnome Shell priedai nėra užregistruoti DBus'e" lt: "GNOME Shell priedai nėra užregistruoti DBus'e"
es: "Las extensiones de Gnome Shell no están registradas en DBus" es: "Las extensiones de GNOME Shell no están registradas en DBus"
fr: "Les extensions de Gnome Shell ne sont pas enregistrées dans DBus" fr: "Les extensions de GNOME Shell ne sont pas enregistrées dans DBus"
zh_CN: "Gnome Shell 扩展在 DBus中未被注册" zh_CN: "GNOME Shell 扩展在 DBus中未被注册"
zh_TW: "Gnome Shell 擴充功能在 DBus 中未被註冊" zh_TW: "GNOME Shell 擴充功能在 DBus 中未被註冊"
de: "GNOME-Shell-Erweiterungen sind im DBus nicht registriert" de: "GNOME-Shell-Erweiterungen sind im DBus nicht registriert"
"Gnome Shell extensions": "GNOME Shell extensions":
en: "Gnome Shell extensions" en: "GNOME Shell extensions"
lt: "Gnome Shell priedai" lt: "GNOME Shell priedai"
es: "Extensiones de Gnome Shell" es: "Extensiones de GNOME Shell"
fr: "Extensions de Gnome Shell" fr: "Extensions de GNOME Shell"
zh_CN: "Gnome Shell 扩展" zh_CN: "GNOME Shell 扩展"
zh_TW: "Gnome Shell 擴充功能" zh_TW: "GNOME Shell 擴充功能"
de: "GNOME-Shell-Erweiterungen" de: "GNOME-Shell-Erweiterungen"
"Not a custom brew for macOS": "Not a custom brew for macOS":
en: "Not a custom brew for macOS" en: "Not a custom brew for macOS"
@@ -1288,3 +1288,11 @@ _version: 2
zh_CN: "jetbrains-toolbox-updater 在更新过程中遇到意外错误" zh_CN: "jetbrains-toolbox-updater 在更新过程中遇到意外错误"
zh_TW: "jetbrains-toolbox-updater 在更新過程中遇到意外錯誤:" zh_TW: "jetbrains-toolbox-updater 在更新過程中遇到意外錯誤:"
de: "jetbrains-toolbox-updater ist auf einen unerwarteten Fehler während der Aktualisierung gestoßen:" de: "jetbrains-toolbox-updater ist auf einen unerwarteten Fehler während der Aktualisierung gestoßen:"
"<output from `deb-get clean` omitted>":
en: "<output from `deb-get clean` omitted>"
lt: "<išvestis iš `deb-get clean` praleista>"
es: "<salida de `deb-get clean` omitido>"
fr: "<sortie de `deb-get clean` omise>"
zh_CN: "<省略了 `deb-get clean` 的输出>"
zh_TW: "<省略了 `deb-get clean` 的輸出>"
de: "<Ausgabe von `deb-get clean` ausgelassen>"

View File

@@ -52,10 +52,11 @@ pub type Commands = BTreeMap<String, String>;
#[strum(serialize_all = "snake_case")] #[strum(serialize_all = "snake_case")]
pub enum Step { pub enum Step {
AM, AM,
AndroidStudio,
AppMan, AppMan,
Aqua,
Asdf, Asdf,
Atom, Atom,
Aqua,
Audit, Audit,
AutoCpufreq, AutoCpufreq,
Bin, Bin,
@@ -90,30 +91,46 @@ pub enum Step {
Gcloud, Gcloud,
Gem, Gem,
Ghcup, Ghcup,
GithubCliExtensions,
GitRepos, GitRepos,
GithubCliExtensions,
GnomeShellExtensions, GnomeShellExtensions,
Go, Go,
Guix, Guix,
Haxelib, Haxelib,
Helix,
Helm, Helm,
HomeManager, HomeManager,
JetBrainsToolbox, // These names are miscapitalized on purpose, so the CLI name is
// `jetbrains_pycharm` instead of `jet_brains_py_charm`.
JetbrainsAqua,
JetbrainsClion,
JetbrainsDatagrip,
JetbrainsDataspell,
JetbrainsGateway,
JetbrainsGoland,
JetbrainsIdea,
JetbrainsMps,
JetbrainsPhpstorm,
JetbrainsPycharm,
JetbrainsRider,
JetbrainsRubymine,
JetbrainsRustrover,
JetbrainsToolbox,
JetbrainsWebstorm,
Jetpack, Jetpack,
Julia, Julia,
Juliaup, Juliaup,
Kakoune, Kakoune,
Helix,
Krew, Krew,
Lure,
Lensfun, Lensfun,
Lure,
Macports, Macports,
Mamba, Mamba,
Miktex,
Mas, Mas,
Maza, Maza,
Micro, Micro,
MicrosoftStore, MicrosoftStore,
Miktex,
Mise, Mise,
Myrepos, Myrepos,
Nix, Nix,
@@ -174,6 +191,7 @@ pub enum Step {
Xcodes, Xcodes,
Yadm, Yadm,
Yarn, Yarn,
Yazi,
Zigup, Zigup,
Zvm, Zvm,
} }

View File

@@ -446,9 +446,63 @@ fn run() -> Result<()> {
runner.execute(Step::Aqua, "aqua", || generic::run_aqua(&ctx))?; runner.execute(Step::Aqua, "aqua", || generic::run_aqua(&ctx))?;
runner.execute(Step::Bun, "bun", || generic::run_bun(&ctx))?; runner.execute(Step::Bun, "bun", || generic::run_bun(&ctx))?;
runner.execute(Step::Zigup, "zigup", || generic::run_zigup(&ctx))?; runner.execute(Step::Zigup, "zigup", || generic::run_zigup(&ctx))?;
runner.execute(Step::JetBrainsToolbox, "JetBrains Toolbox", || { runner.execute(Step::JetbrainsToolbox, "JetBrains Toolbox", || {
generic::run_jetbrains_toolbox(&ctx) generic::run_jetbrains_toolbox(&ctx)
})?; })?;
runner.execute(Step::AndroidStudio, "Android Studio plugins", || {
generic::run_android_studio(&ctx)
})?;
runner.execute(Step::JetbrainsAqua, "JetBrains Aqua plugins", || {
generic::run_jetbrains_aqua(&ctx)
})?;
runner.execute(Step::JetbrainsClion, "JetBrains CLion plugins", || {
generic::run_jetbrains_clion(&ctx)
})?;
runner.execute(Step::JetbrainsDatagrip, "JetBrains DataGrip plugins", || {
generic::run_jetbrains_datagrip(&ctx)
})?;
runner.execute(Step::JetbrainsDataspell, "JetBrains DataSpell plugins", || {
generic::run_jetbrains_dataspell(&ctx)
})?;
// JetBrains dotCover has no CLI
// JetBrains dotMemory has no CLI
// JetBrains dotPeek has no CLI
// JetBrains dotTrace has no CLI
// JetBrains Fleet has a different CLI without a `fleet update` command.
runner.execute(Step::JetbrainsGateway, "JetBrains Gateway plugins", || {
generic::run_jetbrains_gateway(&ctx)
})?;
runner.execute(Step::JetbrainsGoland, "JetBrains GoLand plugins", || {
generic::run_jetbrains_goland(&ctx)
})?;
runner.execute(Step::JetbrainsIdea, "JetBrains IntelliJ IDEA plugins", || {
generic::run_jetbrains_idea(&ctx)
})?;
runner.execute(Step::JetbrainsMps, "JetBrains MPS plugins", || {
generic::run_jetbrains_mps(&ctx)
})?;
runner.execute(Step::JetbrainsPhpstorm, "JetBrains PhpStorm plugins", || {
generic::run_jetbrains_phpstorm(&ctx)
})?;
runner.execute(Step::JetbrainsPycharm, "JetBrains PyCharm plugins", || {
generic::run_jetbrains_pycharm(&ctx)
})?;
// JetBrains ReSharper has no CLI (it's a VSCode extension)
// JetBrains ReSharper C++ has no CLI (it's a VSCode extension)
runner.execute(Step::JetbrainsRider, "JetBrains Rider plugins", || {
generic::run_jetbrains_rider(&ctx)
})?;
runner.execute(Step::JetbrainsRubymine, "JetBrains RubyMine plugins", || {
generic::run_jetbrains_rubymine(&ctx)
})?;
runner.execute(Step::JetbrainsRustrover, "JetBrains RustRover plugins", || {
generic::run_jetbrains_rustrover(&ctx)
})?;
// JetBrains Space Desktop does not have a CLI
runner.execute(Step::JetbrainsWebstorm, "JetBrains WebStorm plugins", || {
generic::run_jetbrains_webstorm(&ctx)
})?;
runner.execute(Step::Yazi, "Yazi packages", || generic::run_yazi(&ctx))?;
if should_run_powershell { if should_run_powershell {
runner.execute(Step::Powershell, "Powershell Modules Update", || { runner.execute(Step::Powershell, "Powershell Modules Update", || {

View File

@@ -1,33 +1,30 @@
#![allow(unused_imports)]
use std::ffi::{OsStr, OsString};
use std::path::PathBuf;
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::Context;
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use color_eyre::eyre::{eyre, OptionExt};
use jetbrains_toolbox_updater::{find_jetbrains_toolbox, update_jetbrains_toolbox, FindError}; use jetbrains_toolbox_updater::{find_jetbrains_toolbox, update_jetbrains_toolbox, FindError};
use lazy_static::lazy_static;
use regex::bytes::Regex; use regex::bytes::Regex;
use rust_i18n::t; use rust_i18n::t;
use semver::Version; use semver::Version;
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command;
use std::sync::LazyLock;
use std::{env, path::Path};
use std::{fs, io::Write};
use tempfile::tempfile_in; use tempfile::tempfile_in;
use tracing::{debug, error}; use tracing::{debug, error, warn};
use crate::command::{CommandExt, Utf8Output}; use crate::command::{CommandExt, Utf8Output};
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::executor::ExecutorOutput; use crate::executor::ExecutorOutput;
use crate::terminal::{print_separator, shell}; use crate::terminal::{print_separator, shell};
use crate::utils::{self, check_is_python_2_or_shim, get_require_sudo_string, require, require_option, which, PathExt}; use crate::utils::{check_is_python_2_or_shim, get_require_sudo_string, require, require_option, which, PathExt};
use crate::Step;
use crate::HOME_DIR; use crate::HOME_DIR;
use crate::{ use crate::{
error::{SkipStep, StepFailed, TopgradeError}, error::{SkipStep, StepFailed, TopgradeError},
terminal::print_warning, terminal::print_warning,
}; };
use crate::{output_changed_message, Step};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn is_wsl() -> Result<bool> { pub fn is_wsl() -> Result<bool> {
@@ -223,15 +220,46 @@ pub fn run_apm(ctx: &ExecutionContext) -> Result<()> {
.status_checked() .status_checked()
} }
pub fn run_aqua(ctx: &ExecutionContext) -> Result<()> { enum Aqua {
JetBrainsAqua(PathBuf),
AquaCLI(PathBuf),
}
impl Aqua {
fn aqua_cli(self) -> Result<PathBuf> {
match self {
Aqua::AquaCLI(aqua) => Ok(aqua),
Aqua::JetBrainsAqua(_) => {
Err(SkipStep("Command `aqua` probably points to JetBrains Aqua".to_string()).into())
}
}
}
fn jetbrains_aqua(self) -> Result<PathBuf> {
match self {
Aqua::JetBrainsAqua(path) => Ok(path),
Aqua::AquaCLI(_) => Err(SkipStep("Command `aqua` probably points to Aqua CLI".to_string()).into()),
}
}
}
fn get_aqua(ctx: &ExecutionContext) -> Result<Aqua> {
let aqua = require("aqua")?; let aqua = require("aqua")?;
// Check if `aqua --help` mentions "aqua". JetBrains aqua does not, aqua CLI does. // Check if `aqua --help` mentions "aqua". JetBrains Aqua does not, Aqua CLI does.
let output = ctx.run_type().execute(&aqua).arg("--help").output_checked()?; let output = ctx.run_type().execute(&aqua).arg("--help").output_checked()?;
if !String::from_utf8(output.stdout)?.contains("aqua") { if String::from_utf8(output.stdout)?.contains("aqua") {
return Err(SkipStep("Command aqua probably points to JetBrains Aqua".to_string()).into()); debug!("Detected `aqua` as Aqua CLI");
Ok(Aqua::AquaCLI(aqua))
} else {
debug!("Detected `aqua` as JetBrains Aqua");
Ok(Aqua::JetBrainsAqua(aqua))
} }
}
pub fn run_aqua(ctx: &ExecutionContext) -> Result<()> {
let aqua = get_aqua(ctx)?.aqua_cli()?;
print_separator("Aqua"); print_separator("Aqua");
if ctx.run_type().dry() { if ctx.run_type().dry() {
@@ -410,88 +438,86 @@ pub fn run_vcpkg_update(ctx: &ExecutionContext) -> Result<()> {
command.args(["upgrade", "--no-dry-run"]).status_checked() command.args(["upgrade", "--no-dry-run"]).status_checked()
} }
/// Make VSCodium a separate step because: /// This functions runs for both VSCode and VSCodium, as most of the process is the same for both.
/// fn run_vscode_compatible<const VSCODIUM: bool>(ctx: &ExecutionContext) -> Result<()> {
/// 1. Users could use both VSCode and VSCodium // Calling VSCode/VSCodium in WSL may install a server instead of updating extensions (https://github.com/topgrade-rs/topgrade/issues/594#issuecomment-1782157367)
/// 2. Just in case, VSCodium could have incompatible changes with VSCode
pub fn run_vscodium_extensions_update(ctx: &ExecutionContext) -> Result<()> {
// Calling vscodoe in WSL may install a server instead of updating extensions (https://github.com/topgrade-rs/topgrade/issues/594#issuecomment-1782157367)
if is_wsl()? { if is_wsl()? {
return Err(SkipStep(String::from("Should not run in WSL")).into()); return Err(SkipStep(String::from("Should not run in WSL")).into());
} }
let vscodium = require("codium")?; let name = if VSCODIUM { "VSCodium" } else { "VSCode" };
let bin_name = if VSCODIUM { "codium" } else { "code" };
let bin = require(bin_name)?;
// VSCode has update command only since 1.86 version ("january 2024" update), disable the update for prior versions // VSCode has update command only since 1.86 version ("january 2024" update), disable the update for prior versions
// Use command `code --version` which returns 3 lines: version, git commit, instruction set. We parse only the first one // Use command `code --version` which returns 3 lines: version, git commit, instruction set. We parse only the first one
// //
// This should apply to VSCodium as well. // This should apply to VSCodium as well.
let version: Result<Version> = match Command::new(&vscodium) let version: Result<Version> = match Command::new(&bin)
.arg("--version") .arg("--version")
.output_checked_utf8()? .output_checked_utf8()?
.stdout .stdout
.lines() .lines()
.next() .next()
{ {
Some(item) => Version::parse(item).map_err(std::convert::Into::into), Some(item) => {
_ => return Err(SkipStep(String::from("Cannot find vscodium version")).into()), // Strip leading zeroes because `semver` does not allow them, but VSCodium uses them sometimes.
// This is not the case for VSCode, but just in case, and it can't really cause any issues.
let item = item
.split('.')
.map(|s| if s == "0" { "0" } else { s.trim_start_matches('0') })
.collect::<Vec<_>>()
.join(".");
Version::parse(&item).map_err(std::convert::Into::into)
}
None => {
return Err(eyre!(output_changed_message!(
&format!("{bin_name} --version"),
"No first line"
)))
}
}; };
if !matches!(version, Ok(version) if version >= Version::new(1, 86, 0)) { // Raise any errors in parsing the version
return Err(SkipStep(String::from( // The benefit of handling VSCodium versions so old that the version format is something
"Too old vscodium version to have update extensions command", // unexpected is outweighed by the benefit of failing fast on new breaking versions
)) let version =
.into()); version.wrap_err_with(|| output_changed_message!(&format!("{bin_name} --version"), "Invalid version"))?;
debug!("Detected {name} version as: {version}");
if version < Version::new(1, 86, 0) {
return Err(SkipStep(format!("Too old {name} version to have update extensions command")).into());
} }
print_separator("VSCodium extensions"); print_separator(if VSCODIUM {
"VSCodium extensions"
} else {
"Visual Studio Code extensions"
});
ctx.run_type() let mut cmd = ctx.run_type().execute(bin);
.execute(vscodium) // If its VSCode (not VSCodium)
.arg("--update-extensions") if !VSCODIUM {
.status_checked() // And we have configured use of a profile
if let Some(profile) = ctx.config().vscode_profile() {
// Add the profile argument
cmd.arg("--profile").arg(profile);
}
}
cmd.arg("--update-extensions").status_checked()
}
/// Make VSCodium a separate step because:
///
/// 1. Users could use both VSCode and VSCodium
/// 2. Just in case, VSCodium could have incompatible changes with VSCode
pub fn run_vscodium_extensions_update(ctx: &ExecutionContext) -> Result<()> {
run_vscode_compatible::<true>(ctx)
} }
pub fn run_vscode_extensions_update(ctx: &ExecutionContext) -> Result<()> { pub fn run_vscode_extensions_update(ctx: &ExecutionContext) -> Result<()> {
// Calling vscode in WSL may install a server instead of updating extensions (https://github.com/topgrade-rs/topgrade/issues/594#issuecomment-1782157367) run_vscode_compatible::<false>(ctx)
if is_wsl()? {
return Err(SkipStep(String::from("Should not run in WSL")).into());
}
let vscode = require("code")?;
// Vscode has update command only since 1.86 version ("january 2024" update), disable the update for prior versions
// Use command `code --version` which returns 3 lines: version, git commit, instruction set. We parse only the first one
let version: Result<Version> = match Command::new(&vscode)
.arg("--version")
.output_checked_utf8()?
.stdout
.lines()
.next()
{
Some(item) => Version::parse(item).map_err(std::convert::Into::into),
_ => return Err(SkipStep(String::from("Cannot find vscode version")).into()),
};
if !matches!(version, Ok(version) if version >= Version::new(1, 86, 0)) {
return Err(SkipStep(String::from("Too old vscode version to have update extensions command")).into());
}
print_separator("Visual Studio Code extensions");
if let Some(profile) = ctx.config().vscode_profile() {
ctx.run_type()
.execute(vscode)
.arg("--profile")
.arg(profile)
.arg("--update-extensions")
.status_checked()
} else {
ctx.run_type()
.execute(vscode)
.arg("--update-extensions")
.status_checked()
}
} }
pub fn run_pipx_update(ctx: &ExecutionContext) -> Result<()> { pub fn run_pipx_update(ctx: &ExecutionContext) -> Result<()> {
@@ -647,9 +673,12 @@ pub fn run_pip3_update(ctx: &ExecutionContext) -> Result<()> {
{ {
Ok(output) => { Ok(output) => {
let stdout = output.stdout.trim(); let stdout = output.stdout.trim();
stdout stdout.parse::<bool>().wrap_err_with(|| {
.parse::<bool>() output_changed_message!(
.expect("unexpected output that is not `true` or `false`") "pip config get global.break-system-packages",
"unexpected output that is not `true` or `false`"
)
})?
} }
// it can fail because this key may not be set // it can fail because this key may not be set
// //
@@ -976,8 +1005,39 @@ pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> {
Ok(()) Ok(())
} }
enum Hx {
Helix(PathBuf),
HxHexdump,
}
impl Hx {
fn helix(self) -> Result<PathBuf> {
match self {
Hx::Helix(hx) => Ok(hx),
Hx::HxHexdump => {
Err(SkipStep("Command `hx` probably points to hx (hexdump alternative)".to_string()).into())
}
}
}
}
fn get_hx(ctx: &ExecutionContext) -> Result<Hx> {
let hx = require("hx")?;
// Check if `hx --help` mentions "helix". Helix does, hx (hexdump alternative) doesn't.
let output = ctx.run_type().execute(&hx).arg("--help").output_checked()?;
if String::from_utf8(output.stdout)?.contains("helix") {
debug!("Detected `hx` as Helix");
Ok(Hx::Helix(hx))
} else {
debug!("Detected `hx` as hx (hexdump alternative)");
Ok(Hx::HxHexdump)
}
}
pub fn run_helix_grammars(ctx: &ExecutionContext) -> Result<()> { pub fn run_helix_grammars(ctx: &ExecutionContext) -> Result<()> {
let helix = require("helix").or(require("hx"))?; let helix = require("helix").or(get_hx(ctx)?.helix())?;
print_separator("Helix"); print_separator("Helix");
@@ -1169,11 +1229,11 @@ pub fn run_poetry(ctx: &ExecutionContext) -> Result<()> {
// Parse the standard Unix shebang line: #!interpreter [optional-arg] // Parse the standard Unix shebang line: #!interpreter [optional-arg]
// Spaces and tabs on either side of interpreter are ignored. // Spaces and tabs on either side of interpreter are ignored.
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
lazy_static! { static SHEBANG_REGEX: LazyLock<Regex> =
static ref SHEBANG_REGEX: Regex = Regex::new(r"^#![ \t]*([^ \t\n]+)(?:[ \t]+([^\n]+)?)?").unwrap(); LazyLock::new(|| Regex::new(r"^#![ \t]*([^ \t\n]+)(?:[ \t]+([^\n]+)?)?").unwrap());
}
let script = fs::read(poetry)?; let script = fs::read(poetry)?;
if let Some(c) = SHEBANG_REGEX.captures(&script) { if let Some(c) = SHEBANG_REGEX.captures(&script) {
@@ -1192,10 +1252,8 @@ pub fn run_poetry(ctx: &ExecutionContext) -> Result<()> {
use std::str; use std::str;
lazy_static! { static SHEBANG_REGEX: LazyLock<Regex> =
static ref SHEBANG_REGEX: Regex = LazyLock::new(|| Regex::new(r#"^#![ \t]*(?:"([^"\n]+)"|([^" \t\n]+))(?:[ \t]+([^\n]+)?)?"#).unwrap());
Regex::new(r#"^#![ \t]*(?:"([^"\n]+)"|([^" \t\n]+))(?:[ \t]+([^\n]+)?)?"#).unwrap();
}
let data = fs::read(poetry)?; let data = fs::read(poetry)?;
@@ -1277,24 +1335,117 @@ pub fn run_uv(ctx: &ExecutionContext) -> Result<()> {
let uv_exec = require("uv")?; let uv_exec = require("uv")?;
print_separator("uv"); print_separator("uv");
// try uv self --help first - if it succeeds, we call uv self update // 1. Run `uv self update` if the `uv` binary is built with the `self-update`
let result = ctx // cargo feature enabled.
//
// To check if this feature is enabled or not, different version of `uv` need
// different approaches, we need to know the version first and handle them
// separately.
let uv_version_output = ctx
.run_type()
.execute(&uv_exec)
.arg("--version")
.output_checked_utf8()?;
// Multiple possible output formats are possible according to uv source code
//
// https://github.com/astral-sh/uv/blob/6b7f60c1eaa840c2e933a0fb056ab46f99c991a5/crates/uv-cli/src/version.rs#L28-L42
//
// For example:
// "uv 0.5.11 (c4d0caaee 2024-12-19)\n"
// "uv 0.5.11+1 (xxxd0cee 2024-12-20)\n"
// "uv 0.6.14\n"
let uv_version_output_stdout = uv_version_output.stdout;
let version_str = {
// Trim the starting "uv" and " " (whitespace)
let start_trimmed = uv_version_output_stdout
.trim_start_matches("uv")
.trim_start_matches(' ');
// Remove the tailing part " (c4d0caaee 2024-12-19)\n", if it's there
match start_trimmed.find(' ') {
None => start_trimmed.trim_end_matches('\n'), // Otherwise, just strip the newline
Some(i) => &start_trimmed[..i],
}
// After trimming, it should be a string in 2 possible formats, both can be handled by `Version::parse()`
//
// 1. "0.5.11"
// 2. "0.5.11+1"
};
let version =
Version::parse(version_str).wrap_err_with(|| output_changed_message!("uv --version", "Invalid version"))?;
if version < Version::new(0, 4, 25) {
// For uv before version 0.4.25 (exclusive), the `self` sub-command only
// exists under the `self-update` feature, we run `uv self --help` to check
// the feature gate.
let self_update_feature_enabled = ctx
.run_type() .run_type()
.execute(&uv_exec) .execute(&uv_exec)
.args(["self", "--help"]) .args(["self", "--help"])
.output_checked(); .output_checked()
.is_ok();
if result.is_ok() { if self_update_feature_enabled {
ctx.run_type() ctx.run_type()
.execute(&uv_exec) .execute(&uv_exec)
.args(["self", "update"]) .args(["self", "update"])
.status_checked()?; .status_checked()?;
} }
} else {
// After 0.4.25 (inclusive), running `uv self` succeeds regardless of the
// feature gate, so the above approach won't work.
//
// We run `uv self update` directly, if it outputs:
//
// "uv was installed through an external package manager, and self-update is not available. Please use your package manager to update uv.\n"
const ERROR_MSG: &str = "uv was installed through an external package manager, and self-update is not available. Please use your package manager to update uv.";
let output = ctx
.run_type()
.execute(&uv_exec)
.args(["self", "update"])
// `output()` captures the output so that users won't see it for now.
.output()
.expect("this should be ok regardless of this child process's exit code");
let output = match output {
ExecutorOutput::Wet(wet) => wet,
ExecutorOutput::Dry => unreachable!("the whole function returns when we run `uv --version` under dry-run"),
};
let stderr = std::str::from_utf8(&output.stderr).expect("output should be UTF-8 encoded");
if stderr.contains(ERROR_MSG) {
// Feature `self-update` is disabled, nothing to do.
} else {
// Feature is enabled, flush the captured output so that users know we did the self-update.
std::io::stdout().write_all(&output.stdout)?;
std::io::stderr().write_all(&output.stderr)?;
// And, if self update failed, fail the step as well.
if !output.status.success() {
return Err(eyre!("uv self update failed"));
}
}
};
// 2. Update the installed tools
ctx.run_type() ctx.run_type()
.execute(&uv_exec) .execute(&uv_exec)
.args(["tool", "upgrade", "--all"]) .args(["tool", "upgrade", "--all"])
.status_checked() .status_checked()?;
if ctx.config().cleanup() {
// 3. Prune cache
ctx.run_type()
.execute(&uv_exec)
.args(["cache", "prune"])
.status_checked()?;
}
Ok(())
} }
/// Involve `zvm upgrade` to update ZVM /// Involve `zvm upgrade` to update ZVM
@@ -1397,3 +1548,115 @@ pub fn run_jetbrains_toolbox(_ctx: &ExecutionContext) -> Result<()> {
} }
} }
} }
fn run_jetbrains_ide_generic<const IS_JETBRAINS: bool>(ctx: &ExecutionContext, bin: PathBuf, name: &str) -> Result<()> {
let prefix = if IS_JETBRAINS { "JetBrains " } else { "" };
print_separator(format!("{prefix}{name} plugins"));
// The `update` command is undocumented, but tested on all of the below.
let output = ctx.run_type().execute(&bin).arg("update").output()?;
let output = match output {
ExecutorOutput::Dry => return Ok(()),
ExecutorOutput::Wet(output) => output,
};
// Write the output which we swallowed in all cases
std::io::stdout().lock().write_all(&output.stdout).unwrap();
std::io::stderr().lock().write_all(&output.stderr).unwrap();
let stdout = String::from_utf8(output.stdout.clone()).wrap_err("Expected valid UTF-8 output")?;
// "Only one instance of RustRover can be run at a time."
if stdout.contains("Only one instance of ") && stdout.contains(" can be run at a time.") {
// It's always paired with status code 1
let status_code = output
.status
.code()
.ok_or_eyre("Failed to get status code; was killed with signal")?;
if status_code != 1 {
return Err(eyre!("Expected status code 1 ('Only one instance of <IDE> can be run at a time.'), but found status code {}. Output: {output:?}", status_code));
}
// Don't crash, but don't be silent either
warn!("{name} is already running, can't update it now.");
Err(SkipStep(format!("{name} is already running, can't update it now.")).into())
} else if !output.status.success() {
// Unknown failure
Err(eyre!("Running `{bin:?} update` failed. Output: {output:?}"))
} else {
// Success. Output was already written above
Ok(())
}
}
fn run_jetbrains_ide(ctx: &ExecutionContext, bin: PathBuf, name: &str) -> Result<()> {
run_jetbrains_ide_generic::<true>(ctx, bin, name)
}
pub fn run_android_studio(ctx: &ExecutionContext) -> Result<()> {
// We don't use `run_jetbrains_ide` here because that would print "JetBrains Android Studio",
// which is incorrect as Android Studio is made by Google. Just "Android Studio" is fine.
run_jetbrains_ide_generic::<false>(ctx, require("studio")?, "Android Studio")
}
pub fn run_jetbrains_aqua(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, get_aqua(ctx)?.jetbrains_aqua()?, "Aqua")
}
pub fn run_jetbrains_clion(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("clion")?, "CLion")
}
pub fn run_jetbrains_datagrip(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("datagrip")?, "DataGrip")
}
pub fn run_jetbrains_dataspell(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("dataspell")?, "DataSpell")
}
pub fn run_jetbrains_gateway(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("gateway")?, "Gateway")
}
pub fn run_jetbrains_goland(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("goland")?, "Goland")
}
pub fn run_jetbrains_idea(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("idea")?, "IntelliJ IDEA")
}
pub fn run_jetbrains_mps(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("mps")?, "MPS")
}
pub fn run_jetbrains_phpstorm(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("phpstorm")?, "PhpStorm")
}
pub fn run_jetbrains_pycharm(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("pycharm")?, "PyCharm")
}
pub fn run_jetbrains_rider(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("rider")?, "Rider")
}
pub fn run_jetbrains_rubymine(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("rubymine")?, "RubyMine")
}
pub fn run_jetbrains_rustrover(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("rustrover")?, "RustRover")
}
pub fn run_jetbrains_webstorm(ctx: &ExecutionContext) -> Result<()> {
run_jetbrains_ide(ctx, require("webstorm")?, "WebStorm")
}
pub fn run_yazi(ctx: &ExecutionContext) -> Result<()> {
let ya = require("ya")?;
print_separator("Yazi packages");
ctx.run_type().execute(ya).args(["pack", "-u"]).status_checked()
}

View File

@@ -3,7 +3,7 @@ use std::ffi::OsString;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use color_eyre::eyre; use color_eyre::eyre;
use color_eyre::eyre::Result; use color_eyre::eyre::{Context, Result};
use rust_i18n::t; use rust_i18n::t;
use walkdir::WalkDir; use walkdir::WalkDir;
@@ -12,7 +12,7 @@ use crate::error::TopgradeError;
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::utils::require_option; use crate::utils::require_option;
use crate::utils::which; use crate::utils::which;
use crate::{config, Step}; use crate::{config, output_changed_message, Step};
fn get_execution_path() -> OsString { fn get_execution_path() -> OsString {
let mut path = OsString::from("/usr/bin:"); let mut path = OsString::from("/usr/bin:");
@@ -285,7 +285,8 @@ impl ArchPackageManager for Aura {
// Output will be something like: "aura x.x.x\n" // Output will be something like: "aura x.x.x\n"
let version_cmd_stdout = version_cmd_output.stdout; let version_cmd_stdout = version_cmd_output.stdout;
let version_str = version_cmd_stdout.trim_start_matches("aura ").trim_end(); let version_str = version_cmd_stdout.trim_start_matches("aura ").trim_end();
let version = Version::parse(version_str).expect("invalid version"); let version = Version::parse(version_str)
.wrap_err_with(|| output_changed_message!("aura --version", "invalid version"))?;
// Aura, since version 4.0.6, no longer needs sudo. // Aura, since version 4.0.6, no longer needs sudo.
// //

View File

@@ -65,7 +65,7 @@ impl Distribution {
Some("nobara") => Distribution::Nobara, Some("nobara") => Distribution::Nobara,
Some("void") => Distribution::Void, Some("void") => Distribution::Void,
Some("debian") | Some("pureos") | Some("Deepin") | Some("linuxmint") => Distribution::Debian, Some("debian") | Some("pureos") | Some("Deepin") | Some("linuxmint") => Distribution::Debian,
Some("arch") | Some("manjaro-arm") | Some("garuda") | Some("artix") => Distribution::Arch, Some("arch") | Some("manjaro-arm") | Some("garuda") | Some("artix") | Some("cachyos") => Distribution::Arch,
Some("solus") => Distribution::Solus, Some("solus") => Distribution::Solus,
Some("gentoo") | Some("funtoo") => Distribution::Gentoo, Some("gentoo") | Some("funtoo") => Distribution::Gentoo,
Some("exherbo") => Distribution::Exherbo, Some("exherbo") => Distribution::Exherbo,
@@ -587,7 +587,11 @@ pub fn run_deb_get(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type().execute(&deb_get).arg("upgrade").status_checked()?; ctx.run_type().execute(&deb_get).arg("upgrade").status_checked()?;
if ctx.config().cleanup() { if ctx.config().cleanup() {
ctx.run_type().execute(&deb_get).arg("clean").status_checked()?; let output = ctx.run_type().execute(&deb_get).arg("clean").output_checked()?;
// Swallow the output, as it's very noisy and not useful.
// The output is automatically printed as part of `output_checked` when an error occurs.
println!("{}", t!("<output from `deb-get clean` omitted>"));
debug!("`deb-get clean` output: {output:?}");
} }
Ok(()) Ok(())
@@ -1316,4 +1320,9 @@ mod tests {
fn test_bazzite() { fn test_bazzite() {
test_template(include_str!("os_release/bazzite"), Distribution::FedoraImmutable); test_template(include_str!("os_release/bazzite"), Distribution::FedoraImmutable);
} }
#[test]
fn test_cachyos() {
test_template(include_str!("os_release/cachyos"), Distribution::Arch);
}
} }

View File

@@ -0,0 +1,11 @@
NAME="CachyOS Linux"
PRETTY_NAME="CachyOS"
ID=cachyos
BUILD_ID=rolling
ANSI_COLOR="38;2;23;147;209"
HOME_URL="https://cachyos.org/"
DOCUMENTATION_URL="https://wiki.cachyos.org/"
SUPPORT_URL="https://discuss.cachyos.org/"
BUG_REPORT_URL="https://github.com/cachyos"
PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/"
LOGO=cachyos

View File

@@ -1,24 +1,23 @@
use crate::command::CommandExt;
use crate::{output_changed_message, Step, HOME_DIR};
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use home;
use ini::Ini;
#[cfg(target_os = "linux")]
use nix::unistd::Uid;
use regex::Regex;
use rust_i18n::t;
use semver::Version;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs; use std::fs;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::path::Component; use std::path::Component;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use std::sync::LazyLock;
use std::{env::var, path::Path}; use std::{env::var, path::Path};
use crate::command::CommandExt;
use crate::{Step, HOME_DIR};
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use home;
use ini::Ini;
use lazy_static::lazy_static;
#[cfg(target_os = "linux")]
use nix::unistd::Uid;
use regex::Regex;
use rust_i18n::t;
use semver::Version;
use tracing::debug; use tracing::debug;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -238,7 +237,7 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
let gdbus = require("gdbus")?; let gdbus = require("gdbus")?;
require_option( require_option(
var("XDG_CURRENT_DESKTOP").ok().filter(|p| p.contains("GNOME")), var("XDG_CURRENT_DESKTOP").ok().filter(|p| p.contains("GNOME")),
t!("Desktop doest not appear to be gnome").to_string(), t!("Desktop does not appear to be GNOME").to_string(),
)?; )?;
let output = Command::new("gdbus") let output = Command::new("gdbus")
.args([ .args([
@@ -253,12 +252,12 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
]) ])
.output_checked_utf8()?; .output_checked_utf8()?;
debug!("Checking for gnome extensions: {}", output); debug!("Checking for GNOME extensions: {}", output);
if !output.stdout.contains("org.gnome.Shell.Extensions") { if !output.stdout.contains("org.gnome.Shell.Extensions") {
return Err(SkipStep(t!("Gnome shell extensions are unregistered in DBus").to_string()).into()); return Err(SkipStep(t!("GNOME shell extensions are unregistered in DBus").to_string()).into());
} }
print_separator(t!("Gnome Shell extensions")); print_separator(t!("GNOME Shell extensions"));
ctx.run_type() ctx.run_type()
.execute(gdbus) .execute(gdbus)
@@ -461,28 +460,31 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
"`nix --version` output" "`nix --version` output"
); );
lazy_static! { static NIX_VERSION_REGEX: LazyLock<Regex> =
static ref NIX_VERSION_REGEX: Regex = LazyLock::new(|| Regex::new(r"^nix \([^)]*\) ([0-9.]+)").expect("Nix version regex always compiles"));
Regex::new(r"^nix \([^)]*\) ([0-9.]+)").expect("Nix version regex always compiles");
}
if get_version_cmd_first_line_stdout.is_empty() { if get_version_cmd_first_line_stdout.is_empty() {
return Err(eyre!("`nix --version` output was empty")); return Err(eyre!("`nix --version` output was empty"));
} }
let captures = NIX_VERSION_REGEX.captures(get_version_cmd_first_line_stdout); let captures = NIX_VERSION_REGEX
let raw_version = match &captures { .captures(get_version_cmd_first_line_stdout)
None => { .ok_or_else(|| eyre!(output_changed_message!("nix --version", "regex did not match")))?;
return Err(eyre!( let raw_version = &captures[1];
"`nix --version` output was weird: {get_version_cmd_first_line_stdout:?}\n\
If the `nix --version` output format changed, please file an issue to Topgrade" debug!("Raw Nix version: {raw_version}");
));
} // Nix 2.29.0 outputs "2.29" instead of "2.29.0", so we need to add that if necessary.
Some(captures) => &captures[1], let corrected_raw_version = if raw_version.chars().filter(|&c| c == '.').count() == 1 {
&format!("{raw_version}.0")
} else {
raw_version
}; };
let version = debug!("Corrected raw Nix version: {corrected_raw_version}");
Version::parse(raw_version).wrap_err_with(|| format!("Unable to parse Nix version: {raw_version:?}"))?;
let version = Version::parse(corrected_raw_version)
.wrap_err_with(|| output_changed_message!("nix --version", "Invalid version"))?;
debug!("Nix version: {:?}", version); debug!("Nix version: {:?}", version);
@@ -648,15 +650,19 @@ pub fn run_asdf(ctx: &ExecutionContext) -> Result<()> {
// v0.15.0-31e8c93 // v0.15.0-31e8c93
// //
// ``` // ```
// ```
// $ asdf version
// v0.16.7
// ```
let version_stdout = version_output.stdout.trim(); let version_stdout = version_output.stdout.trim();
// trim the starting 'v' // trim the starting 'v'
let mut remaining = version_stdout.trim_start_matches('v'); let mut remaining = version_stdout.trim_start_matches('v');
let idx = remaining // remove the hash part if present
.find('-') if let Some(idx) = remaining.find('-') {
.expect("the output of `asdf version` changed, please file an issue to Topgrade");
// remove the hash part
remaining = &remaining[..idx]; remaining = &remaining[..idx];
let version = Version::parse(remaining).expect("should be a valid version"); }
let version =
Version::parse(remaining).wrap_err_with(|| output_changed_message!("asdf version", "invalid version"))?;
if version < Version::new(0, 15, 0) { if version < Version::new(0, 15, 0) {
ctx.run_type() ctx.run_type()
.execute(&asdf) .execute(&asdf)

View File

@@ -42,12 +42,18 @@ pub fn run_winget(ctx: &ExecutionContext) -> Result<()> {
print_separator("winget"); print_separator("winget");
ctx.run_type()
.execute(&winget)
.args(["source", "update"])
.status_checked()?;
let mut args = vec!["upgrade", "--all"]; let mut args = vec!["upgrade", "--all"];
if ctx.config().winget_silent_install() { if ctx.config().winget_silent_install() {
args.push("--silent"); args.push("--silent");
} }
ctx.run_type().execute(winget).args(args).status_checked() ctx.run_type().execute(&winget).args(args).status_checked()?;
Ok(())
} }
pub fn run_scoop(ctx: &ExecutionContext) -> Result<()> { pub fn run_scoop(ctx: &ExecutionContext) -> Result<()> {
@@ -65,7 +71,6 @@ pub fn run_scoop(ctx: &ExecutionContext) -> Result<()> {
.args(["cache", "rm", "-a"]) .args(["cache", "rm", "-a"])
.status_checked()? .status_checked()?
} }
Ok(()) Ok(())
} }

View File

@@ -1,5 +1,3 @@
#[cfg(windows)]
use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
@@ -18,22 +16,9 @@ pub struct Powershell {
} }
impl Powershell { impl Powershell {
/// Returns a powershell instance.
///
/// If the powershell binary is not found, or the current terminal is dumb
/// then the instance of this struct will skip all the powershell steps.
pub fn new() -> Self { pub fn new() -> Self {
let path = which("pwsh").or_else(|| which("powershell")).filter(|_| !is_dumb()); let path = which("pwsh").or_else(|| which("powershell")).filter(|_| !is_dumb());
let profile = path.as_ref().and_then(Self::get_profile);
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()))
.and_then(super::super::utils::PathExt::require)
.ok()
});
Powershell { path, profile } Powershell { path, profile }
} }
@@ -45,117 +30,178 @@ impl Powershell {
} }
} }
pub fn profile(&self) -> Option<&PathBuf> {
self.profile.as_ref()
}
fn get_profile(path: &PathBuf) -> Option<PathBuf> {
Self::execute_with_command(path, &["-NoProfile", "-Command", "Split-Path $PROFILE"], |stdout| {
Ok(stdout)
})
.ok() // Convert the Result<String> to Option<String>
.and_then(|s| super::super::utils::PathExt::require(PathBuf::from(s)).ok())
}
fn execute_with_command<F>(path: &PathBuf, args: &[&str], f: F) -> Result<String>
where
F: FnOnce(String) -> Result<String>,
{
let output = Command::new(path).args(args).output_checked_utf8()?;
let stdout = output.stdout.trim().to_string();
f(stdout)
}
/// Builds a command with common arguments and optional sudo support.
fn build_command_internal<'a>(
&self,
ctx: &'a ExecutionContext,
additional_args: &[&str],
) -> Result<impl CommandExt + 'a> {
let powershell = require_option(self.path.as_ref(), t!("Powershell is not installed").to_string())?;
let executor = &mut ctx.run_type();
let mut command = if let Some(sudo) = ctx.sudo() {
let mut cmd = executor.execute(sudo);
cmd.arg(powershell);
cmd
} else {
executor.execute(powershell)
};
#[cfg(windows)] #[cfg(windows)]
pub fn has_module(powershell: &Path, command: &str) -> bool { {
// Check execution policy and return early if it's not set correctly
self.execution_policy_args_if_needed()?;
}
command.args(Self::common_args()).args(additional_args);
Ok(command)
}
pub fn update_modules(&self, ctx: &ExecutionContext) -> Result<()> {
print_separator(t!("Powershell Modules Update"));
let mut cmd_args = vec!["Update-Module"];
if ctx.config().verbose() {
cmd_args.push("-Verbose");
}
if ctx.config().yes(Step::Powershell) {
cmd_args.push("-Force");
}
println!("{}", t!("Updating modules..."));
self.build_command_internal(ctx, &cmd_args)?.status_checked()
}
fn common_args() -> &'static [&'static str] {
&["-NoProfile"]
}
#[cfg(windows)]
pub fn execution_policy_args_if_needed(&self) -> Result<()> {
if !self.is_execution_policy_set("RemoteSigned") {
Err(color_eyre::eyre::eyre!(
"PowerShell execution policy is too restrictive. \
Please run 'Set-ExecutionPolicy RemoteSigned -Scope CurrentUser' in PowerShell \
(or use Unrestricted/Bypass if you're sure about the security implications)"
))
} else {
Ok(())
}
}
#[cfg(windows)]
fn is_execution_policy_set(&self, policy: &str) -> bool {
if let Some(powershell) = &self.path {
// These policies are ordered from most restrictive to least restrictive
let valid_policies = ["Restricted", "AllSigned", "RemoteSigned", "Unrestricted", "Bypass"];
// Find the index of our target policy
let target_idx = valid_policies.iter().position(|&p| p == policy);
let output = Command::new(powershell)
.args(["-NoProfile", "-Command", "Get-ExecutionPolicy"])
.output_checked_utf8();
if let Ok(output) = output {
let current_policy = output.stdout.trim();
// Find the index of the current policy
let current_idx = valid_policies.iter().position(|&p| p == current_policy);
// Check if current policy exists and is at least as permissive as the target
return match (current_idx, target_idx) {
(Some(current), Some(target)) => current >= target,
_ => false,
};
}
}
false
}
}
#[cfg(windows)]
impl Powershell {
pub fn supports_windows_update(&self) -> bool {
windows::supports_windows_update(self)
}
pub fn windows_update(&self, ctx: &ExecutionContext) -> Result<()> {
windows::windows_update(self, ctx)
}
pub fn microsoft_store(&self, ctx: &ExecutionContext) -> Result<()> {
windows::microsoft_store(self, ctx)
}
}
#[cfg(windows)]
mod windows {
use super::*;
pub fn supports_windows_update(powershell: &Powershell) -> bool {
powershell
.path
.as_ref()
.map(|p| has_module(p, "PSWindowsUpdate"))
.unwrap_or(false)
}
#[cfg(windows)]
pub fn windows_update(powershell: &Powershell, ctx: &ExecutionContext) -> Result<()> {
debug_assert!(supports_windows_update(powershell));
// Build the full command string
let mut command_str = "Install-WindowsUpdate -Verbose".to_string();
if ctx.config().accept_all_windows_updates() {
command_str.push_str(" -AcceptAll");
}
// Pass the command string using the -Command flag
powershell
.build_command_internal(ctx, &["-Command", &command_str])?
.status_checked()
}
pub fn microsoft_store(powershell: &Powershell, ctx: &ExecutionContext) -> Result<()> {
println!("{}", t!("Scanning for updates..."));
let update_command = "Start-Process powershell -Verb RunAs -ArgumentList '-Command', \
'(Get-CimInstance -Namespace \"Root\\cimv2\\mdm\\dmmap\" \
-ClassName \"MDM_EnterpriseModernAppManagement_AppManagement01\" | \
Invoke-CimMethod -MethodName UpdateScanMethod).ReturnValue'";
powershell
.build_command_internal(ctx, &["-Command", update_command])?
.status_checked()
}
fn has_module(powershell: &PathBuf, command: &str) -> bool {
Command::new(powershell) Command::new(powershell)
.args([ .args([
"-NoProfile", "-NoProfile",
"-Command", "-Command",
&format!("Get-Module -ListAvailable {command}"), &format!("Get-Module -ListAvailable {}", command),
]) ])
.output_checked_utf8() .output_checked_utf8()
.map(|result| !result.stdout.is_empty()) .map(|result| !result.stdout.is_empty())
.unwrap_or(false) .unwrap_or(false)
} }
pub fn profile(&self) -> Option<&PathBuf> {
self.profile.as_ref()
}
pub fn update_modules(&self, ctx: &ExecutionContext) -> Result<()> {
let powershell = require_option(self.path.as_ref(), t!("Powershell is not installed").to_string())?;
print_separator(t!("Powershell Modules Update"));
let mut cmd = vec!["Update-Module"];
if ctx.config().verbose() {
cmd.push("-Verbose");
}
if ctx.config().yes(Step::Powershell) {
cmd.push("-Force");
}
println!("{}", t!("Updating modules..."));
ctx.run_type()
.execute(powershell)
// This probably doesn't need `shell_words::join`.
.args(["-NoProfile", "-Command", &cmd.join(" ")])
.status_checked()
}
#[cfg(windows)]
pub fn supports_windows_update(&self) -> bool {
self.path
.as_ref()
.map(|p| Self::has_module(p, "PSWindowsUpdate"))
.unwrap_or(false)
}
#[cfg(windows)]
pub fn windows_update(&self, ctx: &ExecutionContext) -> Result<()> {
let powershell = require_option(self.path.as_ref(), t!("Powershell is not installed").to_string())?;
debug_assert!(self.supports_windows_update());
let accept_all = if ctx.config().accept_all_windows_updates() {
"-AcceptAll"
} else {
""
};
let install_windowsupdate_verbose = "Install-WindowsUpdate -Verbose".to_string();
let mut command = if let Some(sudo) = ctx.sudo() {
let mut command = ctx.run_type().execute(sudo);
command.arg(powershell);
command
} else {
ctx.run_type().execute(powershell)
};
command
.args(["-NoProfile", &install_windowsupdate_verbose, accept_all])
.status_checked()
}
#[cfg(windows)]
pub fn microsoft_store(&self, ctx: &ExecutionContext) -> Result<()> {
let powershell = require_option(self.path.as_ref(), t!("Powershell is not installed").to_string())?;
let mut command = if let Some(sudo) = ctx.sudo() {
let mut command = ctx.run_type().execute(sudo);
command.arg(powershell);
command
} else {
ctx.run_type().execute(powershell)
};
println!("{}", t!("Scanning for updates..."));
// Scan for updates using the MDM UpdateScanMethod
// This method is also available for non-MDM devices
let update_command = "(Get-CimInstance -Namespace \"Root\\cimv2\\mdm\\dmmap\" -ClassName \"MDM_EnterpriseModernAppManagement_AppManagement01\" | Invoke-CimMethod -MethodName UpdateScanMethod).ReturnValue";
command.args(["-NoProfile", update_command]);
command
.output_checked_with_utf8(|output| {
if output.stdout.trim() == "0" {
println!(
"{}",
t!("Success, Microsoft Store apps are being updated in the background")
);
Ok(())
} else {
println!(
"{}",
t!("Unable to update Microsoft Store apps, manual intervention is required")
);
Err(())
}
})
.map(|_| ())
}
} }

View File

@@ -2,14 +2,13 @@ use std::cmp::{max, min};
use std::env; use std::env;
use std::io::{self, Write}; use std::io::{self, Write};
use std::process::Command; use std::process::Command;
use std::sync::Mutex; use std::sync::{LazyLock, Mutex};
use std::time::Duration; use std::time::Duration;
use chrono::{Local, Timelike}; use chrono::{Local, Timelike};
use color_eyre::eyre; use color_eyre::eyre;
use color_eyre::eyre::Context; use color_eyre::eyre::Context;
use console::{style, Key, Term}; use console::{style, Key, Term};
use lazy_static::lazy_static;
use notify_rust::{Notification, Timeout}; use notify_rust::{Notification, Timeout};
use rust_i18n::t; use rust_i18n::t;
use tracing::{debug, error}; use tracing::{debug, error};
@@ -19,9 +18,7 @@ use which_crate::which;
use crate::command::CommandExt; use crate::command::CommandExt;
use crate::report::StepResult; use crate::report::StepResult;
lazy_static! { static TERMINAL: LazyLock<Mutex<Terminal>> = LazyLock::new(|| Mutex::new(Terminal::new()));
static ref TERMINAL: Mutex<Terminal> = Mutex::new(Terminal::new());
}
#[cfg(unix)] #[cfg(unix)]
pub fn shell() -> String { pub fn shell() -> String {

View File

@@ -282,3 +282,15 @@ pub fn install_color_eyre() -> Result<()> {
.display_location_section(true) .display_location_section(true)
.install() .install()
} }
/// Macro to construct an error message for when the output of a command is unexpected.
#[macro_export]
macro_rules! output_changed_message {
($command:expr, $message:expr) => {
format!(
"The output of `{}` changed: {}. This is not your fault, this is an issue in Topgrade. Please open an issue at: https://github.com/topgrade-rs/topgrade/issues/new?template=bug_report.md",
$command,
$message,
)
};
}