Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef0a0d69bb | ||
|
|
4b3a3e74f8 | ||
|
|
2c4751c7b2 | ||
|
|
30941ed26d | ||
|
|
c7163b63db | ||
|
|
6e6b3dcbfe | ||
|
|
1d136a6635 | ||
|
|
0ee67d78ef | ||
|
|
7356b920d4 | ||
|
|
ce8a325c1f | ||
|
|
a2f57e4769 | ||
|
|
751f41bc5e | ||
|
|
fd406f0f82 | ||
|
|
801dddacd4 | ||
|
|
397a537eef | ||
|
|
0423c836eb | ||
|
|
3250337e70 | ||
|
|
9dcd7fffe2 | ||
|
|
30b727b138 | ||
|
|
b86d6981ab | ||
|
|
2bf6a2b100 | ||
|
|
3dc8d31d57 | ||
|
|
b308fb92c0 | ||
|
|
bc9746455e | ||
|
|
109a9c76e3 |
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -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 }}
|
||||
|
||||
105
.github/workflows/create_release_assets.yml
vendored
105
.github/workflows/create_release_assets.yml
vendored
@@ -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/*
|
||||
|
||||
@@ -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/*
|
||||
13
.github/workflows/release_to_aur.yml
vendored
13
.github/workflows/release_to_aur.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
51
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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" }]
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
56
src/main.rs
56
src/main.rs
@@ -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", || {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
11
src/steps/os/os_release/cachyos
Normal file
11
src/steps/os/os_release/cachyos
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
12
src/utils.rs
12
src/utils.rs
@@ -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,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user