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:
TERM: xterm-256color
run: |
rustup component add rustfmt
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:
needs: fmt
needs: [fmt, step-enum-sorted]
name: ${{ matrix.target_name }} (check, clippy)
runs-on: ${{ matrix.os }}
strategy:
@@ -79,9 +97,11 @@ jobs:
run: ${{ matrix.use_cross == true && 'cross' || 'cargo' }} check --locked --target ${{ matrix.target }}
- 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
# ONLY run test with cargo
if: matrix.use_cross == false
run: cargo test --locked --target ${{ matrix.target }}
run: cargo test --locked --target ${{ matrix.target }}

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:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
release:
types: [ created ]
jobs:
build:
# Publish release files for CD native environments
native_build:
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-latest, macos-latest, macos-13, windows-latest ]
platform: [ ubuntu-22.04, macos-latest, macos-13, windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Install cargo-deb
run: cargo install cargo-deb
if: ${{ matrix.platform == 'ubuntu-latest' }}
if: ${{ startsWith(matrix.platform, 'ubuntu-') }}
shell: bash
- name: Check format
@@ -59,14 +56,14 @@ jobs:
rm -rf target/release
cargo build --release
cargo deb --no-build --no-strip
if: ${{ matrix.platform == 'ubuntu-latest' }}
if: ${{ startsWith(matrix.platform, 'ubuntu-') }}
shell: bash
- name: Move Debian-based system package
run: |
mkdir -p assets
mv target/debian/*.deb assets
if: ${{ matrix.platform == 'ubuntu-latest' }}
if: ${{ startsWith(matrix.platform, 'ubuntu-') }}
shell: bash
- name: Rename Release (Windows)
@@ -86,3 +83,91 @@ jobs:
uses: softprops/action-gh-release@v2
with:
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
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
push:
tags:
- "v*"
# Step "Publish binary AUR package" needs the binaries built by the following
# workflow, so we wait for it to complete.
workflow_run:
workflows: ["Publish release files for CD native and non-cd-native environments"]
types:
- completed
jobs:
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
pub enum Step {
// Existed steps
// Existing steps
// ...
// 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,
}
```
@@ -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
like this:
```
```rust
#[cfg(target_os = "linux")]
{
// 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
@@ -129,12 +129,12 @@ $ cargo test
Don't worry about other platforms, we have most of them covered in our CI.
## I18n
## I18n
If your PR introduces user-facing messages, we need to ensure they are translated.
Please add the translations to [`locales/app.yml`][app_yml]. For simple messages
without arguments (e.g., "hello world"), we can simply translate them according
(Tip: ChatGPT or similar LLMs is good at translation). If a message contains
If your PR introduces user-facing messages, we need to ensure they are translated.
Please add the translations to [`locales/app.yml`][app_yml]. For simple messages
without arguments (e.g., "hello world"), we can simply translate them according
(Tip: ChatGPT or similar LLMs is good at translation). If a message contains
arguments, e.g., "hello <NAME>", please follow this convention:
```yml

51
Cargo.lock generated
View File

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

View File

@@ -6,7 +6,7 @@ keywords = ["upgrade", "update"]
license = "GPL-3.0"
repository = "https://github.com/topgrade-rs/topgrade"
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>"]
exclude = ["doc/screenshot.gif", "BREAKINGCHANGES_dev.md"]
edition = "2021"
@@ -33,7 +33,6 @@ clap_complete = "~4.5"
clap_mangen = "~0.2"
walkdir = "~2.5"
console = "~0.15"
lazy_static = "~1.4"
chrono = "~0.4"
glob = "~0.3"
strum = { version = "~0.26", features = ["derive"] }
@@ -54,7 +53,7 @@ notify-rust = "~4.11"
wildmatch = "2.3.0"
rust-i18n = "3.0.1"
sys-locale = "0.3.1"
jetbrains-toolbox-updater = "1.1.0"
jetbrains-toolbox-updater = "5.0.0"
[package.metadata.generate-rpm]
assets = [{ source = "target/release/topgrade", dest = "/usr/bin/topgrade" }]

View File

@@ -654,29 +654,29 @@ _version: 2
zh_CN: "无法使用 `fish_update_completions`"
zh_TW: "無法使用 `fish_update_completions`"
de: "`fish_update_completions` ist nicht verfügbar"
"Desktop doest not appear to be gnome":
en: "Desktop doest not appear to be gnome"
lt: "Darbalaukis, matyt, nėra Gnome"
es: "El escritorio no parece ser Gnome"
fr: "Le bureau ne semble pas être Gnome"
zh_CN: "桌面环境不是 Gnome"
zh_TW: "桌面環境不是 Gnome"
"Desktop does not appear to be GNOME":
en: "Desktop does not appear to be GNOME"
lt: "Darbalaukis, matyt, nėra GNOME"
es: "El escritorio no parece ser GNOME"
fr: "Le bureau ne semble pas être GNOME"
zh_CN: "桌面环境不是 GNOME"
zh_TW: "桌面環境不是 GNOME"
de: "Desktop scheint nicht GNOME zu sein"
"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"
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"
zh_CN: "Gnome Shell 扩展在 DBus中未被注册"
zh_TW: "Gnome Shell 擴充功能在 DBus 中未被註冊"
"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"
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"
zh_CN: "GNOME Shell 扩展在 DBus中未被注册"
zh_TW: "GNOME Shell 擴充功能在 DBus 中未被註冊"
de: "GNOME-Shell-Erweiterungen sind im DBus nicht registriert"
"Gnome Shell extensions":
en: "Gnome Shell extensions"
lt: "Gnome Shell priedai"
es: "Extensiones de Gnome Shell"
fr: "Extensions de Gnome Shell"
zh_CN: "Gnome Shell 扩展"
zh_TW: "Gnome Shell 擴充功能"
"GNOME Shell extensions":
en: "GNOME Shell extensions"
lt: "GNOME Shell priedai"
es: "Extensiones de GNOME Shell"
fr: "Extensions de GNOME Shell"
zh_CN: "GNOME Shell 扩展"
zh_TW: "GNOME Shell 擴充功能"
de: "GNOME-Shell-Erweiterungen"
"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_TW: "jetbrains-toolbox-updater 在更新過程中遇到意外錯誤:"
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")]
pub enum Step {
AM,
AndroidStudio,
AppMan,
Aqua,
Asdf,
Atom,
Aqua,
Audit,
AutoCpufreq,
Bin,
@@ -90,30 +91,46 @@ pub enum Step {
Gcloud,
Gem,
Ghcup,
GithubCliExtensions,
GitRepos,
GithubCliExtensions,
GnomeShellExtensions,
Go,
Guix,
Haxelib,
Helix,
Helm,
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,
Julia,
Juliaup,
Kakoune,
Helix,
Krew,
Lure,
Lensfun,
Lure,
Macports,
Mamba,
Miktex,
Mas,
Maza,
Micro,
MicrosoftStore,
Miktex,
Mise,
Myrepos,
Nix,
@@ -174,6 +191,7 @@ pub enum Step {
Xcodes,
Yadm,
Yarn,
Yazi,
Zigup,
Zvm,
}

View File

@@ -446,9 +446,63 @@ fn run() -> Result<()> {
runner.execute(Step::Aqua, "aqua", || generic::run_aqua(&ctx))?;
runner.execute(Step::Bun, "bun", || generic::run_bun(&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)
})?;
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 {
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::Result;
use color_eyre::eyre::{eyre, OptionExt};
use jetbrains_toolbox_updater::{find_jetbrains_toolbox, update_jetbrains_toolbox, FindError};
use lazy_static::lazy_static;
use regex::bytes::Regex;
use rust_i18n::t;
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 tracing::{debug, error};
use tracing::{debug, error, warn};
use crate::command::{CommandExt, Utf8Output};
use crate::execution_context::ExecutionContext;
use crate::executor::ExecutorOutput;
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::Step;
use crate::utils::{check_is_python_2_or_shim, get_require_sudo_string, require, require_option, which, PathExt};
use crate::HOME_DIR;
use crate::{
error::{SkipStep, StepFailed, TopgradeError},
terminal::print_warning,
};
use crate::{output_changed_message, Step};
#[cfg(target_os = "linux")]
pub fn is_wsl() -> Result<bool> {
@@ -223,15 +220,46 @@ pub fn run_apm(ctx: &ExecutionContext) -> Result<()> {
.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")?;
// 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()?;
if !String::from_utf8(output.stdout)?.contains("aqua") {
return Err(SkipStep("Command aqua probably points to JetBrains Aqua".to_string()).into());
if String::from_utf8(output.stdout)?.contains("aqua") {
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");
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()
}
/// 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<()> {
// Calling vscodoe in WSL may install a server instead of updating extensions (https://github.com/topgrade-rs/topgrade/issues/594#issuecomment-1782157367)
/// 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<()> {
// Calling VSCode/VSCodium in WSL may install a server instead of updating extensions (https://github.com/topgrade-rs/topgrade/issues/594#issuecomment-1782157367)
if is_wsl()? {
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
// 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.
let version: Result<Version> = match Command::new(&vscodium)
let version: Result<Version> = match Command::new(&bin)
.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 vscodium version")).into()),
Some(item) => {
// 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)) {
return Err(SkipStep(String::from(
"Too old vscodium version to have update extensions command",
))
.into());
// Raise any errors in parsing the version
// The benefit of handling VSCodium versions so old that the version format is something
// unexpected is outweighed by the benefit of failing fast on new breaking versions
let version =
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()
.execute(vscodium)
.arg("--update-extensions")
.status_checked()
let mut cmd = ctx.run_type().execute(bin);
// If its VSCode (not VSCodium)
if !VSCODIUM {
// 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<()> {
// Calling vscode in WSL may install a server instead of updating extensions (https://github.com/topgrade-rs/topgrade/issues/594#issuecomment-1782157367)
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()
}
run_vscode_compatible::<false>(ctx)
}
pub fn run_pipx_update(ctx: &ExecutionContext) -> Result<()> {
@@ -647,9 +673,12 @@ pub fn run_pip3_update(ctx: &ExecutionContext) -> Result<()> {
{
Ok(output) => {
let stdout = output.stdout.trim();
stdout
.parse::<bool>()
.expect("unexpected output that is not `true` or `false`")
stdout.parse::<bool>().wrap_err_with(|| {
output_changed_message!(
"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
//
@@ -976,8 +1005,39 @@ pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> {
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<()> {
let helix = require("helix").or(require("hx"))?;
let helix = require("helix").or(get_hx(ctx)?.helix())?;
print_separator("Helix");
@@ -1169,11 +1229,11 @@ pub fn run_poetry(ctx: &ExecutionContext) -> Result<()> {
// Parse the standard Unix shebang line: #!interpreter [optional-arg]
// Spaces and tabs on either side of interpreter are ignored.
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
lazy_static! {
static ref SHEBANG_REGEX: Regex = Regex::new(r"^#![ \t]*([^ \t\n]+)(?:[ \t]+([^\n]+)?)?").unwrap();
}
static SHEBANG_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^#![ \t]*([^ \t\n]+)(?:[ \t]+([^\n]+)?)?").unwrap());
let script = fs::read(poetry)?;
if let Some(c) = SHEBANG_REGEX.captures(&script) {
@@ -1192,10 +1252,8 @@ pub fn run_poetry(ctx: &ExecutionContext) -> Result<()> {
use std::str;
lazy_static! {
static ref SHEBANG_REGEX: Regex =
Regex::new(r#"^#![ \t]*(?:"([^"\n]+)"|([^" \t\n]+))(?:[ \t]+([^\n]+)?)?"#).unwrap();
}
static SHEBANG_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"^#![ \t]*(?:"([^"\n]+)"|([^" \t\n]+))(?:[ \t]+([^\n]+)?)?"#).unwrap());
let data = fs::read(poetry)?;
@@ -1277,24 +1335,117 @@ pub fn run_uv(ctx: &ExecutionContext) -> Result<()> {
let uv_exec = require("uv")?;
print_separator("uv");
// try uv self --help first - if it succeeds, we call uv self update
let result = ctx
// 1. Run `uv self update` if the `uv` binary is built with the `self-update`
// 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)
.args(["self", "--help"])
.output_checked();
.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"
if result.is_ok() {
ctx.run_type()
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()
.execute(&uv_exec)
.args(["self", "--help"])
.output_checked()
.is_ok();
if self_update_feature_enabled {
ctx.run_type()
.execute(&uv_exec)
.args(["self", "update"])
.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"])
.status_checked()?;
}
// `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()
.execute(&uv_exec)
.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
@@ -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 color_eyre::eyre;
use color_eyre::eyre::Result;
use color_eyre::eyre::{Context, Result};
use rust_i18n::t;
use walkdir::WalkDir;
@@ -12,7 +12,7 @@ use crate::error::TopgradeError;
use crate::execution_context::ExecutionContext;
use crate::utils::require_option;
use crate::utils::which;
use crate::{config, Step};
use crate::{config, output_changed_message, Step};
fn get_execution_path() -> OsString {
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"
let version_cmd_stdout = version_cmd_output.stdout;
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.
//

View File

@@ -65,7 +65,7 @@ impl Distribution {
Some("nobara") => Distribution::Nobara,
Some("void") => Distribution::Void,
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("gentoo") | Some("funtoo") => Distribution::Gentoo,
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()?;
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(())
@@ -1316,4 +1320,9 @@ mod tests {
fn test_bazzite() {
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::fs;
use std::os::unix::fs::MetadataExt;
use std::path::Component;
use std::path::PathBuf;
use std::process::Command;
use std::sync::LazyLock;
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;
#[cfg(target_os = "linux")]
@@ -238,7 +237,7 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
let gdbus = require("gdbus")?;
require_option(
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")
.args([
@@ -253,12 +252,12 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
])
.output_checked_utf8()?;
debug!("Checking for gnome extensions: {}", output);
debug!("Checking for GNOME extensions: {}", output);
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()
.execute(gdbus)
@@ -461,28 +460,31 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
"`nix --version` output"
);
lazy_static! {
static ref NIX_VERSION_REGEX: Regex =
Regex::new(r"^nix \([^)]*\) ([0-9.]+)").expect("Nix version regex always compiles");
}
static NIX_VERSION_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^nix \([^)]*\) ([0-9.]+)").expect("Nix version regex always compiles"));
if get_version_cmd_first_line_stdout.is_empty() {
return Err(eyre!("`nix --version` output was empty"));
}
let captures = NIX_VERSION_REGEX.captures(get_version_cmd_first_line_stdout);
let raw_version = match &captures {
None => {
return Err(eyre!(
"`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"
));
}
Some(captures) => &captures[1],
let captures = NIX_VERSION_REGEX
.captures(get_version_cmd_first_line_stdout)
.ok_or_else(|| eyre!(output_changed_message!("nix --version", "regex did not match")))?;
let raw_version = &captures[1];
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.
let corrected_raw_version = if raw_version.chars().filter(|&c| c == '.').count() == 1 {
&format!("{raw_version}.0")
} else {
raw_version
};
let version =
Version::parse(raw_version).wrap_err_with(|| format!("Unable to parse Nix version: {raw_version:?}"))?;
debug!("Corrected raw Nix version: {corrected_raw_version}");
let version = Version::parse(corrected_raw_version)
.wrap_err_with(|| output_changed_message!("nix --version", "Invalid version"))?;
debug!("Nix version: {:?}", version);
@@ -648,15 +650,19 @@ pub fn run_asdf(ctx: &ExecutionContext) -> Result<()> {
// v0.15.0-31e8c93
//
// ```
// ```
// $ asdf version
// v0.16.7
// ```
let version_stdout = version_output.stdout.trim();
// trim the starting 'v'
let mut remaining = version_stdout.trim_start_matches('v');
let idx = remaining
.find('-')
.expect("the output of `asdf version` changed, please file an issue to Topgrade");
// remove the hash part
remaining = &remaining[..idx];
let version = Version::parse(remaining).expect("should be a valid version");
// remove the hash part if present
if let Some(idx) = remaining.find('-') {
remaining = &remaining[..idx];
}
let version =
Version::parse(remaining).wrap_err_with(|| output_changed_message!("asdf version", "invalid version"))?;
if version < Version::new(0, 15, 0) {
ctx.run_type()
.execute(&asdf)

View File

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

View File

@@ -1,5 +1,3 @@
#[cfg(windows)]
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
@@ -18,22 +16,9 @@ pub struct 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 {
let path = which("pwsh").or_else(|| which("powershell")).filter(|_| !is_dumb());
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()
});
let profile = path.as_ref().and_then(Self::get_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)]
{
// 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 has_module(powershell: &Path, command: &str) -> bool {
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)
.args([
"-NoProfile",
"-Command",
&format!("Get-Module -ListAvailable {command}"),
&format!("Get-Module -ListAvailable {}", command),
])
.output_checked_utf8()
.map(|result| !result.stdout.is_empty())
.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::io::{self, Write};
use std::process::Command;
use std::sync::Mutex;
use std::sync::{LazyLock, 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 notify_rust::{Notification, Timeout};
use rust_i18n::t;
use tracing::{debug, error};
@@ -19,9 +18,7 @@ use which_crate::which;
use crate::command::CommandExt;
use crate::report::StepResult;
lazy_static! {
static ref TERMINAL: Mutex<Terminal> = Mutex::new(Terminal::new());
}
static TERMINAL: LazyLock<Mutex<Terminal>> = LazyLock::new(|| Mutex::new(Terminal::new()));
#[cfg(unix)]
pub fn shell() -> String {

View File

@@ -282,3 +282,15 @@ pub fn install_color_eyre() -> Result<()> {
.display_location_section(true)
.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,
)
};
}