Compare commits

..

104 Commits

Author SHA1 Message Date
SteveLauC
4488f3d5d3 chore: bump version to 16.0.3 (#1094) 2025-04-03 17:56:01 +08:00
Gideon
5a7958d20e Fix aqua CLI and JetBrains Aqua conflict (#1092) 2025-04-03 17:40:52 +08:00
Gideon
481a942b76 Fix pixi self-update running when pixi is not installed with the … (#1087)
* Fix `pixi self-update` running when `pixi` is not installed with the `self-update` feature

* Format
2025-04-03 17:38:38 +08:00
Red Wizard
a601d8429d added silent install option for winget (#1089)
* added silent install option for winget

* corrected formatting issues.

* Update src/steps/os/windows.rs

Remove code duplication.

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

---------

Co-authored-by: SteveLauC <stevelauc@outlook.com>
2025-03-30 21:11:04 +08:00
tdslot
a4a2d52a6d i18n(app.yml): new language lt (#1069)
* i18n(app.yml): new language lt

Lithuanian language.

* 🌐 i18n(app.yml): update translations for WSL error message

- resolve merge conflict in translation strings
- update spanish and french translations for clarity
- add lithuanian translation for WSL error message
- standardize zh_TW translation format

* 🌐 i18n(locales): add missing translations

- add zh_CN translation for "Topgrade not found in any WSL distribution"
- add lt translations for JetBrains Toolbox related messages
- add lt translations for operating system and updater error messages

* 🌐 i18n(locales): update spanish translation for WSL error message

- improve accuracy of spanish translation for "Could not find Topgrade in any WSL distribution"
- change from "Topgrade no se ha instalado dentro de WSL" to "No se pudo encontrar Topgrade en ninguna distribución WSL"
2025-03-30 14:50:37 +08:00
Nils
47fa3ba7de Add German translations to localization file (#1065)
* Add German translations to localization file

* refactor(localization): remove unused/duplicate translations from app.yml

* i18n(app.yml): add German translations for JetBrains Toolbox messages

* chore: Convert locales/app.yml from CRLF to LF line endings

* Update locales/app.yml

* fix: correct German translation for "Breaking changes"

---------

Co-authored-by: nistee <lo9s4b7qp@mozmail.com>
Co-authored-by: SteveLauC <stevelauc@outlook.com>
2025-03-26 08:59:37 +08:00
Gideon
e6bb6709b3 Update jetbrains-toolbox-updater (#1077) 2025-03-25 10:40:04 +08:00
Alexandre Veyrenc
c421742c4f fix:(emacs): fix issue #1075 (#1076) 2025-03-21 17:01:58 +08:00
ZeroDegress
1312cc8f6e i18n(app.yml): new language zh_CN (#1072) 2025-03-19 09:24:10 +08:00
Gideon
ed37763d30 Add JetBrains Toolbox via jetbrains-toolbox-updater (#1064)
* Add jetbrains-toolbox-updater

* Update jetbrains-toolbox-updater

* Update jetbrains-toolbox-updater

* Update jetbrains-toolbox-updater

* Localize prints

* Update jetbrains-toolbox-updater

* Format

* Add localization

* Fix translation
2025-03-18 11:19:37 +08:00
Justin
583bbf65e2 docs: fix --log-filter link in --help (#1073)
docs: fix EnvFilter link
2025-03-17 14:07:40 +08:00
Izzy Meyer
5770a5caa7 FIX: Allow for -beta OR -current detection and use the correct system upgrade command for the OpenBSD step (#1066)
* FIX: Allow for -beta OR -current detection and use the correct system upgrade command for the OpenBSD step

* FIX: Run fmt, clippy, and test
2025-03-13 09:02:45 +08:00
Gijs Key
722903fec3 Create Armv7l debian package (#1068)
* Create Armv7l debian package

* returned inadvertently removed comment.

---------

Co-authored-by: Gijs Keij <gijs.keij@bit-key.nl>
2025-03-12 11:20:19 +08:00
Xarblu
30f1c3c1b4 feat(sudo): add run0 as a sudo variant (#1067) 2025-03-12 09:07:37 +08:00
dependabot[bot]
ef7d146282 chore(deps): bump ring from 0.17.8 to 0.17.13 (#1062)
Bumps [ring](https://github.com/briansmith/ring) from 0.17.8 to 0.17.13.
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-09 10:47:33 +08:00
Andreas02-dev
20667a23d3 fix(sudo): fix sudo detection & pre-sudo for GSudo (#1058) 2025-03-06 08:51:40 +08:00
JaRoSchm
26f05827ae feat(pixi): add support for pixi global (#1037)
feat(pixi): add support for pixi global
2025-03-05 08:58:21 +08:00
Max Kapur
b1ffe7d553 Run conda clean after conda update if cleanup = true (#1047)
* Run `conda clean` after `conda upgrade` if `cleanup = true`

* Also run `mamba clean`
2025-03-04 09:15:02 +08:00
yggdr
368a060529 Add pipxu step (#1052) 2025-03-04 09:11:57 +08:00
Gideon
b40bffb1f2 Add "Cinnamon spices" step (#1055)
* Add "Cinnamon spices" step

* Format

* Move step to Linux
2025-03-04 08:57:12 +08:00
Andre Toerien
488ae149f7 fix(poetry): parse arg in script shebang line (#1028)
* fix(poetry): parse arg in script shebang line

* fix(poetry): improved shebang line parsing on windows
2025-02-25 20:00:53 +08:00
Tom van Dijk
fa3e4726b7 fix: uBlue OS should be detected as FedoraImmutable (#1043)
* refactor(parse_os_release): Don't rely on specific `ID`s for Fedora Immutable

Instead match `ID=fedora` or `ID_LIKE=fedora` and decide wether or not
the distro is immutable by `VARIANT`.

* feat: add aurora,bluefin,coreos support

The `os_release`s came from the following images:

- ghcr.io/ublue-os/aurora:93f0fd9f20b3
- ghcr.io/ublue-os/bluefin:5d37394a5d4b
- ghcr.io/ublue-os/ucore:63cd1200c344

* fix: bazzite should be detected as FedoraImmutable

* squash me: cargo fmt

---------

Co-authored-by: Steve Lau <stevelauc@outlook.com>
2025-02-17 09:41:34 +08:00
Ivan Andre Scheel
66a12cc8bf feat(vscode): updated extensions for a given profile (#1022)
* [feat] select user profile for vscode

* [feat] Update example config file

* [fix] Remove unneeded imports

* [feat] PR comments

* [fix] formatting
2025-02-11 12:51:52 +08:00
Alex Böhm
3e0c21e981 docs: fix typo in description (#1032) 2025-02-08 20:03:32 +08:00
Laura Demkowicz-Duffy
da270ae7d9 Add zigup step (#1030)
* feat: add zigup step

* feat(zigup): add various configuration options

* feat(zigup): add cleanup option

* feat(zigup): multiple version support and cleanup

* refactor(zigup): remove set_default and simplify execution

* fix(zigup): always pass path args to zigup for consistent behaviour

* refactor(zigup): use shellexpand to expand tildes
2025-02-08 14:25:10 +08:00
Laura Demkowicz-Duffy
4624f11ba5 Run juliaup gc if cleanup is enabled (#1031)
refactor(juliaup): run juliaup gc if cleanup is enabled
2025-02-08 14:15:25 +08:00
Dan Sully
224bb96a98 chore: update toolchain to 1.84.1. apply clippy fixes & rustfmt (#1026)
* chore: update to stable toolchain. apply clippy fixes & rustfmt

* Bump MSRV

* Try MSRV without the patch version

* fix: pin toolchain to MSRV

* trying again

* fix dead code warning

---------

Co-authored-by: Dan Sully <dsully@users.noreply.github.com>
2025-02-03 11:24:57 +08:00
SteveLauC
9a6fe8eea9 feat: support VSCodium (#788) 2025-01-09 10:35:45 +08:00
SteveLauC
aebc035ec0 fix: do not run asdf update if version >= 0.15.0 (#1008) 2024-12-20 13:31:29 +08:00
LILAY
bd348c328e Add Fedora Copr to Readme.md (#1005)
Update README.md
2024-12-12 10:01:10 +08:00
Samuel Grahn
c5f2d7b473 Detect Elan self update disabled (e.g. installed from distro repos) (#998)
* Add config option for Elan self-update

* Format & Config

* Revert "Format & Config"

This reverts commit 9eedecce8b312f8ad60563488c98cccfd50c0173.

* Revert "Add config option for Elan self-update"

This reverts commit 8c80c7a7d63ecd0936e0bd5cb07c2cbb1452c1fd.

* Allow self-update to fail when disabled

* Formatting

* Don't print in case of failed self-update

* Formatting

* Use the code suggested :)

* Follow the recommendations by Clippy
2024-12-12 09:19:03 +08:00
SteveLauC
dc9d8d55f2 fix: Executor::spawn()/output() should not use their _checked() variants (#1002) 2024-12-11 09:12:57 +08:00
Steve Lau
b172ba7f03 fix: Executor::spawn()/output() should not use their _checked() variants 2024-12-11 08:59:19 +08:00
SteveLauC
8227890808 refactor(uv step): check self update result if self-update feat is available (#1000)
refactor: check self update result if self-update feat is available
2024-12-10 20:55:32 +08:00
befanyt
a0963fe3fc fix: dont ignore rpm-ostree when bootc is found (#999) 2024-12-10 13:00:37 +08:00
SteveLauC
4df30c2587 chore: release v16.0.2 (#995) 2024-12-07 15:21:19 +08:00
Andre Toerien
305a5fbcae fix(poetry): skip if not installed with official script (#989)
* fix(poetry): skip if not installed with official script

* feat(poetry): add poetry_force_self_update config option

* docs: give this config a more detailed explanation

---------

Co-authored-by: Steve Lau <stevelauc@outlook.com>
2024-12-07 15:09:52 +08:00
Tulip Blossom
4f4dcbb643 feat: add bootc support to Fedora atomic distros
* feat(bootc): add Bootc support + docs

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

* docs(bootc): specify that itll supercede rpm-ostree if enabled :p

---------

Co-authored-by: Steve Lau <stevelauc@outlook.com>
2024-11-19 11:07:12 +08:00
Laura Demkowicz-Duffy
202897ba35 refactor: disable julia startup file for julia package update (#983)
* refactor(julia): disable julia startup file for julia package update

* feat(julia): add configuration option for julia startup file

* fix: deny unknown fields on JuliaConfig deserialisation

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

* doc(julia): clarify startup_file option purpose

---------

Co-authored-by: SteveLauC <stevelauc@outlook.com>
2024-11-19 09:17:51 +08:00
Youn Mélois
444689c899 feat: allow version specification for deno (#970)
* feat: allow version specification for deno

* fix: missing quotes for string in toml file

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

* fix: deno upgrade for different executable versions

* fix: tell apart the two cases for v1.x in SkipStep reason

* docs: add comments and documentation on version method for deno

* chore: add explanatory comment on stable channel that does nothing

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

---------

Co-authored-by: SteveLauC <stevelauc@outlook.com>
2024-10-29 18:09:47 +08:00
Gudsfile
98ec13f8db i18n(app.yml): new language fr (#969)
Apply suggestions from code review

Co-authored-by: SteveLauC <stevelauc@outlook.com>
2024-10-29 16:34:44 +08:00
Lucas Parzianello
39f76a3a71 uv step: checking self subcommand exits; fixes #942 (#971)
* uv step: checking self subcommand exits; fixes #942

* uv: fixing return behavior

---------

Co-authored-by: Lucas Parzianello <lucaspar@users.noreply.github.com>
2024-10-29 15:40:31 +08:00
Ricardo Torres
f181a795a6 refactor: flip order of mise upgrade and mise plugins update (#968)
flip order of mise plugins update and mise upgrade to attempt updating plugins first.
2024-10-28 09:59:22 +08:00
Andreas02-dev
ea2f3e07e9 feat(microsoft_store): Add Microsoft Store step for Windows (#963)
* feat(microsoft_store): Add Microsoft Store step for Windows

Add Microsoft Store Apps update step for Windows as Winget cannot update all Microsoft Store apps yet.

Closes #912

* style(translation): modify `zh_TW` translation
2024-10-23 08:15:46 +08:00
SteveLauC
8aad6eae0d refactor: add missing i18n for OpenBSD steps (#965) 2024-10-22 08:47:15 +08:00
SteveLauC
e86e5fe3e7 docs: document that we need to translate user-facing texts (#966) 2024-10-22 08:46:59 +08:00
λP.(P izzy)
2c2569c4f8 Improve OpenBSD -CURRENT detection and Dry-run feedback (#954)
* Improve OpenBSD -CURRENT detection and Dry-run feedback

This commit improves the -CURRENT detection by way of parsing `/etc/motd`. This change is more future-proof as when OpenBSD nears a stable release, `uname` will temporarily report like -STABLE.

This commit *also* adds feedback if -CURRENT is found to make debugging this feature easier with `--dry-run`, or, just a regular run as well.

* Make OpenBSD step less talky and improve verbiage.

This commit removes the command flag feedback. This commit also swaps the output "update", for "upgrade", making this step closer to other steps for consistency.
2024-10-18 08:26:27 +08:00
Rebecca Turner
9ffdc9649e Add support for Lix (Nix fork) (#952)
Add support for Lix

Lix is a fork of Nix 2.18 focused on maintainability and user
experience. It has a different format for the version, to distinguish it
from CppNix:

    $ nix --version
    nix (Lix, like Nix) 2.91.0

See: <https://lix.systems/>
2024-10-18 08:23:25 +08:00
Rikiub%
a5d4f2eec9 i18n (app.yml): Add Spanish localization (es) (#955)
* Update app.yml

* "es" localization added

* Grammar fixes

* Fix YAML syntax errors

* Fix YAML syntax errors

* Fix duplicated

* Fix duplicate

* Grammar fix

* Grammar fix

* Fix duplicate

* Improve grammar

* Update locales/app.yml

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

* Improve Grammar

* Improve Grammar

* Improve Grammar

* Improve Grammar

* Improve Grammar

---------

Co-authored-by: SteveLauC <stevelauc@outlook.com>
2024-10-17 08:04:49 +08:00
Nils
a5df40e01d Refactor config.rs and vagrant.rs files (#949)
* Refactor config.rs and vagrant.rs files

* Refactor config.rs and vagrant.rs files
2024-10-15 17:56:03 +08:00
SteveLauC
0573fc97c6 docs: update release procedure that SECURITY.md should be updated in major release (#946)
docs: update release procedure that SECURITY.md should be updated in major releases
2024-10-14 17:01:22 +08:00
Nils
1ae95f41a1 Update SECURITY.md (#945) 2024-10-14 16:37:15 +08:00
λP.(P izzy)
8a7af2e14d [FIXES #922] properly check for -CURRENT in OpenBSD steps and pass the correct flags to the respective commands (#923)
* [FIXES #922] properly check for -CURRENT in openbsd steps and pass the correct flags

* un-break ctx.config().dry_run() on OpenBSD Step
2024-10-14 08:29:51 +08:00
Nicolas Lorin
c36da89933 ci: add bin pkg to aur (#944) 2024-10-13 21:14:28 +08:00
SteveLauC
bbb84c2ee7 chore: release v16.0.1 (#940) 2024-10-11 10:46:50 +08:00
Tobias Micheler
36fd4b13c0 fix: pypi-release-action: downgrade upload-artifact and download-artifact to v3 (#938)
Since there are breaking changes between the upload-artifact and download-artifact versions v3 and v4, this workflow was broken, and no releases were uploaded to pypi.
A downgrade should make this work again
2024-10-10 07:26:29 +08:00
SteveLauC
49327000fc fix: tmux unknown cmd: attach-client (#937) 2024-10-08 21:33:53 +08:00
Oliver Tzeng
9c25cd7426 i18n(app.yml): new language zh_TW (#931)
* i18n(app.yml): new language zh_TW

translated topgrade to zh_TW

* Update app.yml

* Update app.yml

* fix(i18n): Fixed "self-upgrade" translation

thanks @SteveLauC
2024-10-08 19:11:11 +08:00
Alexandre Veyrenc
9767e4169c Fix typo in exit_status (#934)
fix: typo in exit_status
2024-10-08 08:52:36 +08:00
SteveLauC
0854f9c559 revert: PR 866 (#927) 2024-10-06 21:39:47 +08:00
SteveLauC
e4a068d808 chore: release v16.0.0 (#925)
* chore: release v16.0.0

* chore: it should be containers.runtime
2024-10-06 21:23:00 +08:00
SteveLauC
4c793b0df8 ci: correct checker binary name (#926)
* ci: correct checker binary name

* ci: correct checker binary name
2024-10-06 21:13:25 +08:00
SteveLauC
a021441135 fix: use single quotes for locale keys with new line char (#920) 2024-10-04 12:33:19 +08:00
Florian Nagel
29c555c394 Add i18n by using rust i18n (#807)
* feat: initial i18n setup

* style: fmt

* feat: i18n support for new steps

* fix: build on Linux

* fix: build on Linux

* refactor: rm unused translation keys

---------

Co-authored-by: Steve Lau <stevelauc@outlook.com>
2024-10-03 18:47:35 +08:00
SteveLauC
c33d396489 docs: --only is no longer experimental (#919) 2024-09-29 09:03:26 +08:00
⑆ Neveda ⑈
f6d2ba4dae feat(brew): Add greedy-auto-updates option to Brew (#914) 2024-09-26 18:29:11 +08:00
SteveLauC
a88574204d docs: don't call execute("bin_name_str") (#916) 2024-09-26 15:05:45 +08:00
Marcelo Duarte Trevisani
9435bc4b7d Add Pixi to topgrade (#915)
* Add Pixi

* make linter happy

* Fix args
2024-09-26 14:19:32 +08:00
tomaszn
27245cbd7b feat(brew): use sudo if Homebrew owned by another user on Linux (#904)
feat(brew): use sudo if Homebrew owned by another user

On Linux, run "brew update" with sudo if the Homebrew installation directory
is owned by a different user. This is typically the user who installed
Homebrew, but can also be a dedicated user account. This change ensures that
Homebrew updates can proceed smoothly even when its directory ownership does
not match the current user's UID. Proper sudo configuration is assumed for
this to work properly.
2024-09-22 21:00:52 +08:00
wetfloo
21751aa8a5 feat: tmux session attach mode (#901)
* feat: tmux session attach mode

* feat: example config update

* feat: move the comment down to be relevant

* feat: fix tmux not attaching from non-tmux env when using create_and_switch_client

* feat: make matching on tmux modes as described in suggestions

* feat: make tmux_session_attach_mode private

* feat: remove tmux mode cli option

* feat: wrap default value in quotation marks for tmux session mode

* feat: renames for tmux session management options

* feat: try to make tmux session mode description better
2024-09-17 21:06:39 +08:00
Marcel Coetzee
ad41948450 Remove check for whether conda config contains auto_activate_base (#905)
Signed-off-by: Marcel Coetzee <marcel@mooncoon.com>
2024-09-17 09:14:52 +08:00
SteveLauC
e32246f172 feat: clean scoop cache if cleanup is enabled (#909) 2024-09-16 15:27:01 +08:00
SteveLauC
25d3a816b4 fix: aura since v4.0.6 does not need sudo (#908)
* fix: aura since v4.0.6 does not need sudo

* fix: remove 'aura ' from version str
2024-09-16 13:01:05 +08:00
SteveLauC
05b1a565e0 chore: pin toolchain to MSRV(1.76) (#900)
* chore: pin toolchain to MSRV(1.76)

* chore: remove more toolchain action & update readme
2024-09-04 21:40:09 +08:00
Kreeblah
7b2623ea3c Add Debian-based system builds (#898)
* Add Debian-based system builds

* Address feedback

* Remove git as a listed dependency for Debian package
2024-09-04 11:50:39 +08:00
SteveLauC
983c5243ba fix: a panic introduced by improper unwrap() (#899)
fix: an panic introduced by improper unwrap()
2024-09-03 15:26:41 +08:00
Lucas Parzianello
1958fe1e5b Containers step: new runtime option to configuration (#896)
* pyenv: fixes #849

* feat: adds `uv` python manager step

* moved new uv step from unix to generic

* containers step: added container runtime option to config

* documented breaking change

---------

Co-authored-by: Lucas Parzianello <lucaspar@users.noreply.github.com>
2024-09-01 15:35:23 +08:00
SteveLauC
ca8558d9b4 feat: support step Bun on Windows (#893) 2024-08-26 22:21:17 +08:00
Lucas Parzianello
1b534800a9 Adds uv step (#890)
* pyenv: fixes #849

* feat: adds `uv` python manager step

* moved new uv step from unix to generic

---------

Co-authored-by: Lucas Parzianello <lucaspar@users.noreply.github.com>
2024-08-25 10:22:27 +08:00
Boris Smidt
e91c00c9c0 Add aqua tool installer cli (#889)
* Add aqua cli

* Move aqua cli to generic.rs

* Add a dry-run support to aqua

* style: format code

---------

Co-authored-by: Steve Lau <stevelauc@outlook.com>
2024-08-20 09:18:27 +08:00
Nils
a2375b4820 chore: update winget-releaser to use main branch (#888)
Update the winget-releaser action in the release_to_winget.yml workflow to use the main branch instead of v2. This ensures that the latest version of the action is being used for publishing.
2024-08-18 10:29:17 +08:00
Patrick J. Roddy
2e0c8e9e17 Fix RubyGems issues for mise regarding sudo (#887) 2024-08-18 10:28:22 +08:00
Nils
dc0ddcf9f0 Update README.md (#882)
* Update README.md

Added Chocolatey

* chore: fix broken Chocolatey link in README.md
2024-08-18 10:22:23 +08:00
Diogo Ribeiro
a1f3c86a39 feat: add volta packages (#883)
add print_info when no packages found

apply review feedback
2024-08-01 18:26:22 +08:00
Daniel Horecki
55f672eff7 Allow Nix unfree packages to be upgraded (#881)
Allow unfree packages to be upgraded

Fixes #611.

Co-authored-by: Daniel Horecki <morr@morr.pl>
2024-08-01 09:52:03 +08:00
Nils
8ece0346d8 chore: improve Windows Update step and add PSWindowsUpdate Module (#842)
* chore: improve Windows Update step and add PSWindowsUpdate Module

Refactor the `windows_update` function in `windows.rs` to improve the Windows Update step. Added a prompt for administrator privileges and updated the warning message. Also, added support for installing the PSWindowsUpdate Module as an alternative to using USOClient for Windows Update.

still see warning:
The installer will request to run as administrator, expect a prompt.
Start-Process : A parameter cannot be found that matches parameter name 'Command'.
At line:1 char:74
+ ... ath powershell -Verb runAs -ArgumentList  -NoProfile -Command Import- ...
+                                                          ~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Start-Process], ParameterBindingException
    + FullyQualifiedErrorId : NamedParameterNotFound,Microsoft.PowerShell.Commands.StartProcessCommand

VERBOSE: MSI-THIN-GF36 (6/30/2024 4:48:48 PM): Connecting to Microsoft Update server. Please wait...
VERBOSE: Found [0] Updates in pre search criteria

but as the verbose shows it works

* trying

* fix
2024-08-01 09:50:48 +08:00
Nicolas Lorin
b1fe1d201a ci: fix release_to_aur.yml (#879) 2024-07-29 16:13:33 +08:00
Nils
5010abdc22 Update SECURITY.md (#878) 2024-07-29 10:01:46 +08:00
SteveLauC
e4441d5021 refactor: fix Windows clippy (#880)
Refactor: fix Windows clippy
2024-07-29 09:01:04 +08:00
dashmoho
5af0c6a7e5 Fix nix upgrades (#874)
Nix version 2.21 changed how packages are upgraded.
Fixes #782.

Co-authored-by: Daniel Horecki <morr@morr.pl>
2024-07-24 07:37:22 +08:00
SteveLauC
b8da17106a feat: support ZVM (#777) 2024-07-23 07:26:08 +08:00
Tommaso Melacarne
fdf40dbf43 Fix nightly clippy warnings (#872) 2024-07-22 07:33:42 +08:00
Ryan Zoeller
f3b6530969 feat(os): support NI Linux Real-Time's opkg package manager (#870)
NI Linux Real-Time is a Yocto Linux-based distribution used with
NI's embedded and real-time controllers.

Related links:
- https://www.ni.com/en/shop/linux/introduction-to-ni-linux-real-time.html
- https://github.com/ni/nilrt
- https://github.com/ni/nilrt-docs
2024-07-21 09:09:36 +08:00
Lazerbeak12345
cbc5fc94f9 feat(linux.rs): Add support for Funtoo (#868)
* feat(linus.rs): Add support for Funtoo

* style(linux.rs): fix clippy reccomendations

* test(funtoo support): add funtoo test
2024-07-20 11:04:26 +08:00
SteveLauC
dceb697355 feat: don't run reboot with sudo on Linux with systemd (#866) 2024-07-20 10:13:14 +08:00
Lucas Parzianello
07118fa0d2 Fix pyenv: no such command 'update' (#860)
pyenv: fixes #849

Co-authored-by: Lucas Parzianello <lucaspar@users.noreply.github.com>
2024-07-11 07:52:09 +08:00
dependabot[bot]
16e6db0def chore(deps): bump zerovec-derive from 0.10.2 to 0.10.3 (#858)
Bumps [zerovec-derive](https://github.com/unicode-org/icu4x) from 0.10.2 to 0.10.3.
- [Release notes](https://github.com/unicode-org/icu4x/releases)
- [Changelog](https://github.com/unicode-org/icu4x/blob/main/CHANGELOG.md)
- [Commits](https://github.com/unicode-org/icu4x/commits/ind/zerovec-derive@0.10.3)

---
updated-dependencies:
- dependency-name: zerovec-derive
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-10 07:44:46 +08:00
dependabot[bot]
64d8f6d632 chore(deps): bump zerovec from 0.10.2 to 0.10.4 (#856)
Bumps [zerovec](https://github.com/unicode-org/icu4x) from 0.10.2 to 0.10.4.
- [Release notes](https://github.com/unicode-org/icu4x/releases)
- [Changelog](https://github.com/unicode-org/icu4x/blob/main/CHANGELOG.md)
- [Commits](https://github.com/unicode-org/icu4x/commits/ind/zerovec@0.10.4)

---
updated-dependencies:
- dependency-name: zerovec
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-09 07:59:04 +08:00
SteveLauC
180b5cba58 docs: document that maintenance continues (#855) 2024-07-08 09:26:57 +08:00
Steve Lau
bac416e907 docs: document that it is currently unmaintained 2024-07-07 16:23:23 +08:00
NAKASHIMA, Makoto
cb674a1572 fix: traverse symbolic links under $CONIG_DIR/topgrade.d (#852) (#853) 2024-07-07 13:47:53 +08:00
SteveLauC
960b14fa20 feat: support Poetry (#790) 2024-07-07 10:37:07 +08:00
tranzystorekk
a9f57d4205 Small clap adjustments (#846)
* style(cli): use new clap keywords

* fix(cli): use lowercase command name
2024-07-01 17:06:04 +08:00
SteveLauC
13330b6950 docs: update release procedure (#845) 2024-07-01 10:21:35 +08:00
58 changed files with 4092 additions and 966 deletions

View File

@@ -3,9 +3,10 @@
## Standards checklist
- [ ] The PR title is descriptive.
- [ ] The PR title is descriptive
- [ ] I have read `CONTRIBUTING.md`
- [ ] *Optional:* I have tested the code myself
- [ ] If this PR introduces new user-facing messages they are translated
## For new steps

22
.github/workflows/check_i18n.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
on:
pull_request:
push:
branches:
- main
name: Check i18n
jobs:
check_locale:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install checker
# Build it with the dev profile as this is faster and the checker still works
run: |
cargo install --git https://github.com/topgrade-rs/topgrade_i18n_locale_checker --profile dev
- name: Run the checker
run: topgrade_i18n_locale_checker --locale-file ./locales/app.yml --rust-src-to-check ./src

View File

@@ -7,7 +7,6 @@ on:
name: CI
env:
RUST_VER: 'stable'
CROSS_VER: '0.2.5'
CARGO_NET_RETRY: 3
@@ -19,12 +18,6 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: '${{ env.RUST_VER }}'
components: rustfmt
- name: Run cargo fmt
env:
TERM: xterm-256color
@@ -73,12 +66,6 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: '${{ env.RUST_VER }}'
components: clippy
- name: Setup Rust Cache
uses: Swatinem/rust-cache@v2
with:

View File

@@ -18,10 +18,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Install cargo-deb
run: cargo install cargo-deb
if: ${{ matrix.platform == 'ubuntu-latest' }}
shell: bash
- name: Check format
run: cargo fmt --all -- --check
@@ -41,7 +41,7 @@ jobs:
- name: Rename Release (Unix)
run: |
cargo install default-target
mkdir assets
mkdir -p assets
FILENAME=topgrade-${{github.event.release.tag_name}}-$(default-target)
mv target/release/topgrade assets
cd assets
@@ -51,6 +51,24 @@ jobs:
if: ${{ matrix.platform != 'windows-latest' }}
shell: bash
- name: Build Debian-based system binary and create package
# 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/release
cargo build --release
cargo deb --no-build --no-strip
if: ${{ matrix.platform == 'ubuntu-latest' }}
shell: bash
- name: Move Debian-based system package
run: |
mkdir -p assets
mv target/debian/*.deb assets
if: ${{ matrix.platform == 'ubuntu-latest' }}
shell: bash
- name: Rename Release (Windows)
run: |
cargo install default-target

View File

@@ -6,28 +6,39 @@ on:
# types:
# - completed
release:
types: [ created ]
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",
]
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: setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- 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 }}
@@ -54,7 +65,7 @@ jobs:
- name: Rename Release
run: |
mkdir assets
mkdir -p assets
FILENAME=topgrade-${{github.event.release.tag_name}}-${{matrix.target}}
mv target/${{matrix.target}}/release/topgrade assets
cd assets
@@ -62,6 +73,24 @@ jobs:
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:

View File

@@ -13,10 +13,19 @@ jobs:
aur-publish:
runs-on: ubuntu-latest
steps:
- name: Publish AUR package
uses: ATiltedTree/create-aur-release@v1
- name: Publish source AUR package
uses: aksh1618/update-aur-package@v1.0.5
with:
tag_version_prefix: v
package_name: topgrade
commit_username: "Thomas Schönauer"
commit_email: t.schoenauer@hgs-wt.at
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
- name: Publish binary AUR package
uses: aksh1618/update-aur-package@v1.0.5
with:
tag_version_prefix: v
package_name: topgrade-bin
commit_username: "Thomas Schönauer"
commit_email: t.schoenauer@hgs-wt.at
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}

View File

@@ -23,7 +23,7 @@ jobs:
sccache: 'true'
manylinux: auto
- name: Upload wheels
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -42,7 +42,7 @@ jobs:
args: --release --out dist
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -61,7 +61,7 @@ jobs:
args: --release --out dist
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -76,7 +76,7 @@ jobs:
command: sdist
args: --out dist
- name: Upload sdist
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
@@ -87,7 +87,7 @@ jobs:
if: "startsWith(github.ref, 'refs/tags/')"
needs: [linux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v3
with:
name: wheels
- name: Publish to PyPI

View File

@@ -6,7 +6,7 @@ jobs:
publish:
runs-on: windows-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
- uses: vedantmgoyal2009/winget-releaser@main
with:
identifier: topgrade-rs.topgrade
max-versions-to-keep: 5 # keep only latest 5 versions

View File

@@ -1,9 +0,0 @@
# Git: Pull Repos
1. The output of "Pulling <repository path>" has been moved behind the
--verbose flag / [misc] configuration block.
# Configuration
1. The `enable_winget` configuration entry in the `windows` section has been
removed because it will not cause any issues and will be enabled by default.

View File

@@ -48,7 +48,7 @@ To add a new `step` to `topgrade`:
// Invoke the new step to get things updated!
ctx.run_type()
.execute("xxx")
.execute(xxx)
.arg(/* args required by this step */)
.status_checked()
}
@@ -129,6 +129,24 @@ $ cargo test
Don't worry about other platforms, we have most of them covered in our CI.
## 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
arguments, e.g., "hello <NAME>", please follow this convention:
```yml
"hello {name}": # key
en: "hello %{name}" # translation
```
Arguments in the key should be in format `{argument_name}`, and they will have
a preceeding `%` when used in translations.
[app_yml]: https://github.com/topgrade-rs/topgrade/blob/main/locales/app.yml
## Some tips
1. Locale

541
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,8 @@ categories = ["os"]
keywords = ["upgrade", "update"]
license = "GPL-3.0"
repository = "https://github.com/topgrade-rs/topgrade"
version = "15.0.0"
rust-version = "1.84.1"
version = "16.0.3"
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"
@@ -51,6 +52,9 @@ merge = "~0.1"
regex-split = "~0.1"
notify-rust = "~4.11"
wildmatch = "2.3.0"
rust-i18n = "3.0.1"
sys-locale = "0.3.1"
jetbrains-toolbox-updater = "1.1.0"
[package.metadata.generate-rpm]
assets = [{ source = "target/release/topgrade", dest = "/usr/bin/topgrade" }]
@@ -59,7 +63,15 @@ assets = [{ source = "target/release/topgrade", dest = "/usr/bin/topgrade" }]
git = "*"
[package.metadata.deb]
depends = "$auto,git"
name = "topgrade"
maintainer = "Chris Gelatt <kreeblah@gmail.com>"
copyright = "2024, Topgrade Team"
license-file = ["LICENSE", "0"]
depends = "$auto"
extended-description = "Keeping your system up to date usually involves invoking multiple package managers. This results in big, non-portable shell one-liners saved in your shell. To remedy this, Topgrade detects which tools you use and runs the appropriate commands to update them."
section = "utils"
priority = "optional"
default-features = true
[target.'cfg(unix)'.dependencies]
nix = { version = "~0.29", features = ["hostname", "signal", "user"] }

View File

@@ -29,18 +29,17 @@ To remedy this, **Topgrade** detects which tools you use and runs the appropriat
- NixOS: [Nixpkgs](https://search.nixos.org/packages?show=topgrade)
- Void Linux: [XBPS](https://voidlinux.org/packages/?arch=x86_64&q=topgrade)
- macOS: [Homebrew](https://formulae.brew.sh/formula/topgrade) or [MacPorts](https://ports.macports.org/port/topgrade/)
- Windows: [Scoop][scoop] or [Winget][winget]
- Windows: [Chocolatey][choco], [Scoop][scoop] or [Winget][winget]
- PyPi: [pip](https://pypi.org/project/topgrade/)
- Fedora: [Copr](https://copr.fedorainfracloud.org/coprs/lilay/topgrade/)
[choco]: https://community.chocolatey.org/packages/topgrade
[scoop]: https://scoop.sh/#/apps?q=topgrade
[winget]: https://winstall.app/apps/topgrade-rs.topgrade
Other systems users can either use `cargo install` or the compiled binaries from the release page.
The compiled binaries contain a self-upgrading feature.
> Currently, Topgrade requires Rust 1.65 or above. In general, Topgrade tracks
> the latest stable toolchain.
## Usage
Just run `topgrade`.

View File

@@ -9,12 +9,16 @@
> If there are breaking changes, the major version number should be increased.
2. Overwrite [`BREAKINGCHANGES`][breaking_changes] with
2. If the major versioin number gets bumped, update [SECURITY.md][SECURITY_file_link].
[SECURITY_file_link]: https://github.com/topgrade-rs/topgrade/blob/main/SECURITY.md
3. Overwrite [`BREAKINGCHANGES`][breaking_changes] with
[`BREAKINGCHANGES_dev`][breaking_changes_dev], and create a new dev file:
```sh'
$ cd topgrade
$ cp BREAKINGCHANGES_dev.md BREAKINGCHANGES.md
$ mv BREAKINGCHANGES_dev.md BREAKINGCHANGES.md
$ touch BREAKINGCHANGES_dev.md
```

View File

@@ -6,6 +6,6 @@ We only support the latest major version and each subversion.
| Version | Supported |
| -------- | ------------------ |
| 10.0.x | :white_check_mark: |
| < 10.0 | :x: |
| 16.0.x | :white_check_mark: |
| < 16.0 | :x: |

View File

@@ -47,6 +47,12 @@
# Run inside tmux (default: false)
# run_in_tmux = true
# Changes the way topgrade interacts with
# the tmux session, creating the session
# and only attaching to it if not inside tmux
# (default: "attach_if_not_in_session", allowed values: "attach_if_not_in_session", "attach_always")
# tmux_session_mode = "attach_if_not_in_session"
# Cleanup temporary or old files (default: false)
# cleanup = true
@@ -97,6 +103,13 @@
# enable_pipupgrade = true ###disabled by default
# pipupgrade_arguments = "-y -u --pip-path pip" ###disabled by default
# For the poetry step, by default, Topgrade skips its update if poetry is not
# installed with the official script. This configuration entry forces Topgrade
# to run the update in this case.
#
# (default: false)
# poetry_force_self_update = true
[composer]
# self_update = true
@@ -114,6 +127,12 @@
# both of them, they won't clash with each other.
# greedy_latest = true
# For the BrewCask step
# If `Repo Cask Upgrade` does not exist, then use the `--greedy_auto_updates` option.
# NOTE: the above entry `greedy_cask` contains this entry, though you can enable
# both of them, they won't clash with each other.
# greedy_auto_updates = true
# For the BrewFormula step
# Execute `brew autoremove` after the step.
# autoremove = true
@@ -160,6 +179,11 @@
# rpm_ostree = false
# For Fedora/CentOS/RHEL Atomic variants, if `bootc` is available and this configuration entry is set to true, use
# it to do the update - Will also supercede rpm-ostree if enabled
# (default: false)
# bootc = false
# nix_arguments = "--flake"
# nix_env_arguments = "--prebuilt-only"
@@ -195,6 +219,10 @@
# wsl_update_use_web_download = true
# The default for winget_install_silently is true,
# this example turns off silent install.
# winget_install_silently = false
# Causes Topgrade to rename itself during the run to allow package managers
# to upgrade it. Use this only if you installed Topgrade by using a package
# manager such as Scoop or Cargo
@@ -211,6 +239,11 @@
# use_sudo = true
[deno]
# Upgrade deno executable to the given version.
# version = "stable"
[vim]
# For `vim-plug`, execute `PlugUpdate!` instead of `PlugUpdate`
# force_plug_update = true
@@ -244,10 +277,51 @@
[containers]
# Specify the containers to ignore while updating (Wildcard supported)
# ignored_containers = ["ghcr.io/rancher-sandbox/rancher-desktop/rdx-proxy:latest", "docker.io*"]
# Specify the runtime to use for containers (default: "docker", allowed values: "docker", "podman")
# runtime = "podman"
[lensfun]
# If disabled, Topgrade invokes `lensfunupdatedata` without root priviledge,
# If disabled, Topgrade invokes `lensfunupdatedata` without root priviledge,
# then the update will be only available to you. Otherwise, `sudo` is required,
# and the update will be installed system-wide, i.e., available to all users.
# (default: false)
# use_sudo = false
[julia]
# If disabled, Topgrade invokes julia with the --startup-file=no CLI option.
#
# This may be desirable to avoid loading outdated packages with "using" directives
# in the startup file, which might cause the update run to fail.
# (default: true)
# startup_file = true
[zigup]
# Version strings passed to zigup.
# These may be pinned versions such as "0.13.0" or branches such as "master".
# Each one will be updated in its own zigup invocation.
# (default: ["master"])
# target_versions = ["master", "0.13.0"]
# Specifies the directory that the zig files will be installed to.
# If defined, passed with the --install-dir command line flag.
# If not defined, zigup will use its default behaviour.
# (default: not defined)
# install_dir = "~/.zig"
# Specifies the path of the symlink which will be set to point at the default compiler version.
# If defined, passed with the --path-link command line flag.
# If not defined, zigup will use its default behaviour.
# This is not meaningful if set_default is not enabled.
# (default: not defined)
# path_link = "~/.bin/zig"
# If enabled, run `zigup clean` after updating all versions.
# If enabled, each updated version above will be marked with `zigup keep`.
# (default: false)
# cleanup = false
[vscode]
# If this is set and is a non-empty string, it specifies the profile the
# extensions should be updated for.
# (default: this won't be set by default)
# profile = ""

1290
locales/app.yml Normal file

File diff suppressed because it is too large Load Diff

2
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "1.84.1"

View File

@@ -11,6 +11,7 @@ use crate::WINDOWS_DIRS;
use crate::XDG_DIRS;
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
use rust_i18n::t;
use std::{
env::var,
fs::{read_to_string, OpenOptions},
@@ -45,7 +46,7 @@ impl FromStr for Version {
// They cannot be all 0s
assert!(
!(major == 0 && minor == 0 && patch == 0),
"Version numbers can not be all 0s"
"Version numbers cannot be all 0s"
);
Ok(Self {
@@ -118,12 +119,15 @@ pub(crate) fn first_run_of_major_release() -> Result<bool> {
/// Print breaking changes to the user.
pub(crate) fn print_breaking_changes() {
let header = format!("Topgrade {VERSION_STR} Breaking Changes");
let header = format!(
"{}",
t!("Topgrade {version_str} Breaking Changes", version_str = VERSION_STR)
);
print_separator(header);
let contents = if BREAKINGCHANGES.is_empty() {
"No Breaking changes"
t!("No Breaking changes").to_string()
} else {
BREAKINGCHANGES
BREAKINGCHANGES.to_string()
};
println!("{contents}\n");
}
@@ -159,7 +163,7 @@ mod test {
}
#[test]
#[should_panic(expected = "Version numbers can not be all 0s")]
#[should_panic(expected = "Version numbers cannot be all 0s")]
fn invalid_version() {
let all_0 = "0.0.0";
all_0.parse::<Version>().unwrap();

View File

@@ -45,13 +45,13 @@ impl TryFrom<&Output> for Utf8Output {
type Error = eyre::Error;
fn try_from(Output { status, stdout, stderr }: &Output) -> Result<Self, Self::Error> {
let stdout = String::from_utf8(stdout.to_vec()).map_err(|err| {
let stdout = String::from_utf8(stdout.clone()).map_err(|err| {
eyre!(
"Stdout contained invalid UTF-8: {}",
String::from_utf8_lossy(err.as_bytes())
)
})?;
let stderr = String::from_utf8(stderr.to_vec()).map_err(|err| {
let stderr = String::from_utf8(stderr.clone()).map_err(|err| {
eyre!(
"Stderr contained invalid UTF-8: {}",
String::from_utf8_lossy(err.as_bytes())
@@ -149,6 +149,7 @@ pub trait CommandExt {
/// Like [`Command::spawn`], but gives a nice error message if the command fails to
/// execute.
#[track_caller]
#[allow(dead_code)]
fn spawn_checked(&mut self) -> eyre::Result<Self::Child>;
}

View File

@@ -5,7 +5,7 @@ use std::fs::{write, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
use std::{env, fmt, fs};
use clap::{Parser, ValueEnum};
use clap_complete::Shell;
@@ -15,6 +15,7 @@ use etcetera::base_strategy::BaseStrategy;
use merge::Merge;
use regex::Regex;
use regex_split::RegexSplit;
use rust_i18n::t;
use serde::Deserialize;
use strum::{EnumIter, EnumString, IntoEnumIterator, VariantNames};
use which_crate::which;
@@ -25,6 +26,7 @@ use crate::sudo::SudoKind;
use crate::utils::string_prepend_str;
use tracing::{debug, error};
// TODO: Add i18n to this. Tracking issue: https://github.com/topgrade-rs/topgrade/issues/859
pub static EXAMPLE_CONFIG: &str = include_str!("../config.example.toml");
/// Topgrade's default log level.
@@ -53,6 +55,7 @@ pub enum Step {
AppMan,
Asdf,
Atom,
Aqua,
Audit,
AutoCpufreq,
Bin,
@@ -66,6 +69,7 @@ pub enum Step {
Chezmoi,
Chocolatey,
Choosenim,
CinnamonSpices,
ClamAvDb,
Composer,
Conda,
@@ -94,6 +98,7 @@ pub enum Step {
Haxelib,
Helm,
HomeManager,
JetBrainsToolbox,
Jetpack,
Julia,
Juliaup,
@@ -108,6 +113,7 @@ pub enum Step {
Mas,
Maza,
Micro,
MicrosoftStore,
Mise,
Myrepos,
Nix,
@@ -121,10 +127,13 @@ pub enum Step {
PipReviewLocal,
Pipupgrade,
Pipx,
Pipxu,
Pixi,
Pkg,
Pkgin,
PlatformioCore,
Pnpm,
Poetry,
Powershell,
Protonup,
Pyenv,
@@ -151,10 +160,13 @@ pub enum Step {
Tlmgr,
Tmux,
Toolbx,
Uv,
Vagrant,
Vcpkg,
Vim,
VoltaPackages,
Vscode,
Vscodium,
Waydroid,
Winget,
Wsl,
@@ -162,6 +174,8 @@ pub enum Step {
Xcodes,
Yadm,
Yarn,
Zigup,
Zvm,
}
#[derive(Deserialize, Default, Debug, Merge)]
@@ -176,6 +190,7 @@ pub struct Include {
pub struct Containers {
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
ignored_containers: Option<Vec<String>>,
runtime: Option<ContainerRuntime>,
}
#[derive(Deserialize, Default, Debug, Merge)]
@@ -210,6 +225,7 @@ pub struct Windows {
open_remotes_in_new_terminal: Option<bool>,
wsl_update_pre_release: Option<bool>,
wsl_update_use_web_download: Option<bool>,
winget_silent_install: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
@@ -219,6 +235,7 @@ pub struct Python {
enable_pip_review_local: Option<bool>,
enable_pipupgrade: Option<bool>,
pipupgrade_arguments: Option<String>,
poetry_force_self_update: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
@@ -245,6 +262,13 @@ pub struct NPM {
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
pub struct Deno {
version: Option<String>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
#[allow(clippy::upper_case_acronyms)]
@@ -264,6 +288,7 @@ pub struct Flatpak {
pub struct Brew {
greedy_cask: Option<bool>,
greedy_latest: Option<bool>,
greedy_auto_updates: Option<bool>,
autoremove: Option<bool>,
fetch_head: Option<bool>,
}
@@ -282,6 +307,22 @@ pub enum ArchPackageManager {
Yay,
}
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContainerRuntime {
Docker,
Podman,
}
impl fmt::Display for ContainerRuntime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContainerRuntime::Docker => write!(f, "docker"),
ContainerRuntime::Podman => write!(f, "podman"),
}
}
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Linux {
@@ -324,6 +365,7 @@ pub struct Linux {
redhat_distro_sync: Option<bool>,
suse_dup: Option<bool>,
rpm_ostree: Option<bool>,
bootc: Option<bool>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
emerge_sync_flags: Option<String>,
@@ -381,6 +423,8 @@ pub struct Misc {
run_in_tmux: Option<bool>,
tmux_session_mode: Option<TmuxSessionMode>,
cleanup: Option<bool>,
notify_each_step: Option<bool>,
@@ -397,12 +441,46 @@ pub struct Misc {
log_filters: Option<Vec<String>>,
}
#[derive(Clone, Copy, Debug, Deserialize, ValueEnum)]
#[clap(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum TmuxSessionMode {
AttachIfNotInSession,
AttachAlways,
}
pub struct TmuxConfig {
pub args: Vec<String>,
pub session_mode: TmuxSessionMode,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Lensfun {
use_sudo: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct JuliaConfig {
startup_file: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Zigup {
target_versions: Option<Vec<String>>,
install_dir: Option<String>,
path_link: Option<String>,
cleanup: Option<bool>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct VscodeConfig {
profile: Option<String>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
/// Configuration file
@@ -449,6 +527,9 @@ pub struct ConfigFile {
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
yarn: Option<Yarn>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
deno: Option<Deno>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
vim: Option<Vim>,
@@ -466,6 +547,15 @@ pub struct ConfigFile {
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
lensfun: Option<Lensfun>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
julia: Option<JuliaConfig>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
zigup: Option<Zigup>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
vscode: Option<VscodeConfig>,
}
fn config_directory() -> PathBuf {
@@ -497,7 +587,7 @@ impl ConfigFile {
];
// Search for the main config file
for path in possible_config_paths.iter() {
for path in &possible_config_paths {
if path.exists() {
debug!("Configuration at {}", path.display());
res.0.clone_from(path);
@@ -532,7 +622,9 @@ impl ConfigFile {
if dir_to_search.exists() {
for entry in fs::read_dir(dir_to_search)? {
let entry = entry?;
if entry.file_type()?.is_file() {
// Use `Path::is_file()` here to traverse symbolic links.
// `DirEntry::file_type()` and `FileType::is_file()` will not traverse symbolic links.
if entry.path().is_file() {
debug!(
"Found additional (directory) configuration file at {}",
entry.path().display()
@@ -565,13 +657,11 @@ impl ConfigFile {
to read the include directory before returning the main config path
*/
for include in dir_include {
let include_contents = fs::read_to_string(&include).map_err(|e| {
let include_contents = fs::read_to_string(&include).inspect_err(|_| {
error!("Unable to read {}", include.display());
e
})?;
let include_contents_parsed = toml::from_str(include_contents.as_str()).map_err(|e| {
let include_contents_parsed = toml::from_str(include_contents.as_str()).inspect_err(|_| {
error!("Failed to deserialize {}", include.display());
e
})?;
result.merge(include_contents_parsed);
@@ -586,9 +676,8 @@ impl ConfigFile {
return Ok(result);
}
let mut contents_non_split = fs::read_to_string(&config_path).map_err(|e| {
let mut contents_non_split = fs::read_to_string(&config_path).inspect_err(|_| {
error!("Unable to read {}", config_path.display());
e
})?;
Self::ensure_misc_is_present(&mut contents_non_split, &config_path);
@@ -599,9 +688,8 @@ impl ConfigFile {
let contents_split = regex_match_include.split_inclusive_left(contents_non_split.as_str());
for contents in contents_split {
let config_file_include_only: ConfigFileIncludeOnly = toml::from_str(contents).map_err(|e| {
let config_file_include_only: ConfigFileIncludeOnly = toml::from_str(contents).inspect_err(|_| {
error!("Failed to deserialize an include section of {}", config_path.display());
e
})?;
if let Some(includes) = &config_file_include_only.include {
@@ -613,14 +701,14 @@ impl ConfigFile {
let include_contents = match fs::read_to_string(&include_path) {
Ok(c) => c,
Err(e) => {
error!("Unable to read {}: {}", include_path.display(), e);
error!("Unable to read {}: {e}", include_path.display(),);
continue;
}
};
match toml::from_str::<Self>(&include_contents) {
Ok(include_parsed) => result.merge(include_parsed),
Err(e) => {
error!("Failed to deserialize {}: {}", include_path.display(), e);
error!("Failed to deserialize {}: {e}", include_path.display(),);
continue;
}
};
@@ -630,14 +718,17 @@ impl ConfigFile {
match toml::from_str::<Self>(contents) {
Ok(contents) => result.merge(contents),
Err(e) => error!("Failed to deserialize {}: {}", config_path.display(), e),
Err(e) => error!("Failed to deserialize {}: {e}", config_path.display(),),
}
}
if let Some(paths) = result.git.as_mut().and_then(|git| git.repos.as_mut()) {
for path in paths.iter_mut() {
let expanded = shellexpand::tilde::<&str>(&path.as_ref()).into_owned();
debug!("Path {} expanded to {}", path, expanded);
debug!(
"{}",
t!("Path {path} expanded to {expanded}", path = path, expanded = expanded)
);
*path = expanded;
}
}
@@ -675,63 +766,65 @@ impl ConfigFile {
}
// Command line arguments
// TODO: i18n of clap currently not easily possible. Waiting for https://github.com/clap-rs/clap/issues/380
// Tracking issue for i18n: https://github.com/topgrade-rs/topgrade/issues/859
#[derive(Parser, Debug)]
#[clap(name = "Topgrade", version)]
#[command(name = "topgrade", version)]
pub struct CommandLineArgs {
/// Edit the configuration file
#[clap(long = "edit-config")]
#[arg(long = "edit-config")]
edit_config: bool,
/// Show config reference
#[clap(long = "config-reference")]
#[arg(long = "config-reference")]
show_config_reference: bool,
/// Run inside tmux
#[clap(short = 't', long = "tmux")]
#[arg(short = 't', long = "tmux")]
run_in_tmux: bool,
/// Cleanup temporary or old files
#[clap(short = 'c', long = "cleanup")]
#[arg(short = 'c', long = "cleanup")]
cleanup: bool,
/// Print what would be done
#[clap(short = 'n', long = "dry-run")]
#[arg(short = 'n', long = "dry-run")]
dry_run: bool,
/// Do not ask to retry failed steps
#[clap(long = "no-retry")]
#[arg(long = "no-retry")]
no_retry: bool,
/// Do not perform upgrades for the given steps
#[clap(long = "disable", value_name = "STEP", value_enum, num_args = 1..)]
#[arg(long = "disable", value_name = "STEP", value_enum, num_args = 1..)]
disable: Vec<Step>,
/// Perform only the specified steps (experimental)
#[clap(long = "only", value_name = "STEP", value_enum, num_args = 1..)]
/// Perform only the specified steps
#[arg(long = "only", value_name = "STEP", value_enum, num_args = 1..)]
only: Vec<Step>,
/// Run only specific custom commands
#[clap(long = "custom-commands", value_name = "NAME", num_args = 1..)]
#[arg(long = "custom-commands", value_name = "NAME", num_args = 1..)]
custom_commands: Vec<String>,
/// Set environment variables
#[clap(long = "env", value_name = "NAME=VALUE", num_args = 1..)]
#[arg(long = "env", value_name = "NAME=VALUE", num_args = 1..)]
env: Vec<String>,
/// Output debug logs. Alias for `--log-filter debug`.
#[clap(short = 'v', long = "verbose")]
#[arg(short = 'v', long = "verbose")]
pub verbose: bool,
/// Prompt for a key before exiting
#[clap(short = 'k', long = "keep")]
#[arg(short = 'k', long = "keep")]
keep_at_end: bool,
/// Skip sending a notification at the end of a run
#[clap(long = "skip-notify")]
#[arg(long = "skip-notify")]
skip_notify: bool,
/// Say yes to package manager's prompt
#[clap(
#[arg(
short = 'y',
long = "yes",
value_name = "STEP",
@@ -741,37 +834,37 @@ pub struct CommandLineArgs {
yes: Option<Vec<Step>>,
/// Don't pull the predefined git repos
#[clap(long = "disable-predefined-git-repos")]
#[arg(long = "disable-predefined-git-repos")]
disable_predefined_git_repos: bool,
/// Alternative configuration file
#[clap(long = "config", value_name = "PATH")]
#[arg(long = "config", value_name = "PATH")]
config: Option<PathBuf>,
/// A regular expression for restricting remote host execution
#[clap(long = "remote-host-limit", value_name = "REGEX")]
#[arg(long = "remote-host-limit", value_name = "REGEX")]
remote_host_limit: Option<Regex>,
/// Show the reason for skipped steps
#[clap(long = "show-skipped")]
#[arg(long = "show-skipped")]
show_skipped: bool,
/// Tracing filter directives.
///
/// See: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/struct.EnvFilter.html
#[clap(long, default_value = DEFAULT_LOG_LEVEL)]
/// See: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives
#[arg(long, default_value = DEFAULT_LOG_LEVEL)]
pub log_filter: String,
/// Print completion script for the given shell and exit
#[clap(long, value_enum, hide = true)]
#[arg(long, value_enum, hide = true)]
pub gen_completion: Option<Shell>,
/// Print roff manpage and exit
#[clap(long, hide = true)]
#[arg(long, hide = true)]
pub gen_manpage: bool,
/// Don't update Topgrade
#[clap(long = "no-self-update")]
#[arg(long = "no-self-update")]
pub no_self_update: bool,
}
@@ -832,7 +925,7 @@ impl Config {
ConfigFile::read(opt.config.clone()).unwrap_or_else(|e| {
// Inform the user about errors when loading the configuration,
// but fallback to the default config to at least attempt to do something
error!("failed to load configuration: {}", e);
error!("failed to load configuration: {e}");
ConfigFile::default()
})
} else {
@@ -882,6 +975,15 @@ impl Config {
.and_then(|containers| containers.ignored_containers.as_ref())
}
/// The preferred runtime for container updates (podman / docker).
pub fn containers_runtime(&self) -> ContainerRuntime {
self.config_file
.containers
.as_ref()
.and_then(|containers| containers.runtime)
.unwrap_or(ContainerRuntime::Docker) // defaults to a popular choice
}
/// Tell whether the specified step should run.
///
/// If the step appears either in the `--disable` command line argument
@@ -938,6 +1040,15 @@ impl Config {
.unwrap_or(false)
}
/// The preferred way to run the new tmux session.
fn tmux_session_mode(&self) -> TmuxSessionMode {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.tmux_session_mode)
.unwrap_or(TmuxSessionMode::AttachIfNotInSession)
}
/// Tell whether we should perform cleanup steps.
pub fn cleanup(&self) -> bool {
self.opt.cleanup
@@ -995,8 +1106,16 @@ impl Config {
self.config_file.git.as_ref().and_then(|git| git.arguments.as_ref())
}
pub fn tmux_config(&self) -> Result<TmuxConfig> {
let args = self.tmux_arguments()?;
Ok(TmuxConfig {
args,
session_mode: self.tmux_session_mode(),
})
}
/// Extra Tmux arguments
pub fn tmux_arguments(&self) -> Result<Vec<String>> {
fn tmux_arguments(&self) -> Result<Vec<String>> {
let args = &self
.config_file
.misc
@@ -1117,6 +1236,15 @@ impl Config {
.unwrap_or(false)
}
/// Whether Brew cask should be auto_updates
pub fn brew_greedy_auto_updates(&self) -> bool {
self.config_file
.brew
.as_ref()
.and_then(|c| c.greedy_auto_updates)
.unwrap_or(false)
}
/// Whether Brew should autoremove
pub fn brew_autoremove(&self) -> bool {
self.config_file
@@ -1358,14 +1486,22 @@ impl Config {
.unwrap_or(false)
}
/// Use bootc in *when bootc is detected* (default: false)
pub fn bootc(&self) -> bool {
self.config_file
.linux
.as_ref()
.and_then(|linux| linux.bootc)
.unwrap_or(false)
}
/// Determine if we should ignore failures for this step
pub fn ignore_failure(&self, step: Step) -> bool {
self.config_file
.misc
.as_ref()
.and_then(|misc| misc.ignore_failures.as_ref())
.map(|v| v.contains(&step))
.unwrap_or(false)
.is_some_and(|v| v.contains(&step))
}
pub fn use_predefined_git_repos(&self) -> bool {
@@ -1415,6 +1551,14 @@ impl Config {
.unwrap_or(false)
}
pub fn winget_silent_install(&self) -> bool {
self.config_file
.windows
.as_ref()
.and_then(|windows| windows.winget_silent_install)
.unwrap_or(true)
}
pub fn sudo_command(&self) -> Option<SudoKind> {
self.config_file.misc.as_ref().and_then(|misc| misc.sudo_command)
}
@@ -1446,6 +1590,10 @@ impl Config {
.unwrap_or(false)
}
pub fn deno_version(&self) -> Option<&str> {
self.config_file.deno.as_ref().and_then(|deno| deno.version.as_deref())
}
#[cfg(target_os = "linux")]
pub fn firmware_upgrade(&self) -> bool {
self.config_file
@@ -1487,12 +1635,11 @@ impl Config {
}
pub fn enable_pipupgrade(&self) -> bool {
return self
.config_file
self.config_file
.python
.as_ref()
.and_then(|python| python.enable_pipupgrade)
.unwrap_or(false);
.unwrap_or(false)
}
pub fn pipupgrade_arguments(&self) -> &str {
self.config_file
@@ -1502,20 +1649,25 @@ impl Config {
.unwrap_or("")
}
pub fn enable_pip_review(&self) -> bool {
return self
.config_file
self.config_file
.python
.as_ref()
.and_then(|python| python.enable_pip_review)
.unwrap_or(false);
.unwrap_or(false)
}
pub fn enable_pip_review_local(&self) -> bool {
return self
.config_file
self.config_file
.python
.as_ref()
.and_then(|python| python.enable_pip_review_local)
.unwrap_or(false);
.unwrap_or(false)
}
pub fn poetry_force_self_update(&self) -> bool {
self.config_file
.python
.as_ref()
.and_then(|python| python.poetry_force_self_update)
.unwrap_or(false)
}
pub fn display_time(&self) -> bool {
@@ -1541,6 +1693,55 @@ impl Config {
.and_then(|lensfun| lensfun.use_sudo)
.unwrap_or(false)
}
pub fn julia_use_startup_file(&self) -> bool {
self.config_file
.julia
.as_ref()
.and_then(|julia| julia.startup_file)
.unwrap_or(true)
}
pub fn zigup_target_versions(&self) -> Vec<String> {
self.config_file
.zigup
.as_ref()
.and_then(|zigup| zigup.target_versions.clone())
.unwrap_or(vec!["master".to_owned()])
}
pub fn zigup_install_dir(&self) -> Option<&str> {
self.config_file
.zigup
.as_ref()
.and_then(|zigup| zigup.install_dir.as_deref())
}
pub fn zigup_path_link(&self) -> Option<&str> {
self.config_file
.zigup
.as_ref()
.and_then(|zigup| zigup.path_link.as_deref())
}
pub fn zigup_cleanup(&self) -> bool {
self.config_file
.zigup
.as_ref()
.and_then(|zigup| zigup.cleanup)
.unwrap_or(false)
}
pub fn vscode_profile(&self) -> Option<&str> {
let vscode_cfg = self.config_file.vscode.as_ref()?;
let profile = vscode_cfg.profile.as_ref()?;
if profile.is_empty() {
None
} else {
Some(profile.as_str())
}
}
}
#[cfg(test)]
@@ -1567,40 +1768,40 @@ mod test {
#[test]
fn test_should_execute_remote_different_hostname() {
assert!(config().should_execute_remote(Ok("hostname".to_string()), "remote_hostname"))
assert!(config().should_execute_remote(Ok("hostname".to_string()), "remote_hostname"));
}
#[test]
fn test_should_execute_remote_different_hostname_with_user() {
assert!(config().should_execute_remote(Ok("hostname".to_string()), "user@remote_hostname"))
assert!(config().should_execute_remote(Ok("hostname".to_string()), "user@remote_hostname"));
}
#[test]
fn test_should_execute_remote_unknown_hostname() {
assert!(config().should_execute_remote(Err(eyre!("failed to get hostname")), "remote_hostname"))
assert!(config().should_execute_remote(Err(eyre!("failed to get hostname")), "remote_hostname"));
}
#[test]
fn test_should_not_execute_remote_same_hostname() {
assert!(!config().should_execute_remote(Ok("hostname".to_string()), "hostname"))
assert!(!config().should_execute_remote(Ok("hostname".to_string()), "hostname"));
}
#[test]
fn test_should_not_execute_remote_same_hostname_with_user() {
assert!(!config().should_execute_remote(Ok("hostname".to_string()), "user@hostname"))
assert!(!config().should_execute_remote(Ok("hostname".to_string()), "user@hostname"));
}
#[test]
fn test_should_execute_remote_matching_limit() {
let mut config = config();
config.opt = CommandLineArgs::parse_from(["topgrade", "--remote-host-limit", "remote_hostname"]);
assert!(config.should_execute_remote(Ok("hostname".to_string()), "user@remote_hostname"))
assert!(config.should_execute_remote(Ok("hostname".to_string()), "user@remote_hostname"));
}
#[test]
fn test_should_not_execute_remote_not_matching_limit() {
let mut config = config();
config.opt = CommandLineArgs::parse_from(["topgrade", "--remote-host-limit", "other_hostname"]);
assert!(!config.should_execute_remote(Ok("hostname".to_string()), "user@remote_hostname"))
assert!(!config.should_execute_remote(Ok("hostname".to_string()), "user@remote_hostname"));
}
}

View File

@@ -11,9 +11,9 @@ pub fn interrupted() -> bool {
/// Clears the interrupted flag
pub fn unset_interrupted() {
debug_assert!(INTERRUPTED.load(Ordering::SeqCst));
INTERRUPTED.store(false, Ordering::SeqCst)
INTERRUPTED.store(false, Ordering::SeqCst);
}
pub fn set_interrupted() {
INTERRUPTED.store(true, Ordering::SeqCst)
INTERRUPTED.store(true, Ordering::SeqCst);
}

View File

@@ -4,7 +4,7 @@ use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal
/// Handle SIGINT. Set the interruption flag.
extern "C" fn handle_sigint(_: i32) {
set_interrupted()
set_interrupted();
}
/// Set the necessary signal handlers.

View File

@@ -1,41 +1,98 @@
use std::process::ExitStatus;
use std::{fmt::Display, process::ExitStatus};
use rust_i18n::t;
use thiserror::Error;
#[derive(Error, Debug, PartialEq, Eq)]
pub enum TopgradeError {
#[error("`{0}` failed: {1}")]
ProcessFailed(String, ExitStatus),
#[error("`{0}` failed: {1}")]
ProcessFailedWithOutput(String, ExitStatus, String),
#[error("Unknown Linux Distribution")]
#[cfg(target_os = "linux")]
UnknownLinuxDistribution,
#[error("File \"/etc/os-release\" does not exist or is empty")]
#[cfg(target_os = "linux")]
EmptyOSReleaseFile,
#[error("Failed getting the system package manager")]
#[cfg(target_os = "linux")]
FailedGettingPackageManager,
}
impl Display for TopgradeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TopgradeError::ProcessFailed(process, exit_status) => {
write!(
f,
"{}",
t!(
"`{process}` failed: {exit_status}",
process = process,
exit_status = exit_status
)
)
}
TopgradeError::ProcessFailedWithOutput(process, exit_status, output) => {
write!(
f,
"{}",
t!(
"`{process}` failed: {exit_status} with {output}",
process = process,
exit_status = exit_status,
output = output
)
)
}
#[cfg(target_os = "linux")]
TopgradeError::UnknownLinuxDistribution => write!(f, "{}", t!("Unknown Linux Distribution")),
#[cfg(target_os = "linux")]
TopgradeError::EmptyOSReleaseFile => {
write!(f, "{}", t!("File \"/etc/os-release\" does not exist or is empty"))
}
#[cfg(target_os = "linux")]
TopgradeError::FailedGettingPackageManager => {
write!(f, "{}", t!("Failed getting the system package manager"))
}
}
}
}
#[derive(Error, Debug)]
#[error("A step failed")]
pub struct StepFailed;
#[derive(Error, Debug)]
#[error("Dry running")]
pub struct DryRun();
impl Display for StepFailed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", t!("A step failed"))
}
}
#[derive(Error, Debug)]
pub struct DryRun();
impl Display for DryRun {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", t!("Dry running"))
}
}
#[derive(Error, Debug)]
#[error("{0}")]
pub struct SkipStep(pub String);
impl Display for SkipStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(all(windows, feature = "self-update"))]
#[derive(Error, Debug)]
#[error("Topgrade Upgraded")]
pub struct Upgraded(pub ExitStatus);
#[cfg(all(windows, feature = "self-update"))]
impl Display for Upgraded {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", t!("Topgrade Upgraded"))
}
}

View File

@@ -1,7 +1,7 @@
#![allow(dead_code)]
use crate::executor::RunType;
use crate::sudo::Sudo;
use crate::utils::{require_option, REQUIRE_SUDO};
use crate::utils::{get_require_sudo_string, require_option};
use crate::{config::Config, executor::Executor};
use color_eyre::eyre::Result;
use std::env::var;
@@ -33,7 +33,7 @@ impl<'a> ExecutionContext<'a> {
}
pub fn execute_elevated(&self, command: &Path, interactive: bool) -> Result<Executor> {
let sudo = require_option(self.sudo.as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(self.sudo.as_ref(), get_require_sudo_string())?;
Ok(sudo.execute_elevated(self, command, interactive))
}

View File

@@ -4,6 +4,7 @@ use std::path::Path;
use std::process::{Child, Command, ExitStatus, Output};
use color_eyre::eyre::Result;
use rust_i18n::t;
use tracing::debug;
use crate::command::CommandExt;
@@ -151,7 +152,10 @@ impl Executor {
let result = match self {
Executor::Wet(c) => {
debug!("Running {:?}", c);
c.spawn_checked().map(ExecutorChild::Wet)?
// We should use `spawn()` here rather than `spawn_checked()` since
// their semantics and behaviors are different.
#[allow(clippy::disallowed_methods)]
c.spawn().map(ExecutorChild::Wet)?
}
Executor::Dry(c) => {
c.dry_run();
@@ -165,7 +169,12 @@ impl Executor {
/// See `std::process::Command::output`
pub fn output(&mut self) -> Result<ExecutorOutput> {
match self {
Executor::Wet(c) => Ok(ExecutorOutput::Wet(c.output_checked()?)),
Executor::Wet(c) => {
// We should use `output()` here rather than `output_checked()` since
// their semantics and behaviors are different.
#[allow(clippy::disallowed_methods)]
Ok(ExecutorOutput::Wet(c.output()?))
}
Executor::Dry(c) => {
c.dry_run();
Ok(ExecutorOutput::Dry)
@@ -179,7 +188,7 @@ impl Executor {
pub fn status_checked_with_codes(&mut self, codes: &[i32]) -> Result<()> {
match self {
Executor::Wet(c) => c.status_checked_with(|status| {
if status.success() || status.code().as_ref().map(|c| codes.contains(c)).unwrap_or(false) {
if status.success() || status.code().as_ref().is_some_and(|c| codes.contains(c)) {
Ok(())
} else {
Err(())
@@ -209,17 +218,20 @@ pub struct DryCommand {
impl DryCommand {
fn dry_run(&self) {
print!(
"Dry running: {} {}",
self.program.to_string_lossy(),
shell_words::join(
self.args
.iter()
.map(|a| String::from(a.to_string_lossy()))
.collect::<Vec<String>>()
"{}",
t!(
"Dry running: {program_name} {arguments}",
program_name = self.program.to_string_lossy(),
arguments = shell_words::join(
self.args
.iter()
.map(|a| String::from(a.to_string_lossy()))
.collect::<Vec<String>>()
)
)
);
match &self.directory {
Some(dir) => println!(" in {}", dir.to_string_lossy()),
Some(dir) => println!(" {}", t!("in {directory}", directory = dir.to_string_lossy())),
None => println!(),
};
}

View File

@@ -18,13 +18,16 @@ use etcetera::base_strategy::Windows;
#[cfg(unix)]
use etcetera::base_strategy::Xdg;
use once_cell::sync::Lazy;
use rust_i18n::{i18n, t};
use tracing::debug;
use self::config::{CommandLineArgs, Config, Step};
use self::error::StepFailed;
#[cfg(all(windows, feature = "self-update"))]
use self::error::Upgraded;
#[allow(clippy::wildcard_imports)]
use self::steps::{remote::*, *};
#[allow(clippy::wildcard_imports)]
use self::terminal::*;
use self::utils::{hostname, install_color_eyre, install_tracing, update_tracing};
@@ -54,6 +57,10 @@ pub(crate) static XDG_DIRS: Lazy<Xdg> = Lazy::new(|| Xdg::new().expect("No home
#[cfg(windows)]
pub(crate) static WINDOWS_DIRS: Lazy<Windows> = Lazy::new(|| Windows::new().expect("No home directory"));
// Init and load the i18n files
i18n!("locales", fallback = "en");
#[allow(clippy::too_many_lines)]
fn run() -> Result<()> {
install_color_eyre()?;
ctrlc::set_handler();
@@ -72,6 +79,11 @@ fn run() -> Result<()> {
// and `Config::tracing_filter_directives()`.
let reload_handle = install_tracing(&opt.tracing_filter_directives())?;
// Get current system locale and set it as the default locale
let system_locale = sys_locale::get_locale().unwrap_or("en".to_string());
rust_i18n::set_locale(&system_locale);
debug!("Current system locale is {system_locale}");
if let Some(shell) = opt.gen_completion {
let cmd = &mut CommandLineArgs::command();
clap_complete::generate(shell, cmd, clap::crate_name!(), &mut io::stdout());
@@ -118,7 +130,7 @@ fn run() -> Result<()> {
if config.run_in_tmux() && env::var("TOPGRADE_INSIDE_TMUX").is_err() {
#[cfg(unix)]
{
tmux::run_in_tmux(config.tmux_arguments()?)?;
tmux::run_in_tmux(config.tmux_config()?)?;
return Ok(());
}
}
@@ -197,6 +209,9 @@ fn run() -> Result<()> {
runner.execute(Step::Scoop, "Scoop", || windows::run_scoop(&ctx))?;
runner.execute(Step::Winget, "Winget", || windows::run_winget(&ctx))?;
runner.execute(Step::System, "Windows update", || windows::windows_update(&ctx))?;
runner.execute(Step::MicrosoftStore, "Microsoft Store", || {
windows::microsoft_store(&ctx)
})?;
}
#[cfg(target_os = "linux")]
@@ -210,7 +225,7 @@ fn run() -> Result<()> {
runner.execute(Step::System, "System update", || distribution.upgrade(&ctx))?;
}
Err(e) => {
println!("Error detecting current distribution: {e}");
println!("{}", t!("Error detecting current distribution: {error}", error = e));
}
}
runner.execute(Step::ConfigUpdate, "config-update", || linux::run_config_update(&ctx))?;
@@ -236,6 +251,9 @@ fn run() -> Result<()> {
runner.execute(Step::Lure, "LURE", || linux::run_lure_update(&ctx))?;
runner.execute(Step::Waydroid, "Waydroid", || linux::run_waydroid(&ctx))?;
runner.execute(Step::AutoCpufreq, "auto-cpufreq", || linux::run_auto_cpufreq(&ctx))?;
runner.execute(Step::CinnamonSpices, "Cinnamon spices", || {
linux::run_cinnamon_spices_updater(&ctx)
})?;
}
#[cfg(target_os = "macos")]
@@ -301,7 +319,6 @@ fn run() -> Result<()> {
runner.execute(Step::Asdf, "asdf", || unix::run_asdf(&ctx))?;
runner.execute(Step::Mise, "mise", || unix::run_mise(&ctx))?;
runner.execute(Step::Pkgin, "pkgin", || unix::run_pkgin(&ctx))?;
runner.execute(Step::Bun, "bun", || unix::run_bun(&ctx))?;
runner.execute(Step::BunPackages, "bun-packages", || unix::run_bun_packages(&ctx))?;
runner.execute(Step::Shell, "zr", || zsh::run_zr(&ctx))?;
runner.execute(Step::Shell, "antibody", || zsh::run_antibody(&ctx))?;
@@ -358,11 +375,16 @@ fn run() -> Result<()> {
runner.execute(Step::Opam, "opam", || generic::run_opam_update(&ctx))?;
runner.execute(Step::Vcpkg, "vcpkg", || generic::run_vcpkg_update(&ctx))?;
runner.execute(Step::Pipx, "pipx", || generic::run_pipx_update(&ctx))?;
runner.execute(Step::Pipxu, "pipxu", || generic::run_pipxu_update(&ctx))?;
runner.execute(Step::Vscode, "Visual Studio Code extensions", || {
generic::run_vscode_extensions_update(&ctx)
})?;
runner.execute(Step::Vscodium, "VSCodium extensions", || {
generic::run_vscodium_extensions_update(&ctx)
})?;
runner.execute(Step::Conda, "conda", || generic::run_conda_update(&ctx))?;
runner.execute(Step::Mamba, "mamba", || generic::run_mamba_update(&ctx))?;
runner.execute(Step::Pixi, "pixi", || generic::run_pixi_update(&ctx))?;
runner.execute(Step::Miktex, "miktex", || generic::run_miktex_packages_update(&ctx))?;
runner.execute(Step::Pip3, "pip3", || generic::run_pip3_update(&ctx))?;
runner.execute(Step::PipReview, "pip-review", || generic::run_pip_review_update(&ctx))?;
@@ -385,6 +407,9 @@ fn run() -> Result<()> {
runner.execute(Step::Node, "npm", || node::run_npm_upgrade(&ctx))?;
runner.execute(Step::Yarn, "yarn", || node::run_yarn_upgrade(&ctx))?;
runner.execute(Step::Pnpm, "pnpm", || node::run_pnpm_upgrade(&ctx))?;
runner.execute(Step::VoltaPackages, "volta packages", || {
node::run_volta_packages_upgrade(&ctx)
})?;
runner.execute(Step::Containers, "Containers", || containers::run_containers(&ctx))?;
runner.execute(Step::Deno, "deno", || node::deno_upgrade(&ctx))?;
runner.execute(Step::Composer, "composer", || generic::run_composer_update(&ctx))?;
@@ -415,6 +440,15 @@ fn run() -> Result<()> {
runner.execute(Step::Lensfun, "Lensfun's database update", || {
generic::run_lensfun_update_data(&ctx)
})?;
runner.execute(Step::Poetry, "Poetry", || generic::run_poetry(&ctx))?;
runner.execute(Step::Uv, "uv", || generic::run_uv(&ctx))?;
runner.execute(Step::Zvm, "ZVM", || generic::run_zvm(&ctx))?;
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", || {
generic::run_jetbrains_toolbox(&ctx)
})?;
if should_run_powershell {
runner.execute(Step::Powershell, "Powershell Modules Update", || {
@@ -444,7 +478,7 @@ fn run() -> Result<()> {
runner.execute(Step::Vagrant, "Vagrant boxes", || vagrant::upgrade_vagrant_boxes(&ctx))?;
if !runner.report().data().is_empty() {
print_separator("Summary");
print_separator(t!("Summary"));
for (key, result) in runner.report().data() {
print_result(key, result);
@@ -468,16 +502,16 @@ fn run() -> Result<()> {
}
if config.keep_at_end() {
print_info("\n(R)eboot\n(S)hell\n(Q)uit");
print_info(t!("\n(R)eboot\n(S)hell\n(Q)uit"));
loop {
match get_key() {
Ok(Key::Char('s')) | Ok(Key::Char('S')) => {
Ok(Key::Char('s' | 'S')) => {
run_shell().context("Failed to execute shell")?;
}
Ok(Key::Char('r')) | Ok(Key::Char('R')) => {
Ok(Key::Char('r' | 'R')) => {
reboot().context("Failed to reboot")?;
}
Ok(Key::Char('q')) | Ok(Key::Char('Q')) => (),
Ok(Key::Char('q' | 'Q')) => (),
_ => {
continue;
}
@@ -490,12 +524,13 @@ fn run() -> Result<()> {
if !config.skip_notify() {
notify_desktop(
format!(
"Topgrade finished {}",
if failed { "with errors" } else { "successfully" }
),
if failed {
t!("Topgrade finished with errors")
} else {
t!("Topgrade finished successfully")
},
Some(Duration::from_secs(10)),
)
);
}
if failed {
@@ -528,7 +563,7 @@ fn main() {
// The `Debug` implementation of `eyre::Result` prints a multi-line
// error message that includes all the 'causes' added with
// `.with_context(...)` calls.
println!("Error: {error:?}");
println!("{}", t!("Error: {error}", error = format!("{:?}", error)));
}
exit(1);
}

View File

@@ -5,20 +5,21 @@ use std::process::Command;
use crate::config::Step;
use color_eyre::eyre::{bail, Result};
use rust_i18n::t;
use self_update_crate::backends::github::Update;
use self_update_crate::update::UpdateStatus;
use super::terminal::*;
use super::terminal::{print_info, print_separator};
#[cfg(windows)]
use crate::error::Upgraded;
use crate::execution_context::ExecutionContext;
pub fn self_update(ctx: &ExecutionContext) -> Result<()> {
print_separator("Self update");
print_separator(t!("Self update"));
if ctx.run_type().dry() {
println!("Would self-update");
println!("{}", t!("Would self-update"));
Ok(())
} else {
let assume_yes = ctx.config().yes(Step::SelfUpdate);
@@ -38,17 +39,17 @@ pub fn self_update(ctx: &ExecutionContext) -> Result<()> {
.update_extended()?;
if let UpdateStatus::Updated(release) = &result {
println!("\nTopgrade upgraded to {}:\n", release.version);
println!("{}", t!("Topgrade upgraded to {version}:\n", version = release.version));
if let Some(body) = &release.body {
println!("{body}");
}
} else {
println!("Topgrade is up-to-date");
println!("{}", t!("Topgrade is up-to-date"));
}
{
if result.updated() {
print_info("Respawning...");
print_info(t!("Respawning..."));
let mut command = Command::new(current_exe?);
command.args(env::args().skip(1)).env("TOPGRADE_NO_SELF_UPGRADE", "");

View File

@@ -1,186 +1,196 @@
use std::fmt::{Display, Formatter};
use std::path::Path;
use std::process::Command;
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use tracing::{debug, error, warn};
use wildmatch::WildMatch;
use crate::command::CommandExt;
use crate::error::{self, TopgradeError};
use crate::terminal::print_separator;
use crate::{execution_context::ExecutionContext, utils::require};
// A string found in the output of docker for containers that weren't found in
// the docker registry. We use this to gracefully handle and skip containers
// that cannot be pulled, likely because they don't exist in the registry in
// the first place. This happens e.g. when the user tags an image locally
// themselves or when using docker-compose.
const NONEXISTENT_REPO: &str = "repository does not exist";
/// Uniquely identifies a `Container`.
#[derive(Debug)]
struct Container {
/// `Repository` and `Tag`
///
/// format: `Repository:Tag`, e.g., `nixos/nix:latest`.
repo_tag: String,
/// Platform
///
/// format: `OS/Architecture`, e.g., `linux/amd64`.
platform: String,
}
impl Container {
/// Construct a new `Container`.
fn new(repo_tag: String, platform: String) -> Self {
Self { repo_tag, platform }
}
}
impl Display for Container {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
// e.g., "`fedora:latest` for `linux/amd64`"
write!(f, "`{}` for `{}`", self.repo_tag, self.platform)
}
}
/// Returns a Vector of all containers, with Strings in the format
/// "REGISTRY/[PATH/]CONTAINER_NAME:TAG"
///
/// Containers specified in `ignored_containers` will be filtered out.
fn list_containers(crt: &Path, ignored_containers: Option<&Vec<String>>) -> Result<Vec<Container>> {
let ignored_containers = ignored_containers.map(|patterns| {
patterns
.iter()
.map(|pattern| WildMatch::new(pattern))
.collect::<Vec<WildMatch>>()
});
debug!(
"Querying '{} image ls --format \"{{{{.Repository}}}}:{{{{.Tag}}}}/{{{{.ID}}}}\"' for containers",
crt.display()
);
let output = Command::new(crt)
.args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}"])
.output_checked_with_utf8(|_| Ok(()))?;
let mut retval = vec![];
for line in output.stdout.lines() {
if line.starts_with("localhost") {
// Don't know how to update self-built containers
debug!("Skipping self-built container '{}'", line);
continue;
}
if line.contains("<none>") {
// Bogus/dangling container or intermediate layer
debug!("Skipping bogus container '{}'", line);
continue;
}
if line.starts_with("vsc-") {
debug!("Skipping visual studio code dev container '{}'", line);
continue;
}
debug!("Using container '{}'", line);
// line is of format: `Repository:Tag ImageID`, e.g., `nixos/nix:latest d80fea9c32b4`
let split_res = line.split(' ').collect::<Vec<&str>>();
assert_eq!(split_res.len(), 2);
let (repo_tag, image_id) = (split_res[0], split_res[1]);
if let Some(ref ignored_containers) = ignored_containers {
if ignored_containers.iter().any(|pattern| pattern.matches(repo_tag)) {
debug!("Skipping ignored container '{}'", line);
continue;
}
}
debug!(
"Querying '{} image inspect --format \"{{{{.Os}}}}/{{{{.Architecture}}}}\"' for container {}",
crt.display(),
image_id
);
let inspect_output = Command::new(crt)
.args(["image", "inspect", image_id, "--format", "{{.Os}}/{{.Architecture}}"])
.output_checked_with_utf8(|_| Ok(()))?;
let mut platform = inspect_output.stdout;
// truncate the tailing new line character
platform.truncate(platform.len() - 1);
assert!(platform.contains('/'));
retval.push(Container::new(repo_tag.to_string(), platform));
}
Ok(retval)
}
pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
// Prefer podman, fall back to docker if not present
let crt = require("podman").or_else(|_| require("docker"))?;
debug!("Using container runtime '{}'", crt.display());
print_separator("Containers");
let mut success = true;
let containers =
list_containers(&crt, ctx.config().containers_ignored_tags()).context("Failed to list Docker containers")?;
debug!("Containers to inspect: {:?}", containers);
for container in containers.iter() {
debug!("Pulling container '{}'", container);
let args = vec![
"pull",
container.repo_tag.as_str(),
"--platform",
container.platform.as_str(),
];
let mut exec = ctx.run_type().execute(&crt);
if let Err(e) = exec.args(&args).status_checked() {
error!("Pulling container '{}' failed: {}", container, e);
// Find out if this is 'skippable'
// This is necessary e.g. for docker, because unlike podman docker doesn't tell from
// which repository a container originates (such as `docker.io`). This has the
// practical consequence that all containers, whether self-built, created by
// docker-compose or pulled from the docker hub, look exactly the same to us. We can
// only find out what went wrong by manually parsing the output of the command...
if match exec.output_checked_utf8() {
Ok(s) => s.stdout.contains(NONEXISTENT_REPO) || s.stderr.contains(NONEXISTENT_REPO),
Err(e) => match e.downcast_ref::<TopgradeError>() {
Some(TopgradeError::ProcessFailedWithOutput(_, _, stderr)) => stderr.contains(NONEXISTENT_REPO),
_ => false,
},
} {
warn!("Skipping unknown container '{}'", container);
continue;
}
success = false;
}
}
if ctx.config().cleanup() {
// Remove dangling images
debug!("Removing dangling images");
if let Err(e) = ctx
.run_type()
.execute(&crt)
.args(["image", "prune", "-f"])
.status_checked()
{
error!("Removing dangling images failed: {}", e);
success = false;
}
}
if success {
Ok(())
} else {
Err(eyre!(error::StepFailed))
}
}
use std::fmt::{Display, Formatter};
use std::path::Path;
use std::process::Command;
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use tracing::{debug, error, warn};
use wildmatch::WildMatch;
use crate::command::CommandExt;
use crate::error::{self, TopgradeError};
use crate::terminal::print_separator;
use crate::{execution_context::ExecutionContext, utils::require};
use rust_i18n::t;
// A string found in the output of docker for containers that weren't found in
// the docker registry. We use this to gracefully handle and skip containers
// that cannot be pulled, likely because they don't exist in the registry in
// the first place. This happens e.g. when the user tags an image locally
// themselves or when using docker-compose.
const NONEXISTENT_REPO: &str = "repository does not exist";
/// Uniquely identifies a `Container`.
#[derive(Debug)]
struct Container {
/// `Repository` and `Tag`
///
/// format: `Repository:Tag`, e.g., `nixos/nix:latest`.
repo_tag: String,
/// Platform
///
/// format: `OS/Architecture`, e.g., `linux/amd64`.
platform: String,
}
impl Container {
/// Construct a new `Container`.
fn new(repo_tag: String, platform: String) -> Self {
Self { repo_tag, platform }
}
}
impl Display for Container {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
// e.g., "`fedora:latest` for `linux/amd64`"
write!(
f,
"{}",
t!(
"`{repo_tag}` for `{platform}`",
repo_tag = self.repo_tag,
platform = self.platform
)
)
}
}
/// Returns a Vector of all containers, with Strings in the format
/// "REGISTRY/[PATH/]CONTAINER_NAME:TAG"
///
/// Containers specified in `ignored_containers` will be filtered out.
fn list_containers(crt: &Path, ignored_containers: Option<&Vec<String>>) -> Result<Vec<Container>> {
let ignored_containers = ignored_containers.map(|patterns| {
patterns
.iter()
.map(|pattern| WildMatch::new(pattern))
.collect::<Vec<WildMatch>>()
});
debug!(
"Querying '{} image ls --format \"{{{{.Repository}}}}:{{{{.Tag}}}}/{{{{.ID}}}}\"' for containers",
crt.display()
);
let output = Command::new(crt)
.args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}"])
.output_checked_with_utf8(|_| Ok(()))?;
let mut retval = vec![];
for line in output.stdout.lines() {
if line.starts_with("localhost") {
// Don't know how to update self-built containers
debug!("Skipping self-built container '{}'", line);
continue;
}
if line.contains("<none>") {
// Bogus/dangling container or intermediate layer
debug!("Skipping bogus container '{}'", line);
continue;
}
if line.starts_with("vsc-") {
debug!("Skipping visual studio code dev container '{}'", line);
continue;
}
debug!("Using container '{}'", line);
// line is of format: `Repository:Tag ImageID`, e.g., `nixos/nix:latest d80fea9c32b4`
let split_res = line.split(' ').collect::<Vec<&str>>();
assert_eq!(split_res.len(), 2);
let (repo_tag, image_id) = (split_res[0], split_res[1]);
if let Some(ref ignored_containers) = ignored_containers {
if ignored_containers.iter().any(|pattern| pattern.matches(repo_tag)) {
debug!("Skipping ignored container '{}'", line);
continue;
}
}
debug!(
"Querying '{} image inspect --format \"{{{{.Os}}}}/{{{{.Architecture}}}}\"' for container {}",
crt.display(),
image_id
);
let inspect_output = Command::new(crt)
.args(["image", "inspect", image_id, "--format", "{{.Os}}/{{.Architecture}}"])
.output_checked_with_utf8(|_| Ok(()))?;
let mut platform = inspect_output.stdout;
// truncate the tailing new line character
platform.truncate(platform.len() - 1);
assert!(platform.contains('/'));
retval.push(Container::new(repo_tag.to_string(), platform));
}
Ok(retval)
}
pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
// Check what runtime is specified in the config
let container_runtime = ctx.config().containers_runtime().to_string();
let crt = require(container_runtime)?;
debug!("Using container runtime '{}'", crt.display());
print_separator(t!("Containers"));
let mut success = true;
let containers =
list_containers(&crt, ctx.config().containers_ignored_tags()).context("Failed to list Docker containers")?;
debug!("Containers to inspect: {:?}", containers);
for container in &containers {
debug!("Pulling container '{}'", container);
let args = vec![
"pull",
container.repo_tag.as_str(),
"--platform",
container.platform.as_str(),
];
let mut exec = ctx.run_type().execute(&crt);
if let Err(e) = exec.args(&args).status_checked() {
error!("Pulling container '{}' failed: {}", container, e);
// Find out if this is 'skippable'
// This is necessary e.g. for docker, because unlike podman docker doesn't tell from
// which repository a container originates (such as `docker.io`). This has the
// practical consequence that all containers, whether self-built, created by
// docker-compose or pulled from the docker hub, look exactly the same to us. We can
// only find out what went wrong by manually parsing the output of the command...
if match exec.output_checked_utf8() {
Ok(s) => s.stdout.contains(NONEXISTENT_REPO) || s.stderr.contains(NONEXISTENT_REPO),
Err(e) => match e.downcast_ref::<TopgradeError>() {
Some(TopgradeError::ProcessFailedWithOutput(_, _, stderr)) => stderr.contains(NONEXISTENT_REPO),
_ => false,
},
} {
warn!("Skipping unknown container '{}'", container);
continue;
}
success = false;
}
}
if ctx.config().cleanup() {
// Remove dangling images
debug!("Removing dangling images");
if let Err(e) = ctx
.run_type()
.execute(&crt)
.args(["image", "prune", "-f"])
.status_checked()
{
error!("Removing dangling images failed: {}", e);
success = false;
}
}
if success {
Ok(())
} else {
Err(eyre!(error::StepFailed))
}
}

View File

@@ -1,9 +1,4 @@
(when (fboundp 'paradox-upgrade-packages)
(progn
(unless (boundp 'paradox-github-token)
(setq paradox-github-token t))
(paradox-upgrade-packages)
(princ
(if (get-buffer "*Paradox Report*")
(with-current-buffer "*Paradox Report*" (buffer-string))
"\nNothing to upgrade\n"))))
(when (featurep 'package)
(if (fboundp 'package-upgrade-all)
(package-upgrade-all nil)
(message "Your Emacs version doesn't support unattended packages upgrade")))

View File

@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
use rust_i18n::t;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
@@ -74,9 +75,12 @@ impl Emacs {
if let Some(doom) = &self.doom {
Emacs::update_doom(doom, ctx)?;
}
let init_file = require_option(self.directory.as_ref(), String::from("Emacs directory does not exist"))?
.join("init.el")
.require()?;
let init_file = require_option(
self.directory.as_ref(),
t!("Emacs directory does not exist").to_string(),
)?
.join("init.el")
.require()?;
print_separator("Emacs");

View File

@@ -1,5 +1,6 @@
#![allow(unused_imports)]
use std::ffi::{OsStr, OsString};
use std::path::PathBuf;
use std::process::Command;
use std::{env, path::Path};
@@ -8,6 +9,10 @@ use std::{fs, io::Write};
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
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 tempfile::tempfile_in;
use tracing::{debug, error};
@@ -16,7 +21,7 @@ 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, require, require_option, which, PathExt, REQUIRE_SUDO};
use crate::utils::{self, check_is_python_2_or_shim, get_require_sudo_string, require, require_option, which, PathExt};
use crate::Step;
use crate::HOME_DIR;
use crate::{
@@ -38,8 +43,7 @@ pub fn is_wsl() -> Result<bool> {
pub fn run_cargo_update(ctx: &ExecutionContext) -> Result<()> {
let cargo_dir = env::var_os("CARGO_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| HOME_DIR.join(".cargo"))
.map_or_else(|| HOME_DIR.join(".cargo"), PathBuf::from)
.require()?;
require("cargo").or_else(|_| {
require_option(
@@ -58,13 +62,11 @@ pub fn run_cargo_update(ctx: &ExecutionContext) -> Result<()> {
let cargo_update = require("cargo-install-update")
.ok()
.or_else(|| cargo_dir.join("bin/cargo-install-update").if_exists());
let cargo_update = match cargo_update {
Some(e) => e,
None => {
let message = String::from("cargo-update isn't installed so Topgrade can't upgrade cargo packages.\nInstall cargo-update by running `cargo install cargo-update`");
print_warning(&message);
return Err(SkipStep(message).into());
}
let Some(cargo_update) = cargo_update else {
let message = String::from("cargo-update isn't installed so Topgrade can't upgrade cargo packages.\nInstall cargo-update by running `cargo install cargo-update`");
print_warning(&message);
return Err(SkipStep(message).into());
};
ctx.run_type()
@@ -76,14 +78,11 @@ pub fn run_cargo_update(ctx: &ExecutionContext) -> Result<()> {
let cargo_cache = require("cargo-cache")
.ok()
.or_else(|| cargo_dir.join("bin/cargo-cache").if_exists());
match cargo_cache {
Some(e) => {
ctx.run_type().execute(e).args(["-a"]).status_checked()?;
}
None => {
let message = String::from("cargo-cache isn't installed so Topgrade can't cleanup cargo packages.\nInstall cargo-cache by running `cargo install cargo-cache`");
print_warning(message);
}
if let Some(e) = cargo_cache {
ctx.run_type().execute(e).args(["-a"]).status_checked()?;
} else {
let message = String::from("cargo-cache isn't installed so Topgrade can't cleanup cargo packages.\nInstall cargo-cache by running `cargo install cargo-cache`");
print_warning(message);
}
}
@@ -121,6 +120,7 @@ pub fn run_rubygems(ctx: &ExecutionContext) -> Result<()> {
print_separator("RubyGems");
let gem_path_str = gem.as_os_str();
if gem_path_str.to_str().unwrap().contains("asdf")
|| gem_path_str.to_str().unwrap().contains("mise")
|| gem_path_str.to_str().unwrap().contains(".rbenv")
|| gem_path_str.to_str().unwrap().contains(".rvm")
{
@@ -129,7 +129,7 @@ pub fn run_rubygems(ctx: &ExecutionContext) -> Result<()> {
.args(["update", "--system"])
.status_checked()?;
} else {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
if !Path::new("/usr/lib/ruby/vendor_ruby/rubygems/defaults/operating_system.rb").exists() {
ctx.run_type()
.execute(sudo)
@@ -158,7 +158,7 @@ pub fn run_haxelib_update(ctx: &ExecutionContext) -> Result<()> {
let mut command = if directory_writable {
ctx.run_type().execute(&haxelib)
} else {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let mut c = ctx.run_type().execute(sudo);
c.arg(&haxelib);
c
@@ -223,6 +223,27 @@ pub fn run_apm(ctx: &ExecutionContext) -> Result<()> {
.status_checked()
}
pub fn run_aqua(ctx: &ExecutionContext) -> Result<()> {
let aqua = require("aqua")?;
// 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());
}
print_separator("Aqua");
if ctx.run_type().dry() {
println!("{}", t!("Updating aqua ..."));
println!("{}", t!("Updating aqua installed cli tools ..."));
Ok(())
} else {
ctx.run_type().execute(&aqua).arg("update-aqua").status_checked()?;
ctx.run_type().execute(&aqua).arg("update").status_checked()
}
}
pub fn run_rustup(ctx: &ExecutionContext) -> Result<()> {
let rustup = require("rustup")?;
@@ -241,10 +262,35 @@ pub fn run_elan(ctx: &ExecutionContext) -> Result<()> {
let elan = require("elan")?;
print_separator("elan");
ctx.run_type()
.execute(&elan)
.args(["self", "update"])
.status_checked()?;
let disabled_error_msg = "self-update is disabled";
let executor_output = ctx.run_type().execute(&elan).args(["self", "update"]).output()?;
match executor_output {
ExecutorOutput::Wet(command_output) => {
if command_output.status.success() {
// Flush the captured output
std::io::stdout().lock().write_all(&command_output.stdout).unwrap();
std::io::stderr().lock().write_all(&command_output.stderr).unwrap();
} else {
let stderr_as_str = std::str::from_utf8(&command_output.stderr).unwrap();
if stderr_as_str.contains(disabled_error_msg) {
// `elan` is externally managed, we cannot do the update. Users
// won't see any error message because Topgrade captures them
// all.
} else {
// `elan` is NOT externally managed, `elan self update` can
// be performed, but the invocation failed, so we report the
// error to the user and error out.
std::io::stdout().lock().write_all(&command_output.stdout).unwrap();
std::io::stderr().lock().write_all(&command_output.stderr).unwrap();
return Err(StepFailed.into());
}
}
}
ExecutorOutput::Dry => { /* nothing needed because in a dry run */ }
}
ctx.run_type().execute(&elan).arg("update").status_checked()
}
@@ -260,7 +306,13 @@ pub fn run_juliaup(ctx: &ExecutionContext) -> Result<()> {
.status_checked()?;
}
ctx.run_type().execute(&juliaup).arg("update").status_checked()
ctx.run_type().execute(&juliaup).arg("update").status_checked()?;
if ctx.config().cleanup() {
ctx.run_type().execute(&juliaup).arg("gc").status_checked()?;
}
Ok(())
}
pub fn run_choosenim(ctx: &ExecutionContext) -> Result<()> {
@@ -349,7 +401,7 @@ pub fn run_vcpkg_update(ctx: &ExecutionContext) -> Result<()> {
let mut command = if is_root_install {
ctx.run_type().execute(&vcpkg)
} else {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let mut c = ctx.run_type().execute(sudo);
c.arg(&vcpkg);
c
@@ -358,6 +410,48 @@ 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)
if is_wsl()? {
return Err(SkipStep(String::from("Should not run in WSL")).into());
}
let vscodium = require("codium")?;
// 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)
.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()),
};
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());
}
print_separator("VSCodium extensions");
ctx.run_type()
.execute(vscodium)
.arg("--update-extensions")
.status_checked()
}
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()? {
@@ -375,7 +469,7 @@ pub fn run_vscode_extensions_update(ctx: &ExecutionContext) -> Result<()> {
.lines()
.next()
{
Some(item) => Version::parse(item).map_err(|err| err.into()),
Some(item) => Version::parse(item).map_err(std::convert::Into::into),
_ => return Err(SkipStep(String::from("Cannot find vscode version")).into()),
};
@@ -385,10 +479,19 @@ pub fn run_vscode_extensions_update(ctx: &ExecutionContext) -> Result<()> {
print_separator("Visual Studio Code extensions");
ctx.run_type()
.execute(vscode)
.arg("--update-extensions")
.status_checked()
if let Some(profile) = ctx.config().vscode_profile() {
ctx.run_type()
.execute(vscode)
.arg("--profile")
.arg(profile)
.arg("--update-extensions")
.status_checked()
} else {
ctx.run_type()
.execute(vscode)
.arg("--update-extensions")
.status_checked()
}
}
pub fn run_pipx_update(ctx: &ExecutionContext) -> Result<()> {
@@ -405,12 +508,22 @@ pub fn run_pipx_update(ctx: &ExecutionContext) -> Result<()> {
.map(|s| s.stdout.trim().to_owned());
let version = Version::parse(&version_str?);
if matches!(version, Ok(version) if version >= Version::new(1, 4, 0)) {
command_args.push("--quiet")
command_args.push("--quiet");
}
ctx.run_type().execute(pipx).args(command_args).status_checked()
}
pub fn run_pipxu_update(ctx: &ExecutionContext) -> Result<()> {
let pipxu = require("pipxu")?;
print_separator("pipxu");
ctx.run_type()
.execute(pipxu)
.args(["upgrade", "--all"])
.status_checked()
}
pub fn run_conda_update(ctx: &ExecutionContext) -> Result<()> {
let conda = require("conda")?;
@@ -424,33 +537,69 @@ pub fn run_conda_update(ctx: &ExecutionContext) -> Result<()> {
print_separator("Conda");
let mut command = ctx.run_type().execute(conda);
let mut command = ctx.run_type().execute(&conda);
command.args(["update", "--all", "-n", "base"]);
if ctx.config().yes(Step::Conda) {
command.arg("--yes");
}
command.status_checked()
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(conda);
command.args(["clean", "--all"]);
if ctx.config().yes(Step::Conda) {
command.arg("--yes");
}
command.status_checked()?;
}
Ok(())
}
pub fn run_pixi_update(ctx: &ExecutionContext) -> Result<()> {
let pixi = require("pixi")?;
print_separator("Pixi");
// Check if `pixi --help` mentions self-update, if yes, self-update must be enabled.
// pixi self-update --help works regardless of whether the feature is enabled.
let output = ctx.run_type().execute(&pixi).arg("--help").output_checked()?;
if String::from_utf8(output.stdout)?.contains("self-update") {
ctx.run_type()
.execute(&pixi)
.args(["self-update"])
.status_checked()
.ok();
}
ctx.run_type()
.execute(&pixi)
.args(["global", "update"])
.status_checked()
}
pub fn run_mamba_update(ctx: &ExecutionContext) -> Result<()> {
let mamba = require("mamba")?;
let output = Command::new(&mamba)
.args(["config", "--show", "auto_activate_base"])
.output_checked_utf8()?;
debug!("Mamba output: {}", output.stdout);
if output.stdout.contains("False") {
return Err(SkipStep("auto_activate_base is set to False".to_string()).into());
}
print_separator("Mamba");
let mut command = ctx.run_type().execute(mamba);
let mut command = ctx.run_type().execute(&mamba);
command.args(["update", "--all", "-n", "base"]);
if ctx.config().yes(Step::Mamba) {
command.arg("--yes");
}
command.status_checked()
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&mamba);
command.args(["clean", "--all"]);
if ctx.config().yes(Step::Mamba) {
command.arg("--yes");
}
command.status_checked()?;
}
Ok(())
}
pub fn run_miktex_packages_update(ctx: &ExecutionContext) -> Result<()> {
@@ -472,7 +621,7 @@ pub fn run_pip3_update(ctx: &ExecutionContext) -> Result<()> {
(Ok(py), _) => py,
(Err(_), Ok(py3)) => py3,
(Err(py_err), Err(py3_err)) => {
return Err(SkipStep(format!("Skip due to following reasons: {} {}", py_err, py3_err)).into());
return Err(SkipStep(format!("Skip due to following reasons: {py_err} {py3_err}")).into());
}
};
@@ -651,7 +800,7 @@ pub fn run_tlmgr_update(ctx: &ExecutionContext) -> Result<()> {
let mut command = if directory_writable {
ctx.run_type().execute(&tlmgr)
} else {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let mut c = ctx.run_type().execute(sudo);
c.arg(&tlmgr);
c
@@ -708,19 +857,22 @@ pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> {
let composer_home = Command::new(&composer)
.args(["global", "config", "--absolute", "--quiet", "home"])
.output_checked_utf8()
.map_err(|e| (SkipStep(format!("Error getting the composer directory: {e}"))))
.map_err(|e| (SkipStep(t!("Error getting the composer directory: {error}", error = e).to_string())))
.map(|s| PathBuf::from(s.stdout.trim()))?
.require()?;
if !composer_home.is_descendant_of(&HOME_DIR) {
return Err(SkipStep(format!(
"Composer directory {} isn't a descendant of the user's home directory",
composer_home.display()
))
return Err(SkipStep(
t!(
"Composer directory {composer_home} isn't a descendant of the user's home directory",
composer_home = composer_home.display()
)
.to_string(),
)
.into());
}
print_separator("Composer");
print_separator(t!("Composer"));
if ctx.config().composer_self_update() {
cfg_if::cfg_if! {
@@ -732,7 +884,7 @@ pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> {
};
if has_update {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
ctx.run_type()
.execute(sudo)
.arg(&composer)
@@ -776,9 +928,10 @@ pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> {
{
Ok(output) => output,
Err(_) => {
return Err(SkipStep(String::from(
"Error running `dotnet tool list`. This is expected when a dotnet runtime is installed but no SDK.",
))
return Err(SkipStep(
t!("Error running `dotnet tool list`. This is expected when a dotnet runtime is installed but no SDK.")
.to_string(),
)
.into());
}
};
@@ -806,7 +959,7 @@ pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> {
.peekable();
if packages.peek().is_none() {
return Err(SkipStep(String::from("No dotnet global tools installed")).into());
return Err(SkipStep(t!("No dotnet global tools installed").to_string()).into());
}
print_separator(".NET");
@@ -817,7 +970,7 @@ pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> {
.execute(&dotnet)
.args(["tool", "update", package_name, "--global"])
.status_checked()
.with_context(|| format!("Failed to update .NET package {package_name}"))?;
.with_context(|| format!("Failed to update .NET package {package_name:?}"))?;
}
Ok(())
@@ -846,7 +999,7 @@ pub fn run_helix_grammars(ctx: &ExecutionContext) -> Result<()> {
pub fn run_raco_update(ctx: &ExecutionContext) -> Result<()> {
let raco = require("raco")?;
print_separator("Racket Package Manager");
print_separator(t!("Racket Package Manager"));
ctx.run_type()
.execute(raco)
@@ -874,10 +1027,10 @@ pub fn run_ghcli_extensions_upgrade(ctx: &ExecutionContext) -> Result<()> {
let result = Command::new(&gh).args(["extensions", "list"]).output_checked_utf8();
if result.is_err() {
debug!("GH result {:?}", result);
return Err(SkipStep(String::from("GH failed")).into());
return Err(SkipStep(t!("GH failed").to_string()).into());
}
print_separator("GitHub CLI Extensions");
print_separator(t!("GitHub CLI Extensions"));
ctx.run_type()
.execute(&gh)
.args(["extension", "upgrade", "--all"])
@@ -887,12 +1040,17 @@ pub fn run_ghcli_extensions_upgrade(ctx: &ExecutionContext) -> Result<()> {
pub fn update_julia_packages(ctx: &ExecutionContext) -> Result<()> {
let julia = require("julia")?;
print_separator("Julia Packages");
print_separator(t!("Julia Packages"));
ctx.run_type()
.execute(julia)
.args(["-e", "using Pkg; Pkg.update()"])
.status_checked()
let mut executor = ctx.run_type().execute(julia);
executor.arg(if ctx.config().julia_use_startup_file() {
"--startup-file=yes"
} else {
"--startup-file=no"
});
executor.args(["-e", "using Pkg; Pkg.update()"]).status_checked()
}
pub fn run_helm_repo_update(ctx: &ExecutionContext) -> Result<()> {
@@ -904,7 +1062,7 @@ pub fn run_helm_repo_update(ctx: &ExecutionContext) -> Result<()> {
let mut success = true;
let mut exec = ctx.run_type().execute(helm);
if let Err(e) = exec.arg("repo").arg("update").status_checked() {
error!("Updating repositories failed: {}", e);
error!("Updating repositories failed: {e}");
success = match exec.output_checked_utf8() {
Ok(s) => s.stdout.contains(no_repo) || s.stderr.contains(no_repo),
Err(e) => match e.downcast_ref::<TopgradeError>() {
@@ -937,7 +1095,7 @@ pub fn run_bob(ctx: &ExecutionContext) -> Result<()> {
}
pub fn run_certbot(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let certbot = require("certbot")?;
print_separator("Certbot");
@@ -954,7 +1112,7 @@ pub fn run_certbot(ctx: &ExecutionContext) -> Result<()> {
/// doc: https://docs.clamav.net/manual/Usage/SignatureManagement.html#freshclam
pub fn run_freshclam(ctx: &ExecutionContext) -> Result<()> {
let freshclam = require("freshclam")?;
print_separator("Update ClamAV Database(FreshClam)");
print_separator(t!("Update ClamAV Database(FreshClam)"));
ctx.run_type().execute(freshclam).status_checked()
}
@@ -987,7 +1145,7 @@ pub fn run_lensfun_update_data(ctx: &ExecutionContext) -> Result<()> {
const EXIT_CODE_WHEN_NO_UPDATE: i32 = 1;
if ctx.config().lensfun_use_sudo() {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator(SEPARATOR);
ctx.run_type()
.execute(sudo)
@@ -1002,3 +1160,240 @@ pub fn run_lensfun_update_data(ctx: &ExecutionContext) -> Result<()> {
.status_checked_with_codes(&[EXIT_CODE_WHEN_NO_UPDATE])
}
}
pub fn run_poetry(ctx: &ExecutionContext) -> Result<()> {
let poetry = require("poetry")?;
#[cfg(unix)]
fn get_interpreter(poetry: &PathBuf) -> Result<(PathBuf, Option<OsString>)> {
// Parse the standard Unix shebang line: #!interpreter [optional-arg]
// Spaces and tabs on either side of interpreter are ignored.
use std::os::unix::ffi::OsStrExt;
lazy_static! {
static ref SHEBANG_REGEX: Regex = Regex::new(r"^#![ \t]*([^ \t\n]+)(?:[ \t]+([^\n]+)?)?").unwrap();
}
let script = fs::read(poetry)?;
if let Some(c) = SHEBANG_REGEX.captures(&script) {
let interpreter = OsStr::from_bytes(&c[1]).into();
let args = c.get(2).map(|args| OsStr::from_bytes(args.as_bytes()).into());
return Ok((interpreter, args));
}
Err(eyre!("Could not find shebang"))
}
#[cfg(windows)]
fn get_interpreter(poetry: &PathBuf) -> Result<(PathBuf, Option<OsString>)> {
// Parse the shebang line from scripts using https://bitbucket.org/vinay.sajip/simple_launcher,
// such as those created by pip. In contrast to Unix shebang lines, interpreter paths can
// contain spaces, if they are double-quoted.
use std::str;
lazy_static! {
static ref SHEBANG_REGEX: Regex =
Regex::new(r#"^#![ \t]*(?:"([^"\n]+)"|([^" \t\n]+))(?:[ \t]+([^\n]+)?)?"#).unwrap();
}
let data = fs::read(poetry)?;
let pos = match data.windows(4).rposition(|b| b == b"PK\x05\x06") {
Some(i) => i,
None => return Err(eyre!("Not a ZIP archive")),
};
let cdr_size = match data.get(pos + 12..pos + 16) {
Some(b) => u32::from_le_bytes(b.try_into().unwrap()) as usize,
None => return Err(eyre!("Invalid CDR size")),
};
let cdr_offset = match data.get(pos + 16..pos + 20) {
Some(b) => u32::from_le_bytes(b.try_into().unwrap()) as usize,
None => return Err(eyre!("Invalid CDR offset")),
};
if pos < cdr_size + cdr_offset {
return Err(eyre!("Invalid ZIP archive"));
}
let arc_pos = pos - cdr_size - cdr_offset;
match data[..arc_pos].windows(2).rposition(|b| b == b"#!") {
Some(l) => {
let line = &data[l..arc_pos - 1];
if let Some(c) = SHEBANG_REGEX.captures(line) {
let interpreter = c.get(1).or_else(|| c.get(2)).unwrap();
// shebang line should be valid utf8
let interpreter = str::from_utf8(interpreter.as_bytes())?.into();
let args = match c.get(3) {
Some(args) => Some(str::from_utf8(args.as_bytes())?.into()),
None => None,
};
Ok((interpreter, args))
} else {
Err(eyre!("Invalid shebang line"))
}
}
None => Err(eyre!("Could not find shebang")),
}
}
if ctx.config().poetry_force_self_update() {
debug!("forcing poetry self update");
} else {
let (interp, interp_args) = get_interpreter(&poetry)
.map_err(|e| SkipStep(format!("Could not find interpreter for {}: {}", poetry.display(), e)))?;
debug!("poetry interpreter: {:?}, args: {:?}", interp, interp_args);
let check_official_install_script =
"import sys; from os import path; print('Y') if path.isfile(path.join(sys.prefix, 'poetry_env')) else print('N')";
let mut command = Command::new(&interp);
if let Some(args) = interp_args {
command.arg(args);
}
let output = command
.args(["-c", check_official_install_script])
.output_checked_utf8()?;
let stdout = output.stdout.trim();
let official_install = match stdout {
"N" => false,
"Y" => true,
_ => unreachable!("unexpected output from `check_official_install_script`"),
};
debug!("poetry is official install: {}", official_install);
if !official_install {
return Err(SkipStep("Not installed with the official script".to_string()).into());
}
}
print_separator("Poetry");
ctx.run_type()
.execute(&poetry)
.args(["self", "update"])
.status_checked()
}
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
.run_type()
.execute(&uv_exec)
.args(["self", "--help"])
.output_checked();
if result.is_ok() {
ctx.run_type()
.execute(&uv_exec)
.args(["self", "update"])
.status_checked()?;
}
ctx.run_type()
.execute(&uv_exec)
.args(["tool", "upgrade", "--all"])
.status_checked()
}
/// Involve `zvm upgrade` to update ZVM
pub fn run_zvm(ctx: &ExecutionContext) -> Result<()> {
let zvm = require("zvm")?;
print_separator("ZVM");
ctx.run_type().execute(zvm).arg("upgrade").status_checked()
}
pub fn run_bun(ctx: &ExecutionContext) -> Result<()> {
let bun = require("bun")?;
print_separator("Bun");
ctx.run_type().execute(bun).arg("upgrade").status_checked()
}
pub fn run_zigup(ctx: &ExecutionContext) -> Result<()> {
let zigup = require("zigup")?;
let config = ctx.config();
print_separator("zigup");
let mut path_args = Vec::new();
if let Some(path) = config.zigup_path_link() {
path_args.push("--path-link".to_owned());
path_args.push(shellexpand::tilde(path).into_owned());
}
if let Some(path) = config.zigup_install_dir() {
path_args.push("--install-dir".to_owned());
path_args.push(shellexpand::tilde(path).into_owned());
}
for zig_version in config.zigup_target_versions() {
ctx.run_type()
.execute(&zigup)
.args(&path_args)
.arg("fetch")
.arg(&zig_version)
.status_checked()?;
if config.zigup_cleanup() {
ctx.run_type()
.execute(&zigup)
.args(&path_args)
.arg("keep")
.arg(&zig_version)
.status_checked()?;
}
}
if config.zigup_cleanup() {
ctx.run_type()
.execute(zigup)
.args(&path_args)
.arg("clean")
.status_checked()?;
}
Ok(())
}
pub fn run_jetbrains_toolbox(_ctx: &ExecutionContext) -> Result<()> {
let installation = find_jetbrains_toolbox();
match installation {
Err(FindError::NotFound) => {
// Skip
Err(SkipStep(format!("{}", t!("No JetBrains Toolbox installation found"))).into())
}
Err(FindError::UnsupportedOS(os)) => {
// Skip
Err(SkipStep(format!("{}", t!("Unsupported operating system {os}", os = os))).into())
}
Err(e) => {
// Unexpected error
println!(
"{}",
t!("jetbrains-toolbox-updater encountered an unexpected error during finding:")
);
println!("{e:?}");
Err(StepFailed.into())
}
Ok(installation) => {
print_separator("JetBrains Toolbox");
match update_jetbrains_toolbox(installation) {
Err(e) => {
// Unexpected error
println!(
"{}",
t!("jetbrains-toolbox-updater encountered an unexpected error during updating:")
);
println!("{e:?}");
Err(StepFailed.into())
}
Ok(()) => Ok(()),
}
}
}
}

View File

@@ -20,6 +20,7 @@ use crate::terminal::print_separator;
use crate::utils::{require, PathExt};
use crate::{error::SkipStep, terminal::print_warning, HOME_DIR};
use etcetera::base_strategy::BaseStrategy;
use rust_i18n::t;
#[cfg(unix)]
use crate::XDG_DIRS;
@@ -100,16 +101,18 @@ pub fn run_git_pull(ctx: &ExecutionContext) -> Result<()> {
// NOTE: this should be executed **before** skipping the Git step or the
// user won't receive this warning in the cases where all the paths configured
// are bad patterns.
repos
.bad_patterns
.iter()
.for_each(|pattern| print_warning(format!("Path {pattern} did not contain any git repositories")));
repos.bad_patterns.iter().for_each(|pattern| {
print_warning(t!(
"Path {pattern} did not contain any git repositories",
pattern = pattern
));
});
if repos.is_repos_empty() {
return Err(SkipStep(String::from("No repositories to pull")).into());
return Err(SkipStep(t!("No repositories to pull").to_string()).into());
}
print_separator("Git repositories");
print_separator(t!("Git repositories"));
repos.pull_repos(ctx)
}
@@ -143,7 +146,7 @@ fn get_head_revision<P: AsRef<Path>>(git: &Path, repo: P) -> Option<String> {
.output_checked_utf8()
.map(|output| output.stdout.trim().to_string())
.map_err(|e| {
error!("Error getting revision for {}: {}", repo.as_ref().display(), e);
error!("Error getting revision for {}: {e}", repo.as_ref().display(),);
e
})
@@ -204,10 +207,13 @@ impl RepoStep {
return output;
}
Err(e) => match e.kind() {
io::ErrorKind::NotFound => debug!("{} does not exist", path.as_ref().display()),
_ => error!("Error looking for {}: {}", path.as_ref().display(), e),
},
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
debug!("{} does not exist", path.as_ref().display());
} else {
error!("Error looking for {}: {e}", path.as_ref().display());
}
}
}
None
@@ -236,7 +242,7 @@ impl RepoStep {
res.map(|output| output.stdout.lines().count() > 0)
.map_err(|e| {
error!("Error getting remotes for {}: {}", repo.as_ref().display(), e);
error!("Error getting remotes for {}: {e}", repo.as_ref().display());
e
})
.ok()
@@ -264,7 +270,7 @@ impl RepoStep {
}
}
Err(e) => {
error!("Error in path {}", e);
error!("Error in path {e}");
}
}
}
@@ -273,7 +279,7 @@ impl RepoStep {
self.bad_patterns.push(String::from(pattern));
}
} else {
error!("Bad glob pattern: {}", pattern);
error!("Bad glob pattern: {pattern}");
}
}
@@ -296,7 +302,7 @@ impl RepoStep {
let before_revision = get_head_revision(&self.git, &repo);
if ctx.config().verbose() {
println!("{} {}", style("Pulling").cyan().bold(), repo.as_ref().display());
println!("{} {}", style(t!("Pulling")).cyan().bold(), repo.as_ref().display());
}
let mut command = AsyncCommand::new(&self.git);
@@ -318,17 +324,22 @@ impl RepoStep {
.output()
.await?;
let result = output_checked_utf8(pull_output)
.and_then(|_| output_checked_utf8(submodule_output))
.and_then(|()| output_checked_utf8(submodule_output))
.wrap_err_with(|| format!("Failed to pull {}", repo.as_ref().display()));
if result.is_err() {
println!("{} pulling {}", style("Failed").red().bold(), repo.as_ref().display());
println!(
"{} {} {}",
style(t!("Failed")).red().bold(),
t!("pulling"),
repo.as_ref().display()
);
} else {
let after_revision = get_head_revision(&self.git, repo.as_ref());
match (&before_revision, &after_revision) {
(Some(before), Some(after)) if before != after => {
println!("{} {}", style("Changed").yellow().bold(), repo.as_ref().display());
println!("{} {}", style(t!("Changed")).yellow().bold(), repo.as_ref().display());
Command::new(&self.git)
.stdin(Stdio::null())
@@ -345,13 +356,13 @@ impl RepoStep {
}
_ => {
if ctx.config().verbose() {
println!("{} {}", style("Up-to-date").green().bold(), repo.as_ref().display());
println!("{} {}", style(t!("Up-to-date")).green().bold(), repo.as_ref().display());
}
}
}
}
result.map(|_| ())
result
}
/// Pull the repositories specified in `self.repos`.
@@ -363,15 +374,16 @@ impl RepoStep {
if ctx.run_type().dry() {
self.repos
.iter()
.for_each(|repo| println!("Would pull {}", repo.display()));
.for_each(|repo| println!("{}", t!("Would pull {repo}", repo = repo.display())));
return Ok(());
}
if !ctx.config().verbose() {
println!(
"\n{} updated repositories will be shown...\n",
style("Only").green().bold()
"\n{} {}\n",
style(t!("Only")).green().bold(),
t!("updated repositories will be shown...")
);
}
@@ -381,9 +393,10 @@ impl RepoStep {
.filter(|repo| match self.has_remotes(repo) {
Some(false) => {
println!(
"{} {} because it has no remotes",
style("Skipping").yellow().bold(),
repo.display()
"{} {} {}",
style(t!("Skipping")).yellow().bold(),
repo.display(),
t!("because it has no remotes")
);
false
}
@@ -400,7 +413,7 @@ impl RepoStep {
let basic_rt = runtime::Runtime::new()?;
let results = basic_rt.block_on(async { stream_of_futures.collect::<Vec<Result<()>>>().await });
let error = results.into_iter().find(|r| r.is_err());
let error = results.into_iter().find(std::result::Result::is_err);
error.unwrap_or(Ok(()))
}
}

View File

@@ -1,6 +1,7 @@
use crate::terminal::print_separator;
use crate::utils::require;
use color_eyre::eyre::Result;
use rust_i18n::t;
use crate::execution_context::ExecutionContext;
@@ -17,7 +18,7 @@ pub fn upgrade_kak_plug(ctx: &ExecutionContext) -> Result<()> {
.args(["-ui", "dummy", "-e", UPGRADE_KAK])
.output()?;
println!("Plugins upgraded");
println!("{}", t!("Plugins upgraded"));
Ok(())
}

View File

@@ -4,16 +4,17 @@ use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::process::Command;
use crate::utils::{require_option, REQUIRE_SUDO};
use crate::utils::{get_require_sudo_string, require_option};
use crate::HOME_DIR;
use color_eyre::eyre::Result;
#[cfg(target_os = "linux")]
use nix::unistd::Uid;
use rust_i18n::t;
use semver::Version;
use tracing::debug;
use crate::command::CommandExt;
use crate::terminal::print_separator;
use crate::terminal::{print_info, print_separator};
use crate::utils::{require, PathExt};
use crate::{error::SkipStep, execution_context::ExecutionContext};
@@ -86,13 +87,13 @@ impl NPM {
.args(["--version"])
.output_checked_utf8()
.map(|s| s.stdout.trim().to_owned());
Version::parse(&version_str?).map_err(|err| err.into())
Version::parse(&version_str?).map_err(std::convert::Into::into)
}
fn upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> {
let args = ["update", self.global_location_arg()];
if use_sudo {
let sudo = require_option(ctx.sudo().clone(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().clone(), get_require_sudo_string())?;
ctx.run_type()
.execute(sudo)
.arg(&self.command)
@@ -156,7 +157,7 @@ impl Yarn {
let args = ["global", "upgrade"];
if use_sudo {
let sudo = require_option(ctx.sudo().clone(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().clone(), get_require_sudo_string())?;
ctx.run_type()
.execute(sudo)
.arg(self.yarn.as_ref().unwrap_or(&self.command))
@@ -183,6 +184,92 @@ impl Yarn {
}
}
struct Deno {
command: PathBuf,
}
impl Deno {
fn new(command: PathBuf) -> Self {
Self { command }
}
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let mut args = vec![];
let version = ctx.config().deno_version();
if let Some(version) = version {
let bin_version = self.version()?;
if bin_version >= Version::new(2, 0, 0) {
args.push(version);
} else if bin_version >= Version::new(1, 6, 0) {
match version {
"stable" => { /* do nothing, as stable is the default channel to upgrade */ }
"rc" => {
return Err(SkipStep(
"Deno (1.6.0-2.0.0) cannot be upgraded to a release candidate".to_string(),
)
.into());
}
"canary" => args.push("--canary"),
_ => {
if Version::parse(version).is_err() {
return Err(SkipStep("Invalid Deno version".to_string()).into());
}
args.push("--version");
args.push(version);
}
}
} else if bin_version >= Version::new(1, 0, 0) {
match version {
"stable" | "rc" | "canary" => {
// Prior to v1.6.0, `deno upgrade` is not able fetch the latest tag version.
return Err(
SkipStep("Deno (1.0.0-1.6.0) cannot be upgraded to a named channel".to_string()).into(),
);
}
_ => {
if Version::parse(version).is_err() {
return Err(SkipStep("Invalid Deno version".to_string()).into());
}
args.push("--version");
args.push(version);
}
}
} else {
// v0.x cannot be upgraded with `deno upgrade` to v1.x or v2.x
// nor can be upgraded to a specific version.
return Err(SkipStep("Unsupported Deno version".to_string()).into());
}
}
ctx.run_type()
.execute(&self.command)
.arg("upgrade")
.args(args)
.status_checked()?;
Ok(())
}
/// Get the version of Deno.
///
/// This function will return the version of Deno installed on the system.
/// The version is parsed from the output of `deno -V`.
///
/// ```sh
/// deno -V # deno 1.6.0
/// ```
fn version(&self) -> Result<Version> {
let version_str = Command::new(&self.command)
.args(["-V"])
.output_checked_utf8()
.map(|s| s.stdout.trim().to_owned().split_off(5)); // remove "deno " prefix
Version::parse(&version_str?).map_err(std::convert::Into::into)
}
}
#[cfg(target_os = "linux")]
fn should_use_sudo(npm: &NPM, ctx: &ExecutionContext) -> Result<bool> {
if npm.should_use_sudo()? {
@@ -214,7 +301,7 @@ fn should_use_sudo_yarn(yarn: &Yarn, ctx: &ExecutionContext) -> Result<bool> {
pub fn run_npm_upgrade(ctx: &ExecutionContext) -> Result<()> {
let npm = require("npm").map(|b| NPM::new(b, NPMVariant::Npm))?;
print_separator("Node Package Manager");
print_separator(t!("Node Package Manager"));
#[cfg(target_os = "linux")]
{
@@ -230,7 +317,7 @@ pub fn run_npm_upgrade(ctx: &ExecutionContext) -> Result<()> {
pub fn run_pnpm_upgrade(ctx: &ExecutionContext) -> Result<()> {
let pnpm = require("pnpm").map(|b| NPM::new(b, NPMVariant::Pnpm))?;
print_separator("Performant Node Package Manager");
print_separator(t!("Performant Node Package Manager"));
#[cfg(target_os = "linux")]
{
@@ -251,7 +338,7 @@ pub fn run_yarn_upgrade(ctx: &ExecutionContext) -> Result<()> {
return Ok(());
}
print_separator("Yarn Package Manager");
print_separator(t!("Yarn Package Manager"));
#[cfg(target_os = "linux")]
{
@@ -265,14 +352,59 @@ pub fn run_yarn_upgrade(ctx: &ExecutionContext) -> Result<()> {
}
pub fn deno_upgrade(ctx: &ExecutionContext) -> Result<()> {
let deno = require("deno")?;
let deno = require("deno").map(Deno::new)?;
let deno_dir = HOME_DIR.join(".deno");
if !deno.canonicalize()?.is_descendant_of(&deno_dir) {
let skip_reason = SkipStep("Deno installed outside of .deno directory".to_string());
if !deno.command.canonicalize()?.is_descendant_of(&deno_dir) {
let skip_reason = SkipStep(t!("Deno installed outside of .deno directory").to_string());
return Err(skip_reason.into());
}
print_separator("Deno");
ctx.run_type().execute(&deno).arg("upgrade").status_checked()
deno.upgrade(ctx)
}
/// There is no `volta upgrade` command, so we need to upgrade each package
pub fn run_volta_packages_upgrade(ctx: &ExecutionContext) -> Result<()> {
let volta = require("volta")?;
print_separator("Volta");
if ctx.run_type().dry() {
print_info(t!("Updating Volta packages..."));
return Ok(());
}
let list_output = ctx
.run_type()
.execute(&volta)
.args(["list", "--format=plain"])
.output_checked_utf8()?
.stdout;
let installed_packages: Vec<&str> = list_output
.lines()
.filter_map(|line| {
// format is 'kind package@version ...'
let mut parts = line.split_whitespace();
parts.next();
let package_part = parts.next()?;
let version_index = package_part.rfind('@').unwrap_or(package_part.len());
Some(package_part[..version_index].trim())
})
.collect();
if installed_packages.is_empty() {
print_info(t!("No packages installed with Volta"));
return Ok(());
}
for package in &installed_packages {
ctx.run_type()
.execute(&volta)
.args(["install", package])
.status_checked()?;
}
Ok(())
}

View File

@@ -4,12 +4,13 @@ use std::path::{Path, PathBuf};
use color_eyre::eyre;
use color_eyre::eyre::Result;
use rust_i18n::t;
use walkdir::WalkDir;
use crate::command::CommandExt;
use crate::error::TopgradeError;
use crate::execution_context::ExecutionContext;
use crate::sudo::Sudo;
use crate::utils::require_option;
use crate::utils::which;
use crate::{config, Step};
@@ -144,13 +145,13 @@ impl Trizen {
}
pub struct Pacman {
sudo: Sudo,
executable: PathBuf,
}
impl ArchPackageManager for Pacman {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let mut command = ctx.run_type().execute(&self.sudo);
let sudo = require_option(ctx.sudo().as_ref(), "sudo is required to run pacman".into())?;
let mut command = ctx.run_type().execute(sudo);
command
.arg(&self.executable)
.arg("-Syu")
@@ -161,7 +162,7 @@ impl ArchPackageManager for Pacman {
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.sudo);
let mut command = ctx.run_type().execute(sudo);
command.arg(&self.executable).arg("-Scc");
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
@@ -174,10 +175,9 @@ impl ArchPackageManager for Pacman {
}
impl Pacman {
pub fn get(ctx: &ExecutionContext) -> Option<Self> {
pub fn get() -> Option<Self> {
Some(Self {
executable: which("powerpill").unwrap_or_else(|| PathBuf::from("pacman")),
sudo: ctx.sudo().to_owned()?,
})
}
}
@@ -263,47 +263,76 @@ impl ArchPackageManager for Pamac {
pub struct Aura {
executable: PathBuf,
sudo: Sudo,
}
impl Aura {
fn get(ctx: &ExecutionContext) -> Option<Self> {
fn get() -> Option<Self> {
Some(Self {
executable: which("aura")?,
sudo: ctx.sudo().to_owned()?,
})
}
}
impl ArchPackageManager for Aura {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let sudo = which("sudo").unwrap_or_default();
let mut aur_update = ctx.run_type().execute(&sudo);
use semver::Version;
if sudo.ends_with("sudo") {
aur_update
.arg(&self.executable)
let version_cmd_output = ctx
.run_type()
.execute(&self.executable)
.arg("--version")
.output_checked_utf8()?;
// 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");
// Aura, since version 4.0.6, no longer needs sudo.
//
// https://github.com/fosskers/aura/releases/tag/v4.0.6
let version_no_sudo = Version::new(4, 0, 6);
if version >= version_no_sudo {
let mut cmd = ctx.run_type().execute(&self.executable);
cmd.arg("-Au")
.args(ctx.config().aura_aur_arguments().split_whitespace());
if ctx.config().yes(Step::System) {
cmd.arg("--noconfirm");
}
cmd.status_checked()?;
let mut cmd = ctx.run_type().execute(&self.executable);
cmd.arg("-Syu")
.args(ctx.config().aura_pacman_arguments().split_whitespace());
if ctx.config().yes(Step::System) {
cmd.arg("--noconfirm");
}
cmd.status_checked()?;
} else {
let sudo = crate::utils::require_option(
ctx.sudo().as_ref(),
t!("Aura(<0.4.6) requires sudo installed to work with AUR packages").to_string(),
)?;
let mut cmd = ctx.run_type().execute(sudo);
cmd.arg(&self.executable)
.arg("-Au")
.args(ctx.config().aura_aur_arguments().split_whitespace());
if ctx.config().yes(Step::System) {
aur_update.arg("--noconfirm");
cmd.arg("--noconfirm");
}
cmd.status_checked()?;
aur_update.status_checked()?;
} else {
println!("Aura requires sudo installed to work with AUR packages")
let mut cmd = ctx.run_type().execute(sudo);
cmd.arg(&self.executable)
.arg("-Syu")
.args(ctx.config().aura_pacman_arguments().split_whitespace());
if ctx.config().yes(Step::System) {
cmd.arg("--noconfirm");
}
cmd.status_checked()?;
}
let mut pacman_update = ctx.run_type().execute(&self.sudo);
pacman_update
.arg(&self.executable)
.arg("-Syu")
.args(ctx.config().aura_pacman_arguments().split_whitespace());
if ctx.config().yes(Step::System) {
pacman_update.arg("--noconfirm");
}
pacman_update.status_checked()?;
Ok(())
}
}
@@ -323,16 +352,16 @@ pub fn get_arch_package_manager(ctx: &ExecutionContext) -> Option<Box<dyn ArchPa
.or_else(|| Trizen::get().map(box_package_manager))
.or_else(|| Pikaur::get().map(box_package_manager))
.or_else(|| Pamac::get().map(box_package_manager))
.or_else(|| Pacman::get(ctx).map(box_package_manager))
.or_else(|| Aura::get(ctx).map(box_package_manager)),
.or_else(|| Pacman::get().map(box_package_manager))
.or_else(|| Aura::get().map(box_package_manager)),
config::ArchPackageManager::GarudaUpdate => GarudaUpdate::get().map(box_package_manager),
config::ArchPackageManager::Trizen => Trizen::get().map(box_package_manager),
config::ArchPackageManager::Paru => YayParu::get("paru", &pacman).map(box_package_manager),
config::ArchPackageManager::Yay => YayParu::get("yay", &pacman).map(box_package_manager),
config::ArchPackageManager::Pacman => Pacman::get(ctx).map(box_package_manager),
config::ArchPackageManager::Pacman => Pacman::get().map(box_package_manager),
config::ArchPackageManager::Pikaur => Pikaur::get().map(box_package_manager),
config::ArchPackageManager::Pamac => Pamac::get().map(box_package_manager),
config::ArchPackageManager::Aura => Aura::get(ctx).map(box_package_manager),
config::ArchPackageManager::Aura => Aura::get().map(box_package_manager),
}
}
@@ -355,7 +384,7 @@ pub fn show_pacnew() {
.peekable();
if iter.peek().is_some() {
println!("\nPacman backup configuration files found:");
println!("\n{}", t!("Pacman backup configuration files found:"));
for entry in iter {
println!("{}", entry.path().display());

View File

@@ -1,14 +1,14 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::terminal::print_separator;
use crate::utils::{require_option, REQUIRE_SUDO};
use crate::utils::{get_require_sudo_string, require_option};
use crate::Step;
use color_eyre::eyre::Result;
use std::process::Command;
pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("DragonFly BSD Packages");
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator(t!("DragonFly BSD Packages"));
let mut cmd = ctx.run_type().execute(sudo);
cmd.args(["/usr/local/sbin/pkg", "upgrade"]);
if ctx.config().yes(Step::System) {
@@ -18,9 +18,9 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
}
pub fn audit_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator("DragonFly BSD Audit");
print_separator(t!("DragonFly BSD Audit"));
#[allow(clippy::disallowed_methods)]
if !Command::new(sudo)
@@ -28,7 +28,9 @@ pub fn audit_packages(ctx: &ExecutionContext) -> Result<()> {
.status()?
.success()
{
println!("The package audit was successful, but vulnerable packages still remain on the system");
println!(t!(
"The package audit was successful, but vulnerable packages still remain on the system"
));
}
Ok(())
}

View File

@@ -1,14 +1,15 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::terminal::print_separator;
use crate::utils::{require_option, REQUIRE_SUDO};
use crate::utils::{get_require_sudo_string, require_option};
use crate::Step;
use color_eyre::eyre::Result;
use rust_i18n::t;
use std::process::Command;
pub fn upgrade_freebsd(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("FreeBSD Update");
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator(t!("FreeBSD Update"));
ctx.run_type()
.execute(sudo)
.args(["/usr/sbin/freebsd-update", "fetch", "install"])
@@ -16,8 +17,8 @@ pub fn upgrade_freebsd(ctx: &ExecutionContext) -> Result<()> {
}
pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("FreeBSD Packages");
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator(t!("FreeBSD Packages"));
let mut command = ctx.run_type().execute(sudo);
@@ -29,9 +30,9 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
}
pub fn audit_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator("FreeBSD Audit");
print_separator(t!("FreeBSD Audit"));
Command::new(sudo)
.args(["/usr/sbin/pkg", "audit", "-Fr"])

View File

@@ -3,6 +3,7 @@ use std::process::Command;
use color_eyre::eyre::Result;
use ini::Ini;
use rust_i18n::t;
use tracing::{debug, warn};
use crate::command::CommandExt;
@@ -11,7 +12,7 @@ use crate::execution_context::ExecutionContext;
use crate::steps::generic::is_wsl;
use crate::steps::os::archlinux;
use crate::terminal::{print_separator, prompt_yesno};
use crate::utils::{require, require_option, which, PathExt, REQUIRE_SUDO};
use crate::utils::{get_require_sudo_string, require, require_option, which, PathExt};
use crate::{Step, HOME_DIR};
static OS_RELEASE_PATH: &str = "/etc/os-release";
@@ -30,6 +31,7 @@ pub enum Distribution {
FedoraImmutable,
Debian,
Gentoo,
NILRT,
OpenMandriva,
OpenSuseTumbleweed,
PCLinuxOS,
@@ -58,25 +60,14 @@ impl Distribution {
Some("wolfi") => Distribution::Wolfi,
Some("centos") | Some("rhel") | Some("ol") => Distribution::CentOS,
Some("clear-linux-os") => Distribution::ClearLinux,
Some("fedora") => {
return if let Some(variant) = variant {
match variant {
"Silverblue" | "Kinoite" | "Sericea" | "Onyx" | "IoT Edition" | "Sway Atomic" => {
Ok(Distribution::FedoraImmutable)
}
_ => Ok(Distribution::Fedora),
}
} else {
Ok(Distribution::Fedora)
};
}
Some("fedora") => Distribution::match_fedora_variant(&variant),
Some("nilrt") => Distribution::NILRT,
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("solus") => Distribution::Solus,
Some("gentoo") => Distribution::Gentoo,
Some("gentoo") | Some("funtoo") => Distribution::Gentoo,
Some("exherbo") => Distribution::Exherbo,
Some("nixos") => Distribution::NixOS,
Some("opensuse-microos") => Distribution::SuseMicro,
@@ -106,7 +97,7 @@ impl Distribution {
} else if id_like.contains(&"alpine") {
return Ok(Distribution::Alpine);
} else if id_like.contains(&"fedora") {
return Ok(Distribution::Fedora);
return Ok(Distribution::match_fedora_variant(&variant));
}
}
return Err(TopgradeError::UnknownLinuxDistribution.into());
@@ -114,6 +105,15 @@ impl Distribution {
})
}
fn match_fedora_variant(variant: &Option<&str>) -> Self {
if let Some("Silverblue" | "Kinoite" | "Sericea" | "Onyx" | "IoT Edition" | "Sway Atomic" | "CoreOS") = variant
{
Distribution::FedoraImmutable
} else {
Distribution::Fedora
}
}
pub fn detect() -> Result<Self> {
if PathBuf::from("/bedrock").exists() {
return Ok(Distribution::Bedrock);
@@ -133,7 +133,7 @@ impl Distribution {
}
pub fn upgrade(self, ctx: &ExecutionContext) -> Result<()> {
print_separator("System update");
print_separator(t!("System update"));
match self {
Distribution::Alpine => upgrade_alpine_linux(ctx),
@@ -158,6 +158,7 @@ impl Distribution {
Distribution::OpenMandriva => upgrade_openmandriva(ctx),
Distribution::PCLinuxOS => upgrade_pclinuxos(ctx),
Distribution::Nobara => upgrade_nobara(ctx),
Distribution::NILRT => upgrade_nilrt(ctx),
}
}
@@ -173,7 +174,7 @@ impl Distribution {
}
fn update_bedrock(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
ctx.run_type().execute(sudo).args(["brl", "update"]);
@@ -198,7 +199,7 @@ fn update_bedrock(ctx: &ExecutionContext) -> Result<()> {
fn upgrade_alpine_linux(ctx: &ExecutionContext) -> Result<()> {
let apk = require("apk")?;
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
ctx.run_type().execute(sudo).arg(&apk).arg("update").status_checked()?;
ctx.run_type().execute(sudo).arg(&apk).arg("upgrade").status_checked()
@@ -206,7 +207,7 @@ fn upgrade_alpine_linux(ctx: &ExecutionContext) -> Result<()> {
fn upgrade_chimera_linux(ctx: &ExecutionContext) -> Result<()> {
let apk = require("apk")?;
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
ctx.run_type().execute(sudo).arg(&apk).arg("update").status_checked()?;
ctx.run_type().execute(sudo).arg(&apk).arg("upgrade").status_checked()
@@ -214,13 +215,20 @@ fn upgrade_chimera_linux(ctx: &ExecutionContext) -> Result<()> {
fn upgrade_wolfi_linux(ctx: &ExecutionContext) -> Result<()> {
let apk = require("apk")?;
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
ctx.run_type().execute(sudo).arg(&apk).arg("update").status_checked()?;
ctx.run_type().execute(sudo).arg(&apk).arg("upgrade").status_checked()
}
fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> {
if let Some(bootc) = which("bootc") {
if ctx.config().bootc() {
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
return ctx.run_type().execute(sudo).arg(&bootc).arg("upgrade").status_checked();
}
}
if let Some(ostree) = which("rpm-ostree") {
if ctx.config().rpm_ostree() {
let mut command = ctx.run_type().execute(ostree);
@@ -229,7 +237,7 @@ fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> {
}
};
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let mut command = ctx.run_type().execute(sudo);
command
.arg(which("dnf").unwrap_or_else(|| Path::new("yum").to_path_buf()))
@@ -252,7 +260,7 @@ fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> {
}
fn upgrade_nobara(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let pkg_manager = require("dnf")?;
let mut update_command = ctx.run_type().execute(sudo);
@@ -285,7 +293,22 @@ fn upgrade_nobara(ctx: &ExecutionContext) -> Result<()> {
Ok(())
}
fn upgrade_nilrt(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let opkg = require("opkg")?;
ctx.run_type().execute(sudo).arg(&opkg).arg("update").status_checked()?;
ctx.run_type().execute(sudo).arg(&opkg).arg("upgrade").status_checked()
}
fn upgrade_fedora_immutable(ctx: &ExecutionContext) -> Result<()> {
if let Some(bootc) = which("bootc") {
if ctx.config().bootc() {
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
return ctx.run_type().execute(sudo).arg(&bootc).arg("upgrade").status_checked();
}
}
let ostree = require("rpm-ostree")?;
let mut command = ctx.run_type().execute(ostree);
command.arg("upgrade");
@@ -294,14 +317,14 @@ fn upgrade_fedora_immutable(ctx: &ExecutionContext) -> Result<()> {
}
fn upgrade_bedrock_strata(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
ctx.run_type().execute(sudo).args(["brl", "update"]).status_checked()?;
Ok(())
}
fn upgrade_suse(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
ctx.run_type()
.execute(sudo)
.args(["zypper", "refresh"])
@@ -324,7 +347,7 @@ fn upgrade_suse(ctx: &ExecutionContext) -> Result<()> {
}
fn upgrade_opensuse_tumbleweed(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
ctx.run_type()
.execute(sudo)
.args(["zypper", "refresh"])
@@ -342,7 +365,7 @@ fn upgrade_opensuse_tumbleweed(ctx: &ExecutionContext) -> Result<()> {
}
fn upgrade_suse_micro(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let mut cmd = ctx.run_type().execute(sudo);
cmd.arg("transactional-update");
if ctx.config().yes(Step::System) {
@@ -355,10 +378,10 @@ fn upgrade_suse_micro(ctx: &ExecutionContext) -> Result<()> {
}
fn upgrade_openmandriva(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let mut command = ctx.run_type().execute(sudo);
command.arg(&which("dnf").unwrap()).arg("upgrade");
command.arg(which("dnf").unwrap()).arg("upgrade");
if let Some(args) = ctx.config().dnf_arguments() {
command.args(args.split_whitespace());
@@ -374,10 +397,10 @@ fn upgrade_openmandriva(ctx: &ExecutionContext) -> Result<()> {
}
fn upgrade_pclinuxos(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let mut command_update = ctx.run_type().execute(sudo);
command_update.arg(&which("apt-get").unwrap()).arg("update");
command_update.arg(which("apt-get").unwrap()).arg("update");
if let Some(args) = ctx.config().dnf_arguments() {
command_update.args(args.split_whitespace());
@@ -390,7 +413,7 @@ fn upgrade_pclinuxos(ctx: &ExecutionContext) -> Result<()> {
command_update.status_checked()?;
let mut cmd = ctx.run_type().execute(sudo);
cmd.arg(&which("apt-get").unwrap());
cmd.arg(which("apt-get").unwrap());
cmd.arg("dist-upgrade");
if ctx.config().yes(Step::System) {
cmd.arg("-y");
@@ -421,7 +444,7 @@ fn upgrade_vanilla(ctx: &ExecutionContext) -> Result<()> {
}
fn upgrade_void(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let mut command = ctx.run_type().execute(sudo);
command.args(["xbps-install", "-Su", "xbps"]);
if ctx.config().yes(Step::System) {
@@ -442,7 +465,7 @@ fn upgrade_void(ctx: &ExecutionContext) -> Result<()> {
fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> {
let run_type = ctx.run_type();
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
if let Some(layman) = which("layman") {
run_type
.execute(sudo)
@@ -451,17 +474,22 @@ fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> {
.status_checked()?;
}
println!("Syncing portage");
run_type
.execute(sudo)
.args(["emerge", "--sync"])
.args(
ctx.config()
.emerge_sync_flags()
.map(|s| s.split_whitespace().collect())
.unwrap_or_else(|| vec!["-q"]),
)
.status_checked()?;
println!("{}", t!("Syncing portage"));
if let Some(ego) = which("ego") {
// The Funtoo team doesn't reccomend running both ego sync and emerge --sync
run_type.execute(sudo).arg(ego).arg("sync").status_checked()?;
} else {
run_type
.execute(sudo)
.args(["emerge", "--sync"])
.args(
ctx.config()
.emerge_sync_flags()
.map(|s| s.split_whitespace().collect())
.unwrap_or_else(|| vec!["-q"]),
)
.status_checked()?;
}
if let Some(eix_update) = which("eix-update") {
run_type.execute(sudo).arg(eix_update).status_checked()?;
@@ -512,7 +540,7 @@ fn upgrade_debian(ctx: &ExecutionContext) -> Result<()> {
return Ok(());
}
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
if !is_nala {
ctx.run_type()
.execute(sudo)
@@ -566,7 +594,7 @@ pub fn run_deb_get(ctx: &ExecutionContext) -> Result<()> {
}
fn upgrade_solus(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let mut cmd = ctx.run_type().execute(sudo);
cmd.arg("eopkg");
if ctx.config().yes(Step::System) {
@@ -675,7 +703,7 @@ pub fn run_packer_nu(ctx: &ExecutionContext) -> Result<()> {
}
fn upgrade_clearlinux(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let mut cmd = ctx.run_type().execute(sudo);
cmd.args(["swupd", "update"]);
if ctx.config().yes(Step::System) {
@@ -687,7 +715,7 @@ fn upgrade_clearlinux(ctx: &ExecutionContext) -> Result<()> {
}
fn upgrade_exherbo(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
ctx.run_type().execute(sudo).args(["cave", "sync"]).status_checked()?;
ctx.run_type()
@@ -716,7 +744,7 @@ fn upgrade_exherbo(ctx: &ExecutionContext) -> Result<()> {
}
fn upgrade_nixos(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let mut command = ctx.run_type().execute(sudo);
command.args(["/run/current-system/sw/bin/nixos-rebuild", "switch", "--upgrade"]);
@@ -742,7 +770,7 @@ fn upgrade_neon(ctx: &ExecutionContext) -> Result<()> {
// seems rare
// if that comes up we need to create a Distribution::PackageKit or some such
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let pkcon = which("pkcon").unwrap();
// pkcon ignores update with update and refresh provided together
ctx.run_type()
@@ -771,7 +799,7 @@ fn upgrade_neon(ctx: &ExecutionContext) -> Result<()> {
/// alternative
fn should_skip_needrestart() -> Result<()> {
let distribution = Distribution::detect()?;
let msg = "needrestart will be ran by the package manager";
let msg = t!("needrestart will be ran by the package manager");
if distribution.redhat_based() {
return Err(SkipStep(String::from(msg)).into());
@@ -806,12 +834,12 @@ fn should_skip_needrestart() -> Result<()> {
}
pub fn run_needrestart(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let needrestart = require("needrestart")?;
should_skip_needrestart()?;
print_separator("Check for needed restarts");
print_separator(t!("Check for needed restarts"));
ctx.run_type().execute(sudo).arg(needrestart).status_checked()?;
@@ -822,10 +850,10 @@ pub fn run_fwupdmgr(ctx: &ExecutionContext) -> Result<()> {
let fwupdmgr = require("fwupdmgr")?;
if is_wsl()? {
return Err(SkipStep(String::from("Should not run in WSL")).into());
return Err(SkipStep(t!("Should not run in WSL").to_string()).into());
}
print_separator("Firmware upgrades");
print_separator(t!("Firmware upgrades"));
ctx.run_type()
.execute(&fwupdmgr)
@@ -847,7 +875,7 @@ pub fn run_fwupdmgr(ctx: &ExecutionContext) -> Result<()> {
pub fn run_flatpak(ctx: &ExecutionContext) -> Result<()> {
let flatpak = require("flatpak")?;
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let cleanup = ctx.config().cleanup();
let yes = ctx.config().yes(Step::Flatpak);
let run_type = ctx.run_type();
@@ -867,7 +895,7 @@ pub fn run_flatpak(ctx: &ExecutionContext) -> Result<()> {
run_type.execute(&flatpak).args(&cleanup_args).status_checked()?;
}
print_separator("Flatpak System Packages");
print_separator(t!("Flatpak System Packages"));
if ctx.config().flatpak_use_sudo() || std::env::var("SSH_CLIENT").is_ok() {
let mut update_args = vec!["update", "--system"];
if yes {
@@ -908,11 +936,11 @@ pub fn run_flatpak(ctx: &ExecutionContext) -> Result<()> {
}
pub fn run_snap(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let snap = require("snap")?;
if !PathBuf::from("/var/snapd.socket").exists() && !PathBuf::from("/run/snapd.socket").exists() {
return Err(SkipStep(String::from("Snapd socket does not exist")).into());
return Err(SkipStep(t!("Snapd socket does not exist").to_string()).into());
}
print_separator("snap");
@@ -920,7 +948,7 @@ pub fn run_snap(ctx: &ExecutionContext) -> Result<()> {
}
pub fn run_pihole_update(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let pihole = require("pihole")?;
Path::new("/opt/pihole/update.sh").require()?;
@@ -954,7 +982,7 @@ pub fn run_distrobox_update(ctx: &ExecutionContext) -> Result<()> {
) {
(r, Some(c)) => {
if c.is_empty() {
return Err(SkipStep("You need to specify at least one container".to_string()).into());
return Err(SkipStep(t!("You need to specify at least one container").to_string()).into());
}
r.args(c)
}
@@ -969,7 +997,7 @@ pub fn run_distrobox_update(ctx: &ExecutionContext) -> Result<()> {
}
pub fn run_dkp_pacman_update(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let dkp_pacman = require("dkp-pacman")?;
print_separator("Devkitpro pacman");
@@ -992,20 +1020,20 @@ pub fn run_dkp_pacman_update(ctx: &ExecutionContext) -> Result<()> {
}
pub fn run_config_update(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
if ctx.config().yes(Step::ConfigUpdate) {
return Err(SkipStep("Skipped in --yes".to_string()).into());
return Err(SkipStep(t!("Skipped in --yes").to_string()).into());
}
if let Ok(etc_update) = require("etc-update") {
print_separator("Configuration update");
print_separator(t!("Configuration update"));
ctx.run_type().execute(sudo).arg(etc_update).status_checked()?;
} else if let Ok(pacdiff) = require("pacdiff") {
if std::env::var("DIFFPROG").is_err() {
require("vim")?;
}
print_separator("Configuration update");
print_separator(t!("Configuration update"));
ctx.execute_elevated(&pacdiff, false)?.status_checked()?;
}
@@ -1029,7 +1057,7 @@ pub fn run_lure_update(ctx: &ExecutionContext) -> Result<()> {
}
pub fn run_waydroid(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let waydroid = require("waydroid")?;
let status = ctx.run_type().execute(&waydroid).arg("status").output_checked_utf8()?;
// example output of `waydroid status`:
@@ -1053,17 +1081,20 @@ pub fn run_waydroid(ctx: &ExecutionContext) -> Result<()> {
.stdout
.lines()
.find(|line| line.contains("Session:"))
.expect("the output of `waydroid status` should contain `Session:`");
.unwrap_or_else(|| panic!("the output of `waydroid status` should contain `Session:`"));
let is_container_running = session.contains("RUNNING");
let assume_yes = ctx.config().yes(Step::Waydroid);
print_separator("Waydroid");
if is_container_running && !assume_yes {
let update_allowed =
prompt_yesno("Going to execute `waydroid upgrade`, which would STOP the running container, is this ok?")?;
let update_allowed = prompt_yesno(&t!(
"Going to execute `waydroid upgrade`, which would STOP the running container, is this ok?"
))?;
if !update_allowed {
return Err(SkipStep("Skip the Waydroid step because the user don't want to proceed".to_string()).into());
return Err(
SkipStep(t!("Skip the Waydroid step because the user don't want to proceed").to_string()).into(),
);
}
}
ctx.run_type()
@@ -1074,7 +1105,7 @@ pub fn run_waydroid(ctx: &ExecutionContext) -> Result<()> {
}
pub fn run_auto_cpufreq(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let auto_cpu_freq = require("auto-cpufreq")?;
print_separator("auto-cpufreq");
@@ -1086,6 +1117,17 @@ pub fn run_auto_cpufreq(ctx: &ExecutionContext) -> Result<()> {
.status_checked()
}
pub fn run_cinnamon_spices_updater(ctx: &ExecutionContext) -> Result<()> {
let cinnamon_spice_updater = require("cinnamon-spice-updater")?;
print_separator("Cinnamon spices");
ctx.run_type()
.execute(cinnamon_spice_updater)
.arg("--update-all")
.status_checked()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1185,6 +1227,11 @@ mod tests {
test_template(include_str!("os_release/gentoo"), Distribution::Gentoo);
}
#[test]
fn test_funtoo() {
test_template(include_str!("os_release/funtoo"), Distribution::Gentoo);
}
#[test]
fn test_exherbo() {
test_template(include_str!("os_release/exherbo"), Distribution::Exherbo);
@@ -1244,4 +1291,29 @@ mod tests {
fn test_nobara() {
test_template(include_str!("os_release/nobara"), Distribution::Nobara);
}
#[test]
fn test_nilrt() {
test_template(include_str!("os_release/nilrt"), Distribution::NILRT);
}
#[test]
fn test_coreos() {
test_template(include_str!("os_release/coreos"), Distribution::FedoraImmutable);
}
#[test]
fn test_aurora() {
test_template(include_str!("os_release/aurora"), Distribution::FedoraImmutable);
}
#[test]
fn test_bluefin() {
test_template(include_str!("os_release/bluefin"), Distribution::FedoraImmutable);
}
#[test]
fn test_bazzite() {
test_template(include_str!("os_release/bazzite"), Distribution::FedoraImmutable);
}
}

View File

@@ -1,9 +1,10 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::terminal::{print_separator, prompt_yesno};
use crate::utils::{require_option, REQUIRE_SUDO};
use crate::utils::{get_require_sudo_string, require_option};
use crate::{utils::require, Step};
use color_eyre::eyre::Result;
use rust_i18n::t;
use std::collections::HashSet;
use std::fs;
use std::process::Command;
@@ -11,7 +12,7 @@ use tracing::debug;
pub fn run_macports(ctx: &ExecutionContext) -> Result<()> {
require("port")?;
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator("MacPorts");
ctx.run_type()
@@ -34,25 +35,25 @@ pub fn run_macports(ctx: &ExecutionContext) -> Result<()> {
pub fn run_mas(ctx: &ExecutionContext) -> Result<()> {
let mas = require("mas")?;
print_separator("macOS App Store");
print_separator(t!("macOS App Store"));
ctx.run_type().execute(mas).arg("upgrade").status_checked()
}
pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> {
print_separator("macOS system update");
print_separator(t!("macOS system update"));
let should_ask = !(ctx.config().yes(Step::System) || ctx.config().dry_run());
if should_ask {
println!("Finding available software");
println!("{}", t!("Finding available software"));
if system_update_available()? {
let answer = prompt_yesno("A system update is available. Do you wish to install it?")?;
let answer = prompt_yesno(t!("A system update is available. Do you wish to install it?").as_ref())?;
if !answer {
return Ok(());
}
println!();
} else {
println!("No new software available.");
println!("{}", t!("No new software available."));
return Ok(());
}
}
@@ -115,7 +116,7 @@ pub fn update_xcodes(ctx: &ExecutionContext) -> Result<()> {
.collect();
if releases_installed.is_empty() {
println!("No Xcode releases installed.");
println!("{}", t!("No Xcode releases installed."));
return Ok(());
}
@@ -194,14 +195,15 @@ pub fn update_xcodes(ctx: &ExecutionContext) -> Result<()> {
releases_regular_new_installed,
] {
if should_ask && releases_new_installed.len() == 2 {
let answer_uninstall = prompt_yesno("Would you like to move the former Xcode release to the trash?")?;
let answer_uninstall =
prompt_yesno(t!("Would you like to move the former Xcode release to the trash?").as_ref())?;
if answer_uninstall {
let _ = ctx
.run_type()
.execute(&xcodes)
.args([
"uninstall",
releases_new_installed.iter().next().cloned().unwrap_or_default(),
releases_new_installed.iter().next().copied().unwrap_or_default(),
])
.status_checked();
}
@@ -214,18 +216,14 @@ pub fn update_xcodes(ctx: &ExecutionContext) -> Result<()> {
pub fn process_xcodes_releases(releases_filtered: Vec<String>, should_ask: bool, ctx: &ExecutionContext) -> Result<()> {
let xcodes = require("xcodes")?;
if releases_filtered
.last()
.map(|s| !s.contains("(Installed)"))
.unwrap_or(true)
&& !releases_filtered.is_empty()
{
if releases_filtered.last().map_or(true, |s| !s.contains("(Installed)")) && !releases_filtered.is_empty() {
println!(
"New Xcode release detected: {}",
"{} {}",
t!("New Xcode release detected:"),
releases_filtered.last().cloned().unwrap_or_default()
);
if should_ask {
let answer_install = prompt_yesno("Would you like to install it?")?;
let answer_install = prompt_yesno(t!("Would you like to install it?").as_ref())?;
if answer_install {
let _ = ctx
.run_type()

View File

@@ -1,21 +1,52 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::terminal::print_separator;
use crate::utils::{require_option, REQUIRE_SUDO};
use crate::utils::{get_require_sudo_string, require_option};
use color_eyre::eyre::Result;
use rust_i18n::t;
use std::fs;
fn is_openbsd_current(ctx: &ExecutionContext) -> Result<bool> {
let motd_content = fs::read_to_string("/etc/motd")?;
let is_current = ["-current", "-beta"].iter().any(|&s| motd_content.contains(s));
if ctx.config().dry_run() {
println!("{}", t!("Would check if OpenBSD is -current"));
Ok(is_current)
} else {
Ok(is_current)
}
}
pub fn upgrade_openbsd(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("OpenBSD Update");
ctx.run_type()
.execute(sudo)
.args(["/usr/sbin/sysupgrade", "-n"])
.status_checked()
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator(t!("OpenBSD Update"));
let is_current = is_openbsd_current(ctx)?;
if ctx.config().dry_run() {
println!("{}", t!("Would upgrade the OpenBSD system"));
return Ok(());
}
let args = if is_current {
vec!["/usr/sbin/sysupgrade", "-sn"]
} else {
vec!["/usr/sbin/syspatch"]
};
ctx.run_type().execute(sudo).args(&args).status_checked()
}
pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("OpenBSD Packages");
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator(t!("OpenBSD Packages"));
let is_current = is_openbsd_current(ctx)?;
if ctx.config().dry_run() {
println!("{}", t!("Would upgrade OpenBSD packages"));
return Ok(());
}
if ctx.config().cleanup() {
ctx.run_type()
@@ -24,10 +55,12 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
.status_checked()?;
}
ctx.run_type()
.execute(sudo)
.args(["/usr/sbin/pkg_add", "-u"])
.status_checked()?;
let mut args = vec!["/usr/sbin/pkg_add", "-u"];
if is_current {
args.push("-Dsnap");
}
ctx.run_type().execute(sudo).args(&args).status_checked()?;
Ok(())
}

View File

@@ -0,0 +1,23 @@
NAME="Aurora"
VERSION="latest-41.20250210.4 (Kinoite)"
RELEASE_TYPE=stable
ID=aurora
ID_LIKE="fedora"
VERSION_ID=41
VERSION_CODENAME=""
PLATFORM_ID="platform:f41"
PRETTY_NAME="Aurora (Version: latest-41.20250210.4 / FROM Fedora Kinoite 41)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:universal-blue:aurora:41"
DEFAULT_HOSTNAME="aurora"
HOME_URL="https://getaurora.dev/"
DOCUMENTATION_URL="https://docs.getaurora.dev"
SUPPORT_URL="https://github.com/ublue-os/aurora/issues/"
BUG_REPORT_URL="https://github.com/ublue-os/aurora/issues/"
SUPPORT_END=2025-12-15
VARIANT="Kinoite"
VARIANT_ID=aurora
OSTREE_VERSION='latest-41.20250210.4'
BUILD_ID="fc1570c"
IMAGE_ID="aurora"

View File

@@ -0,0 +1,25 @@
NAME="Bazzite"
VERSION="41.20250208.0 (Kinoite)"
RELEASE_TYPE=stable
ID=bazzite
ID_LIKE="fedora"
VERSION_ID=41
VERSION_CODENAME="Holographic"
PLATFORM_ID="platform:f41"
PRETTY_NAME="Bazzite 41 (FROM Fedora Kinoite)"
ANSI_COLOR="0;38;2;138;43;226"
LOGO=bazzite-logo-icon
CPE_NAME="cpe:/o:universal-blue:bazzite:41"
DEFAULT_HOSTNAME="bazzite"
HOME_URL="https://bazzite.gg"
DOCUMENTATION_URL="https://docs.bazzite.gg"
SUPPORT_URL="https://discord.bazzite.gg"
BUG_REPORT_URL="https://github.com/ublue-os/bazzite/issues/"
SUPPORT_END=2025-12-15
VARIANT="Kinoite"
VARIANT_ID=bazzite-nvidia-open
OSTREE_VERSION='41.20250208.0'
BUILD_ID="Stable (F41.20250208)"
BOOTLOADER_NAME="Bazzite Stable (F41.20250208)"
BUILD_ID="Stable (F41.20250208)"
BOOTLOADER_NAME="Bazzite Stable (F41.20250208)"

View File

@@ -0,0 +1,24 @@
NAME="Bluefin"
VERSION="41.20250216.1 (Silverblue)"
RELEASE_TYPE=stable
ID=bluefin
ID_LIKE="fedora"
VERSION_ID=41
VERSION_CODENAME="Archaeopteryx"
PLATFORM_ID="platform:f41"
PRETTY_NAME="Bluefin (Version: 41.20250216.1 / FROM Fedora Silverblue 41)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:universal-blue:bluefin:41"
DEFAULT_HOSTNAME="bluefin"
HOME_URL="https://projectbluefin.io"
DOCUMENTATION_URL="https://docs.projectbluefin.io"
SUPPORT_URL="https://github.com/ublue-os/bluefin/issues/"
BUG_REPORT_URL="https://github.com/ublue-os/bluefin/issues/"
SUPPORT_END=2025-12-15
VARIANT="Silverblue"
VARIANT_ID=bluefin
OSTREE_VERSION='41.20250216.1'
BUILD_ID="185146a"
IMAGE_ID="bluefin"
IMAGE_VERSION="41.20250216.1"

View File

@@ -0,0 +1,23 @@
NAME="Fedora Linux"
VERSION="41.20250117.3.0 (CoreOS)"
RELEASE_TYPE=stable
ID=fedora
VERSION_ID=41
VERSION_CODENAME=""
PLATFORM_ID="platform:f41"
PRETTY_NAME="Fedora CoreOS 41.20250117.3.0 (uCore)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:41"
HOME_URL="https://getfedora.org/coreos/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora-coreos/"
SUPPORT_URL="https://github.com/coreos/fedora-coreos-tracker/"
BUG_REPORT_URL="https://github.com/coreos/fedora-coreos-tracker/"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=41
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=41
SUPPORT_END=2025-12-15
VARIANT="CoreOS"
VARIANT_ID=coreos
OSTREE_VERSION='41.20250117.3.0'

View File

@@ -0,0 +1,6 @@
ID="funtoo"
NAME="Funtoo"
PRETTY_NAME="Funtoo Linux"
ANSI_COLOR="0;34"
HOME_URL="https://www.funtoo.org"
BUG_REPORT_URL="https://bugs.funtoo.org"

View File

@@ -0,0 +1,8 @@
ID=nilrt
NAME="NI Linux Real-Time"
VERSION="10.0 (kirkstone)"
VERSION_ID=10.0
PRETTY_NAME="NI Linux Real-Time 10.0 (kirkstone)"
DISTRO_CODENAME="kirkstone"
BUILD_ID="23.8.0f153-x64"
VERSION_CODENAME="kirkstone"

View File

@@ -13,6 +13,12 @@ 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")]
@@ -24,7 +30,7 @@ use crate::executor::Executor;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::executor::RunType;
use crate::terminal::print_separator;
use crate::utils::{require, require_option, PathExt, REQUIRE_SUDO};
use crate::utils::{get_require_sudo_string, require, require_option, PathExt};
#[cfg(any(target_os = "linux", target_os = "macos"))]
const INTEL_BREW: &str = "/usr/local/bin/brew";
@@ -98,19 +104,19 @@ pub fn run_fisher(ctx: &ExecutionContext) -> Result<()> {
.args(["-c", "type -t fisher"])
.output_checked_utf8()
.map(|_| ())
.map_err(|_| SkipStep("`fisher` is not defined in `fish`".to_owned()))?;
.map_err(|_| SkipStep(t!("`fisher` is not defined in `fish`").to_string()))?;
Command::new(&fish)
.args(["-c", "echo \"$__fish_config_dir/fish_plugins\""])
.output_checked_utf8()
.and_then(|output| Path::new(&output.stdout.trim()).require().map(|_| ()))
.map_err(|err| SkipStep(format!("`fish_plugins` path doesn't exist: {err}")))?;
.map_err(|err| SkipStep(t!("`fish_plugins` path doesn't exist: {err}", err = err).to_string()))?;
Command::new(&fish)
.args(["-c", "fish_update_completions"])
.output_checked_utf8()
.map(|_| ())
.map_err(|_| SkipStep("`fish_update_completions` is not available".to_owned()))?;
.map_err(|_| SkipStep(t!("`fish_update_completions` is not available").to_string()))?;
print_separator("Fisher");
@@ -177,7 +183,7 @@ pub fn run_oh_my_fish(ctx: &ExecutionContext) -> Result<()> {
pub fn run_pkgin(ctx: &ExecutionContext) -> Result<()> {
let pkgin = require("pkgin")?;
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator("Pkgin");
@@ -232,7 +238,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")),
"Desktop doest not appear to be gnome".to_string(),
t!("Desktop doest not appear to be gnome").to_string(),
)?;
let output = Command::new("gdbus")
.args([
@@ -249,10 +255,10 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
debug!("Checking for gnome extensions: {}", output);
if !output.stdout.contains("org.gnome.Shell.Extensions") {
return Err(SkipStep(String::from("Gnome shell extensions are unregistered in DBus")).into());
return Err(SkipStep(t!("Gnome shell extensions are unregistered in DBus").to_string()).into());
}
print_separator("Gnome Shell extensions");
print_separator(t!("Gnome Shell extensions"));
ctx.run_type()
.execute(gdbus)
@@ -269,6 +275,23 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
.status_checked()
}
#[cfg(target_os = "linux")]
pub fn brew_linux_sudo_uid() -> Option<u32> {
let linuxbrew_directory = "/home/linuxbrew/.linuxbrew";
if let Ok(metadata) = std::fs::metadata(linuxbrew_directory) {
let owner_id = metadata.uid();
let current_id = Uid::effective();
// print debug these two values
debug!("linuxbrew_directory owner_id: {}, current_id: {}", owner_id, current_id);
return if owner_id == current_id.as_raw() {
None // no need for sudo if linuxbrew is owned by the current user
} else {
Some(owner_id) // otherwise use sudo to run brew as the owner
};
}
None
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()> {
#[allow(unused_variables)]
@@ -277,10 +300,37 @@ pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<
#[cfg(target_os = "macos")]
{
if variant.is_path() && !BrewVariant::is_macos_custom(binary_name) {
return Err(SkipStep("Not a custom brew for macOS".to_string()).into());
return Err(SkipStep(t!("Not a custom brew for macOS").to_string()).into());
}
}
#[cfg(target_os = "linux")]
{
let sudo_uid = brew_linux_sudo_uid();
// if brew is owned by another user, execute "sudo -Hu <uid> brew update"
if let Some(user_id) = sudo_uid {
let uid = nix::unistd::Uid::from_raw(user_id);
let user = nix::unistd::User::from_uid(uid)
.expect("failed to call getpwuid()")
.expect("this user should exist");
let sudo_as_user = t!("sudo as user '{user}'", user = user.name);
print_separator(format!("{} ({})", variant.step_title(), sudo_as_user));
let sudo = crate::utils::require_option(ctx.sudo().as_ref(), crate::utils::get_require_sudo_string())?;
ctx.run_type()
.execute(sudo)
.current_dir("/tmp") // brew needs a writable current directory
.args([
"--set-home",
&format!("--user={}", user.name),
&format!("{}", binary_name.to_string_lossy()),
"update",
])
.status_checked()?;
return Ok(());
}
}
print_separator(variant.step_title());
let run_type = ctx.run_type();
@@ -310,7 +360,7 @@ pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<
pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()> {
let binary_name = require(variant.binary_name())?;
if variant.is_path() && !BrewVariant::is_macos_custom(binary_name) {
return Err(SkipStep("Not a custom brew for macOS".to_string()).into());
return Err(SkipStep(t!("Not a custom brew for macOS").to_string()).into());
}
print_separator(format!("{} - Cask", variant.step_title()));
let run_type = ctx.run_type();
@@ -336,6 +386,9 @@ pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()>
if ctx.config().brew_greedy_latest() {
brew_args.push("--greedy-latest");
}
if ctx.config().brew_greedy_auto_updates() {
brew_args.push("--greedy-auto-updates");
}
}
variant.execute(run_type).args(&brew_args).status_checked()?;
@@ -362,7 +415,7 @@ pub fn run_guix(ctx: &ExecutionContext) -> Result<()> {
if should_upgrade {
return run_type.execute(&guix).args(["package", "-u"]).status_checked();
}
Err(SkipStep(String::from("Guix Pull Failed, Skipping")).into())
Err(SkipStep(t!("Guix Pull Failed, Skipping").to_string()).into())
}
pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
@@ -382,23 +435,72 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
#[cfg(target_os = "macos")]
{
if require("darwin-rebuild").is_ok() {
return Err(SkipStep(String::from(
"Nix-darwin on macOS must be upgraded via darwin-rebuild switch",
))
.into());
return Err(
SkipStep(t!("Nix-darwin on macOS must be upgraded via darwin-rebuild switch").to_string()).into(),
);
}
}
let run_type = ctx.run_type();
run_type.execute(nix_channel).arg("--update").status_checked()?;
let mut get_version_cmd = ctx.run_type().execute(&nix);
get_version_cmd.arg("--version");
let get_version_cmd_output = get_version_cmd.output_checked_utf8()?;
let get_version_cmd_first_line_stdout = get_version_cmd_output
.stdout
.lines()
.next()
.ok_or_else(|| eyre!("`nix --version` output is empty"))?;
let is_lix = get_version_cmd_first_line_stdout.contains("Lix");
debug!(
output=%get_version_cmd_output,
?is_lix,
"`nix --version` output"
);
lazy_static! {
static ref NIX_VERSION_REGEX: Regex =
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 version =
Version::parse(raw_version).wrap_err_with(|| format!("Unable to parse Nix version: {raw_version:?}"))?;
debug!("Nix version: {:?}", version);
// Nix since 2.21.0 uses `--all --impure` rather than `.*` to upgrade all packages.
// Lix is based on Nix 2.18, so it doesn't!
let packages = if version >= Version::new(2, 21, 0) && !is_lix {
vec!["--all", "--impure"]
} else {
vec![".*"]
};
if Path::new(&manifest_json_path).exists() {
run_type
.execute(nix)
.args(nix_args())
.arg("profile")
.arg("upgrade")
.arg(".*")
.args(&packages)
.arg("--verbose")
.status_checked()
} else {
@@ -427,20 +529,16 @@ pub fn run_nix_self_upgrade(ctx: &ExecutionContext) -> Result<()> {
}
if !should_self_upgrade {
return Err(SkipStep(String::from(
"`nix upgrade-nix` can only be used on macOS or non-NixOS Linux",
))
.into());
return Err(SkipStep(t!("`nix upgrade-nix` can only be used on macOS or non-NixOS Linux").to_string()).into());
}
if nix_profile_dir(&nix)?.is_none() {
return Err(SkipStep(String::from(
"`nix upgrade-nix` cannot be run when Nix is installed in a profile",
))
.into());
return Err(
SkipStep(t!("`nix upgrade-nix` cannot be run when Nix is installed in a profile").to_string()).into(),
);
}
print_separator("Nix (self-upgrade)");
print_separator(t!("Nix (self-upgrade)"));
let multi_user = fs::metadata(&nix)?.uid() == 0;
debug!("Multi user nix: {}", multi_user);
@@ -505,7 +603,6 @@ fn nix_profile_dir(nix: &Path) -> Result<Option<PathBuf>> {
}
debug!("Found Nix profile {profile_dir:?}");
let user_env = profile_dir
.canonicalize()
.wrap_err_with(|| format!("Failed to canonicalize {profile_dir:?}"))?;
@@ -514,8 +611,7 @@ fn nix_profile_dir(nix: &Path) -> Result<Option<PathBuf>> {
if user_env
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.ends_with("user-environment"))
.unwrap_or(false)
.is_some_and(|name| name.ends_with("user-environment"))
{
Some(profile_dir)
} else {
@@ -540,10 +636,33 @@ pub fn run_asdf(ctx: &ExecutionContext) -> Result<()> {
let asdf = require("asdf")?;
print_separator("asdf");
ctx.run_type()
.execute(&asdf)
.arg("update")
.status_checked_with_codes(&[42])?;
// asdf (>= 0.15.0) won't support the self-update command
//
// https://github.com/topgrade-rs/topgrade/issues/1007
let version_output = Command::new(&asdf).arg("version").output_checked_utf8()?;
// Example output
//
// ```
// $ asdf version
// v0.15.0-31e8c93
//
// ```
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");
if version < Version::new(0, 15, 0) {
ctx.run_type()
.execute(&asdf)
.arg("update")
.status_checked_with_codes(&[42])?;
}
ctx.run_type()
.execute(&asdf)
@@ -556,12 +675,12 @@ pub fn run_mise(ctx: &ExecutionContext) -> Result<()> {
print_separator("mise");
ctx.run_type().execute(&mise).arg("upgrade").status_checked()?;
ctx.run_type()
.execute(&mise)
.args(["plugins", "update"])
.status_checked()
.status_checked()?;
ctx.run_type().execute(&mise).arg("upgrade").status_checked()
}
pub fn run_home_manager(ctx: &ExecutionContext) -> Result<()> {
@@ -597,16 +716,18 @@ pub fn run_pyenv(ctx: &ExecutionContext) -> Result<()> {
let pyenv = require("pyenv")?;
print_separator("pyenv");
let pyenv_dir = var("PYENV_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".pyenv"));
let pyenv_dir = var("PYENV_ROOT").map_or_else(|_| HOME_DIR.join(".pyenv"), PathBuf::from);
if !pyenv_dir.exists() {
return Err(SkipStep("Pyenv is installed, but $PYENV_ROOT is not set correctly".to_string()).into());
return Err(SkipStep(t!("Pyenv is installed, but $PYENV_ROOT is not set correctly").to_string()).into());
}
if !pyenv_dir.join(".git").exists() {
return Err(SkipStep("pyenv is not a git repository".to_string()).into());
return Err(SkipStep(t!("pyenv is not a git repository").to_string()).into());
}
if !pyenv_dir.join("plugins").join("pyenv-update").exists() {
return Err(SkipStep(t!("pyenv-update plugin is not installed").to_string()).into());
}
ctx.run_type().execute(pyenv).arg("update").status_checked()
@@ -616,8 +737,7 @@ pub fn run_sdkman(ctx: &ExecutionContext) -> Result<()> {
let bash = require("bash")?;
let sdkman_init_path = var("SDKMAN_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".sdkman"))
.map_or_else(|_| HOME_DIR.join(".sdkman"), PathBuf::from)
.join("bin")
.join("sdkman-init.sh")
.require()
@@ -626,8 +746,7 @@ pub fn run_sdkman(ctx: &ExecutionContext) -> Result<()> {
print_separator("SDKMAN!");
let sdkman_config_path = var("SDKMAN_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".sdkman"))
.map_or_else(|_| HOME_DIR.join(".sdkman"), PathBuf::from)
.join("etc")
.join("config")
.require()?;
@@ -675,26 +794,16 @@ pub fn run_sdkman(ctx: &ExecutionContext) -> Result<()> {
Ok(())
}
pub fn run_bun(ctx: &ExecutionContext) -> Result<()> {
let bun = require("bun")?;
print_separator("Bun");
ctx.run_type().execute(bun).arg("upgrade").status_checked()
}
pub fn run_bun_packages(ctx: &ExecutionContext) -> Result<()> {
let bun = require("bun")?;
print_separator("Bun Packages");
print_separator(t!("Bun Packages"));
let mut package_json: PathBuf = var("BUN_INSTALL")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".bun"));
let mut package_json: PathBuf = var("BUN_INSTALL").map_or_else(|_| HOME_DIR.join(".bun"), PathBuf::from);
package_json.push("install/global/package.json");
if !package_json.exists() {
println!("No global packages installed");
println!("{}", t!("No global packages installed"));
return Ok(());
}
@@ -719,6 +828,7 @@ pub fn run_maza(ctx: &ExecutionContext) -> Result<()> {
}
pub fn reboot() -> Result<()> {
print!("Rebooting...");
print!("{}", t!("Rebooting..."));
Command::new("sudo").arg("reboot").status_checked()
}

View File

@@ -11,6 +11,7 @@ use crate::terminal::{print_separator, print_warning};
use crate::utils::{require, which};
use crate::{error::SkipStep, steps::git::RepoStep};
use crate::{powershell, Step};
use rust_i18n::t;
pub fn run_chocolatey(ctx: &ExecutionContext) -> Result<()> {
let choco = require("choco")?;
@@ -41,10 +42,12 @@ pub fn run_winget(ctx: &ExecutionContext) -> Result<()> {
print_separator("winget");
ctx.run_type()
.execute(winget)
.args(["upgrade", "--all"])
.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()
}
pub fn run_scoop(ctx: &ExecutionContext) -> Result<()> {
@@ -57,6 +60,10 @@ pub fn run_scoop(ctx: &ExecutionContext) -> Result<()> {
if ctx.config().cleanup() {
ctx.run_type().execute(&scoop).args(["cleanup", "*"]).status_checked()?;
ctx.run_type()
.execute(&scoop)
.args(["cache", "rm", "-a"])
.status_checked()?
}
Ok(())
@@ -64,12 +71,12 @@ pub fn run_scoop(ctx: &ExecutionContext) -> Result<()> {
pub fn update_wsl(ctx: &ExecutionContext) -> Result<()> {
if !is_wsl_installed()? {
return Err(SkipStep("WSL not installed".to_string()).into());
return Err(SkipStep(t!("WSL not installed").to_string()).into());
}
let wsl = require("wsl")?;
print_separator("Update WSL");
print_separator(t!("Update WSL"));
let mut wsl_command = ctx.run_type().execute(wsl);
wsl_command.args(["--update"]);
@@ -122,7 +129,7 @@ fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> R
let topgrade = Command::new(wsl)
.args(["-d", dist, "bash", "-lc", "which topgrade"])
.output_checked_utf8()
.map_err(|_| SkipStep(String::from("Could not find Topgrade installed in WSL")))?
.map_err(|_| SkipStep(t!("Could not find Topgrade installed in WSL").to_string()))?
.stdout // The normal output from `which topgrade` appends a newline, so we trim it here.
.trim_end()
.to_owned();
@@ -171,7 +178,7 @@ fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> R
pub fn run_wsl_topgrade(ctx: &ExecutionContext) -> Result<()> {
if !is_wsl_installed()? {
return Err(SkipStep("WSL not installed".to_string()).into());
return Err(SkipStep(t!("WSL not installed").to_string()).into());
}
let wsl = require("wsl")?;
@@ -194,26 +201,36 @@ pub fn run_wsl_topgrade(ctx: &ExecutionContext) -> Result<()> {
if ran {
Ok(())
} else {
Err(SkipStep(String::from("Could not find Topgrade in any WSL disribution")).into())
Err(SkipStep(t!("Could not find Topgrade in any WSL disribution").to_string()).into())
}
}
pub fn windows_update(ctx: &ExecutionContext) -> Result<()> {
let powershell = powershell::Powershell::windows_powershell();
print_separator("Windows Update");
print_separator(t!("Windows Update"));
if powershell.supports_windows_update() {
println!("The installer will request to run as administrator, expect a prompt.");
powershell.windows_update(ctx)
} else {
print_warning(
"Consider installing PSWindowsUpdate as the use of Windows Update via USOClient is not supported.",
);
print_warning(t!(
"Consider installing PSWindowsUpdate as the use of Windows Update via USOClient is not supported."
));
Err(SkipStep("USOClient not supported.".to_string()).into())
Err(SkipStep(t!("USOClient not supported.").to_string()).into())
}
}
pub fn microsoft_store(ctx: &ExecutionContext) -> Result<()> {
let powershell = powershell::Powershell::windows_powershell();
print_separator(t!("Microsoft Store"));
powershell.microsoft_store(ctx)
}
pub fn reboot() -> Result<()> {
// If this works, it won't return, but if it doesn't work, it may return a useful error
// message.
@@ -230,7 +247,7 @@ pub fn insert_startup_scripts(git_repos: &mut RepoStep) -> Result<()> {
if let Ok(lnk) = parselnk::Lnk::try_from(Path::new(&path)) {
debug!("Startup link: {:?}", lnk);
if let Some(path) = lnk.relative_path() {
git_repos.insert_if_repo(&startup_dir.join(path));
git_repos.insert_if_repo(startup_dir.join(path));
}
}
}

View File

@@ -4,11 +4,12 @@ use std::path::PathBuf;
use std::process::Command;
use color_eyre::eyre::Result;
use rust_i18n::t;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::terminal::{is_dumb, print_separator};
use crate::utils::{require_option, which, PathExt};
use crate::utils::{require_option, which};
use crate::Step;
pub struct Powershell {
@@ -29,7 +30,7 @@ impl Powershell {
.args(["-NoProfile", "-Command", "Split-Path $profile"])
.output_checked_utf8()
.map(|output| PathBuf::from(output.stdout.trim()))
.and_then(|p| p.require())
.and_then(super::super::utils::PathExt::require)
.ok()
});
@@ -62,21 +63,21 @@ impl Powershell {
}
pub fn update_modules(&self, ctx: &ExecutionContext) -> Result<()> {
let powershell = require_option(self.path.as_ref(), String::from("Powershell is not installed"))?;
let powershell = require_option(self.path.as_ref(), t!("Powershell is not installed").to_string())?;
print_separator("Powershell Modules Update");
print_separator(t!("Powershell Modules Update"));
let mut cmd = vec!["Update-Module"];
if ctx.config().verbose() {
cmd.push("-Verbose")
cmd.push("-Verbose");
}
if ctx.config().yes(Step::Powershell) {
cmd.push("-Force")
cmd.push("-Force");
}
println!("Updating modules...");
println!("{}", t!("Updating modules..."));
ctx.run_type()
.execute(powershell)
// This probably doesn't need `shell_words::join`.
@@ -94,10 +95,18 @@ impl Powershell {
#[cfg(windows)]
pub fn windows_update(&self, ctx: &ExecutionContext) -> Result<()> {
let powershell = require_option(self.path.as_ref(), String::from("Powershell is not installed"))?;
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);
@@ -107,18 +116,46 @@ impl Powershell {
};
command
.args([
"-NoProfile",
"-Command",
&format!(
"Start-Process powershell -Verb runAs -ArgumentList 'Import-Module PSWindowsUpdate; Install-WindowsUpdate -MicrosoftUpdate {} -Verbose'",
if ctx.config().accept_all_windows_updates() {
"-AcceptAll"
} else {
""
}
),
])
.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

@@ -1,54 +1,55 @@
use color_eyre::eyre::Result;
use crate::{
command::CommandExt, error::SkipStep, execution_context::ExecutionContext, terminal::print_separator, utils,
};
fn prepare_async_ssh_command(args: &mut Vec<&str>) {
args.insert(0, "ssh");
args.push("--keep");
}
pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> {
let ssh = utils::require("ssh")?;
let topgrade = ctx.config().remote_topgrade_path();
let mut args = vec!["-t", hostname];
if let Some(ssh_arguments) = ctx.config().ssh_arguments() {
args.extend(ssh_arguments.split_whitespace());
}
let env = format!("TOPGRADE_PREFIX={hostname}");
args.extend(["env", &env, "$SHELL", "-lc", topgrade]);
if ctx.config().run_in_tmux() && !ctx.run_type().dry() {
#[cfg(unix)]
{
prepare_async_ssh_command(&mut args);
crate::tmux::run_command(ctx, hostname, &shell_words::join(args))?;
Err(SkipStep(String::from("Remote Topgrade launched in Tmux")).into())
}
#[cfg(not(unix))]
unreachable!("Tmux execution is only implemented in Unix");
} else if ctx.config().open_remotes_in_new_terminal() && !ctx.run_type().dry() && cfg!(windows) {
prepare_async_ssh_command(&mut args);
ctx.run_type().execute("wt").args(&args).spawn()?;
Err(SkipStep(String::from("Remote Topgrade launched in an external terminal")).into())
} else {
let mut args = vec!["-t", hostname];
if let Some(ssh_arguments) = ctx.config().ssh_arguments() {
args.extend(ssh_arguments.split_whitespace());
}
let env = format!("TOPGRADE_PREFIX={hostname}");
args.extend(["env", &env, "$SHELL", "-lc", topgrade]);
print_separator(format!("Remote ({hostname})"));
println!("Connecting to {hostname}...");
ctx.run_type().execute(ssh).args(&args).status_checked()
}
}
use color_eyre::eyre::Result;
use rust_i18n::t;
use crate::{
command::CommandExt, error::SkipStep, execution_context::ExecutionContext, terminal::print_separator, utils,
};
fn prepare_async_ssh_command(args: &mut Vec<&str>) {
args.insert(0, "ssh");
args.push("--keep");
}
pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> {
let ssh = utils::require("ssh")?;
let topgrade = ctx.config().remote_topgrade_path();
let mut args = vec!["-t", hostname];
if let Some(ssh_arguments) = ctx.config().ssh_arguments() {
args.extend(ssh_arguments.split_whitespace());
}
let env = format!("TOPGRADE_PREFIX={hostname}");
args.extend(["env", &env, "$SHELL", "-lc", topgrade]);
if ctx.config().run_in_tmux() && !ctx.run_type().dry() {
#[cfg(unix)]
{
prepare_async_ssh_command(&mut args);
crate::tmux::run_command(ctx, hostname, &shell_words::join(args))?;
Err(SkipStep(String::from(t!("Remote Topgrade launched in Tmux"))).into())
}
#[cfg(not(unix))]
unreachable!("Tmux execution is only implemented in Unix");
} else if ctx.config().open_remotes_in_new_terminal() && !ctx.run_type().dry() && cfg!(windows) {
prepare_async_ssh_command(&mut args);
ctx.run_type().execute("wt").args(&args).spawn()?;
Err(SkipStep(String::from(t!("Remote Topgrade launched in an external terminal"))).into())
} else {
let mut args = vec!["-t", hostname];
if let Some(ssh_arguments) = ctx.config().ssh_arguments() {
args.extend(ssh_arguments.split_whitespace());
}
let env = format!("TOPGRADE_PREFIX={hostname}");
args.extend(["env", &env, "$SHELL", "-lc", topgrade]);
print_separator(format!("Remote ({hostname})"));
println!("{}", t!("Connecting to {hostname}...", hostname = hostname));
ctx.run_type().execute(ssh).args(&args).status_checked()
}
}

View File

@@ -4,6 +4,7 @@ use std::{fmt::Display, rc::Rc, str::FromStr};
use color_eyre::eyre::Result;
use regex::Regex;
use rust_i18n::t;
use strum::EnumString;
use tracing::{debug, error};
@@ -125,7 +126,7 @@ impl<'a> TemporaryPowerOn<'a> {
}
}
impl<'a> Drop for TemporaryPowerOn<'a> {
impl Drop for TemporaryPowerOn<'_> {
fn drop(&mut self) {
let subcommand = if self.ctx.config().vagrant_always_suspend().unwrap_or(false) {
"suspend"
@@ -151,14 +152,14 @@ impl<'a> Drop for TemporaryPowerOn<'a> {
pub fn collect_boxes(ctx: &ExecutionContext) -> Result<Vec<VagrantBox>> {
let directories = utils::require_option(
ctx.config().vagrant_directories(),
String::from("No Vagrant directories were specified in the configuration file"),
String::from(t!("No Vagrant directories were specified in the configuration file")),
)?;
let vagrant = Vagrant {
path: utils::require("vagrant")?,
};
print_separator("Vagrant");
println!("Collecting Vagrant boxes");
println!("{}", t!("Collecting Vagrant boxes"));
let mut result = Vec::new();
@@ -183,7 +184,11 @@ pub fn topgrade_vagrant_box(ctx: &ExecutionContext, vagrant_box: &VagrantBox) ->
let mut _poweron = None;
if !vagrant_box.initial_status.powered_on() {
if !(ctx.config().vagrant_power_on().unwrap_or(true)) {
return Err(SkipStep(format!("Skipping powered off box {vagrant_box}")).into());
return Err(SkipStep(format!(
"{}",
t!("Skipping powered off box {vagrant_box}", vagrant_box = vagrant_box)
))
.into());
} else {
print_separator(seperator);
_poweron = Some(vagrant.temporary_power_on(vagrant_box, ctx)?);
@@ -205,7 +210,7 @@ pub fn topgrade_vagrant_box(ctx: &ExecutionContext, vagrant_box: &VagrantBox) ->
pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> {
let vagrant = utils::require("vagrant")?;
print_separator("Vagrant boxes");
print_separator(t!("Vagrant boxes"));
let outdated = Command::new(&vagrant)
.args(["box", "outdated", "--global"])
@@ -227,7 +232,7 @@ pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> {
}
if !found {
println!("No outdated boxes")
println!("{}", t!("No outdated boxes"));
} else {
ctx.run_type()
.execute(&vagrant)

View File

@@ -7,6 +7,8 @@ use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use crate::command::CommandExt;
use crate::config::TmuxConfig;
use crate::config::TmuxSessionMode;
use crate::terminal::print_separator;
use crate::HOME_DIR;
use crate::{
@@ -14,6 +16,7 @@ use crate::{
utils::{which, PathExt},
};
use rust_i18n::t;
#[cfg(unix)]
use std::os::unix::process::CommandExt as _;
@@ -125,13 +128,13 @@ impl Tmux {
.output_checked_utf8()?
.stdout
.lines()
.map(|l| l.parse())
.map(str::parse)
.collect::<Result<Vec<usize>, _>>()
.context("Failed to compute tmux windows")
}
}
pub fn run_in_tmux(args: Vec<String>) -> Result<()> {
pub fn run_in_tmux(config: TmuxConfig) -> Result<()> {
let command = {
let mut command = vec![
String::from("env"),
@@ -144,39 +147,50 @@ pub fn run_in_tmux(args: Vec<String>) -> Result<()> {
shell_words::join(command)
};
let tmux = Tmux::new(args);
let tmux = Tmux::new(config.args);
// Find an unused session and run `topgrade` in it with the current command's arguments.
let session_name = "topgrade";
let window_name = "topgrade";
let session = tmux.new_unique_session(session_name, window_name, &command)?;
// Only attach to the newly-created session if we're not currently in a tmux session.
if env::var("TMUX").is_err() {
let err = tmux.build().args(["attach-session", "-t", &session]).exec();
Err(eyre!("{err}")).context("Failed to `execvp(3)` tmux")
} else {
println!("Topgrade launched in a new tmux session");
Ok(())
}
let is_inside_tmux = env::var("TMUX").is_ok();
let err = match config.session_mode {
TmuxSessionMode::AttachIfNotInSession => {
if is_inside_tmux {
// Only attach to the newly-created session if we're not currently in a tmux session.
println!("{}", t!("Topgrade launched in a new tmux session"));
return Ok(());
} else {
tmux.build().args(["attach-session", "-t", &session]).exec()
}
}
TmuxSessionMode::AttachAlways => {
if is_inside_tmux {
tmux.build().args(["switch-client", "-t", &session]).exec()
} else {
tmux.build().args(["attach-session", "-t", &session]).exec()
}
}
};
Err(eyre!("{err}")).context("Failed to `execvp(3)` tmux")
}
pub fn run_command(ctx: &ExecutionContext, window_name: &str, command: &str) -> Result<()> {
let tmux = Tmux::new(ctx.config().tmux_arguments()?);
let tmux = Tmux::new(ctx.config().tmux_config()?.args);
match ctx.get_tmux_session() {
Some(session_name) => {
let indices = tmux.window_indices(&session_name)?;
let last_window = indices
.iter()
.last()
.ok_or_else(|| eyre!("tmux session {session_name} has no windows"))?;
tmux.new_window(&session_name, &format!("{last_window}"), command)?;
}
None => {
let name = tmux.new_unique_session("topgrade", window_name, command)?;
ctx.set_tmux_session(name);
}
if let Some(session_name) = ctx.get_tmux_session() {
let indices = tmux.window_indices(&session_name)?;
let last_window = indices
.iter()
.last()
.ok_or_else(|| eyre!("tmux session {session_name} has no windows"))?;
tmux.new_window(&session_name, &format!("{last_window}"), command)?;
} else {
let name = tmux.new_unique_session("topgrade", window_name, command)?;
ctx.set_tmux_session(name);
}
Ok(())
}

View File

@@ -10,6 +10,7 @@ use crate::{
execution_context::ExecutionContext,
utils::{require, PathExt},
};
use rust_i18n::t;
use std::path::PathBuf;
use std::{
io::{self, Write},
@@ -64,7 +65,7 @@ fn upgrade(command: &mut Executor, ctx: &ExecutionContext) -> Result<()> {
if !status.success() {
return Err(TopgradeError::ProcessFailed(command.get_program(), status).into());
} else {
println!("Plugins upgraded")
println!("{}", t!("Plugins upgraded"));
}
}
@@ -77,7 +78,7 @@ pub fn upgrade_ultimate_vimrc(ctx: &ExecutionContext) -> Result<()> {
let python = require("python3")?;
let update_plugins = config_dir.join("update_plugins.py").require()?;
print_separator("The Ultimate vimrc");
print_separator(t!("The Ultimate vimrc"));
ctx.run_type()
.execute(&git)
@@ -108,7 +109,7 @@ pub fn upgrade_vim(ctx: &ExecutionContext) -> Result<()> {
let output = Command::new(&vim).arg("--version").output_checked_utf8()?;
if !output.stdout.starts_with("VIM") {
return Err(SkipStep(String::from("vim binary might be actually nvim")).into());
return Err(SkipStep(t!("vim binary might be actually nvim").to_string()).into());
}
let vimrc = vimrc()?;

View File

@@ -30,9 +30,7 @@ pub fn run_zr(ctx: &ExecutionContext) -> Result<()> {
}
fn zdotdir() -> PathBuf {
env::var("ZDOTDIR")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.clone())
env::var("ZDOTDIR").map_or_else(|_| HOME_DIR.clone(), PathBuf::from)
}
pub fn zshrc() -> PathBuf {
@@ -66,8 +64,7 @@ pub fn run_antigen(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?;
let zshrc = zshrc().require()?;
env::var("ADOTDIR")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join("antigen.zsh"))
.map_or_else(|_| HOME_DIR.join("antigen.zsh"), PathBuf::from)
.require()?;
print_separator("antigen");
@@ -83,8 +80,7 @@ pub fn run_zgenom(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?;
let zshrc = zshrc().require()?;
env::var("ZGEN_SOURCE")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".zgenom"))
.map_or_else(|_| HOME_DIR.join(".zgenom"), PathBuf::from)
.require()?;
print_separator("zgenom");
@@ -101,8 +97,7 @@ pub fn run_zplug(ctx: &ExecutionContext) -> Result<()> {
zshrc().require()?;
env::var("ZPLUG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".zplug"))
.map_or_else(|_| HOME_DIR.join(".zplug"), PathBuf::from)
.require()?;
print_separator("zplug");
@@ -118,8 +113,7 @@ pub fn run_zinit(ctx: &ExecutionContext) -> Result<()> {
let zshrc = zshrc().require()?;
env::var("ZINIT_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| XDG_DIRS.data_dir().join("zinit"))
.map_or_else(|_| XDG_DIRS.data_dir().join("zinit"), PathBuf::from)
.require()?;
print_separator("zinit");
@@ -153,8 +147,7 @@ pub fn run_zim(ctx: &ExecutionContext) -> Result<()> {
.output_checked_utf8()
.map(|o| o.stdout)
})
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".zim"))
.map_or_else(|_| HOME_DIR.join(".zim"), PathBuf::from)
.require()?;
print_separator("zim");
@@ -210,8 +203,7 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
.unwrap_or_else(|e| {
let default_path = oh_my_zsh.join("custom");
debug!(
"Running zsh returned {}. Using default path: {}",
e,
"Running zsh returned {e}. Using default path: {}",
default_path.display()
);
default_path
@@ -229,7 +221,7 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
custom_repos.remove(&oh_my_zsh);
ctx.run_type()
.execute("zsh")
.arg(&oh_my_zsh.join("tools/upgrade.sh"))
.arg(oh_my_zsh.join("tools/upgrade.sh"))
// oh-my-zsh returns 80 when it is already updated and no changes pulled
// in this update.
// See this comment: https://github.com/r-darwish/topgrade/issues/569#issuecomment-736756731

View File

@@ -22,13 +22,28 @@ pub struct Sudo {
}
impl Sudo {
/// Get the `sudo` binary or the `gsudo` binary in the case of `gsudo`
/// masquerading as the `sudo` binary.
fn determine_sudo_variant(sudo_p: PathBuf) -> (PathBuf, SudoKind) {
match which("gsudo") {
Some(gsudo_p) => {
match std::fs::canonicalize(&gsudo_p).unwrap() == std::fs::canonicalize(&sudo_p).unwrap() {
true => (gsudo_p, SudoKind::Gsudo),
false => (sudo_p, SudoKind::Sudo),
}
}
None => (sudo_p, SudoKind::Sudo),
}
}
/// Get the `sudo` binary for this platform.
pub fn detect() -> Option<Self> {
which("doas")
.map(|p| (p, SudoKind::Doas))
.or_else(|| which("sudo").map(|p| (p, SudoKind::Sudo)))
.or_else(|| which("sudo").map(Self::determine_sudo_variant))
.or_else(|| which("gsudo").map(|p| (p, SudoKind::Gsudo)))
.or_else(|| which("pkexec").map(|p| (p, SudoKind::Pkexec)))
.or_else(|| which("run0").map(|p| (p, SudoKind::Run0)))
.or_else(|| which("please").map(|p| (p, SudoKind::Please)))
.map(|(path, kind)| Self { path, kind })
}
@@ -65,9 +80,11 @@ impl Sudo {
cmd.arg("-v");
}
SudoKind::Gsudo => {
// Shows current user, cache and console status.
// `gsudo` doesn't have anything like `sudo -v` to cache credentials,
// so we just execute a dummy `echo` command so we have something
// unobtrusive to run.
// See: https://gerardog.github.io/gsudo/docs/usage
cmd.arg("status");
cmd.arg("echo");
}
SudoKind::Pkexec => {
// I don't think this does anything; `pkexec` usually asks for
@@ -79,6 +96,13 @@ impl Sudo {
// See: https://linux.die.net/man/1/pkexec
cmd.arg("echo");
}
SudoKind::Run0 => {
// `run0` uses polkit for authentication
// and thus has the same issues as `pkexec`.
//
// See: https://www.freedesktop.org/software/systemd/man/devel/run0.html
cmd.arg("echo");
}
SudoKind::Please => {
// From `man please`
// -w, --warm
@@ -115,6 +139,7 @@ pub enum SudoKind {
Sudo,
Gsudo,
Pkexec,
Run0,
Please,
}

View File

@@ -11,6 +11,7 @@ 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};
#[cfg(windows)]
use which_crate::which;
@@ -51,9 +52,7 @@ impl Terminal {
Self {
width: term.size_checked().map(|(_, w)| w),
term,
prefix: env::var("TOPGRADE_PREFIX")
.map(|prefix| format!("({prefix}) "))
.unwrap_or_else(|_| String::new()),
prefix: env::var("TOPGRADE_PREFIX").map_or_else(|_| String::new(), |prefix| format!("({prefix}) ")),
set_title: true,
display_time: true,
desktop_notification: false,
@@ -61,15 +60,15 @@ impl Terminal {
}
fn set_desktop_notifications(&mut self, desktop_notifications: bool) {
self.desktop_notification = desktop_notifications
self.desktop_notification = desktop_notifications;
}
fn set_title(&mut self, set_title: bool) {
self.set_title = set_title
self.set_title = set_title;
}
fn display_time(&mut self, display_time: bool) {
self.display_time = display_time
self.display_time = display_time;
}
fn notify_desktop<P: AsRef<str>>(&self, message: P, timeout: Option<Duration>) {
@@ -144,7 +143,7 @@ impl Terminal {
self.term
.write_fmt(format_args!(
"{} {}",
style(format!("{key} failed:")).red().bold(),
style(format!("{}", t!("{key} failed:", key = key))).red().bold(),
message
))
.ok();
@@ -174,10 +173,10 @@ impl Terminal {
"{}: {}\n",
key,
match result {
StepResult::Success => format!("{}", style("OK").bold().green()),
StepResult::Failure => format!("{}", style("FAILED").bold().red()),
StepResult::Ignored => format!("{}", style("IGNORED").bold().yellow()),
StepResult::Skipped(reason) => format!("{}: {}", style("SKIPPED").bold().blue(), reason),
StepResult::Success => format!("{}", style(t!("OK")).bold().green()),
StepResult::Failure => format!("{}", style(t!("FAILED")).bold().red()),
StepResult::Ignored => format!("{}", style(t!("IGNORED")).bold().yellow()),
StepResult::Skipped(reason) => format!("{}: {}", style(t!("SKIPPED")).bold().blue(), reason),
}
))
.ok();
@@ -188,7 +187,7 @@ impl Terminal {
self.term
.write_fmt(format_args!(
"{}",
style(format!("{question} (y)es/(N)o",)).yellow().bold()
style(format!("{question} {}", t!("(Y)es/(N)o"))).yellow().bold()
))
.ok();
@@ -207,14 +206,14 @@ impl Terminal {
}
if self.set_title {
self.term.set_title("Topgrade - Awaiting user");
self.term.set_title(format!("Topgrade - {}", t!("Awaiting user")));
}
if self.desktop_notification {
self.notify_desktop(format!("{step_name} failed"), None);
self.notify_desktop(format!("{}", t!("{step_name} failed", step_name = step_name)), None);
}
let prompt_inner = style(format!("{}Retry? (y)es/(N)o/(s)hell/(q)uit", self.prefix))
let prompt_inner = style(format!("{}{}", self.prefix, t!("Retry? (y)es/(N)o/(s)hell/(q)uit")))
.yellow()
.bold();
@@ -222,21 +221,24 @@ impl Terminal {
let answer = loop {
match self.term.read_key() {
Ok(Key::Char('y')) | Ok(Key::Char('Y')) => break Ok(true),
Ok(Key::Char('s')) | Ok(Key::Char('S')) => {
println!("\n\nDropping you to shell. Fix what you need and then exit the shell.\n");
Ok(Key::Char('y' | 'Y')) => break Ok(true),
Ok(Key::Char('s' | 'S')) => {
println!(
"\n\n{}\n",
t!("Dropping you to shell. Fix what you need and then exit the shell.")
);
if let Err(err) = run_shell().context("Failed to run shell") {
self.term.write_fmt(format_args!("{err:?}\n{prompt_inner}")).ok();
} else {
break Ok(true);
}
}
Ok(Key::Char('n')) | Ok(Key::Char('N')) | Ok(Key::Enter) => break Ok(false),
Ok(Key::Char('n' | 'N') | Key::Enter) => break Ok(false),
Err(e) => {
error!("Error reading from terminal: {}", e);
break Ok(false);
}
Ok(Key::Char('q')) | Ok(Key::Char('Q')) => {
Ok(Key::Char('q' | 'Q')) => {
return Err(io::Error::from(io::ErrorKind::Interrupted)).context("Quit from user input")
}
_ => (),
@@ -264,26 +266,26 @@ pub fn should_retry(interrupted: bool, step_name: &str) -> eyre::Result<bool> {
}
pub fn print_separator<P: AsRef<str>>(message: P) {
TERMINAL.lock().unwrap().print_separator(message)
TERMINAL.lock().unwrap().print_separator(message);
}
#[allow(dead_code)]
pub fn print_error<P: AsRef<str>, Q: AsRef<str>>(key: Q, message: P) {
TERMINAL.lock().unwrap().print_error(key, message)
TERMINAL.lock().unwrap().print_error(key, message);
}
#[allow(dead_code)]
pub fn print_warning<P: AsRef<str>>(message: P) {
TERMINAL.lock().unwrap().print_warning(message)
TERMINAL.lock().unwrap().print_warning(message);
}
#[allow(dead_code)]
pub fn print_info<P: AsRef<str>>(message: P) {
TERMINAL.lock().unwrap().print_info(message)
TERMINAL.lock().unwrap().print_info(message);
}
pub fn print_result<P: AsRef<str>>(key: P, result: &StepResult) {
TERMINAL.lock().unwrap().print_result(key, result)
TERMINAL.lock().unwrap().print_result(key, result);
}
/// Tells whether the terminal is dumb.
@@ -312,7 +314,7 @@ pub fn prompt_yesno(question: &str) -> Result<bool, io::Error> {
}
pub fn notify_desktop<P: AsRef<str>>(message: P, timeout: Option<Duration>) {
TERMINAL.lock().unwrap().notify_desktop(message, timeout)
TERMINAL.lock().unwrap().notify_desktop(message, timeout);
}
pub fn display_time(display_time: bool) {

View File

@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use color_eyre::eyre::Result;
use rust_i18n::t;
use tracing::{debug, error};
use tracing_subscriber::layer::SubscriberExt;
@@ -51,7 +52,11 @@ where
debug!("Path {:?} exists", self.as_ref());
Ok(self)
} else {
Err(SkipStep(format!("Path {:?} doesn't exist", self.as_ref())).into())
Err(SkipStep(format!(
"{}",
t!("Path {path} doesn't exist", path = format!("{:?}", self.as_ref()))
))
.into())
}
}
}
@@ -81,7 +86,7 @@ pub fn editor() -> Vec<String> {
env::var("EDITOR")
.unwrap_or_else(|_| String::from(if cfg!(windows) { "notepad" } else { "vi" }))
.split_whitespace()
.map(|s| s.to_owned())
.map(std::borrow::ToOwned::to_owned)
.collect()
}
@@ -92,9 +97,14 @@ pub fn require<T: AsRef<OsStr> + Debug>(binary_name: T) -> Result<PathBuf> {
Ok(path)
}
Err(e) => match e {
which_crate::Error::CannotFindBinaryPath => {
Err(SkipStep(format!("Cannot find {:?} in PATH", &binary_name)).into())
}
which_crate::Error::CannotFindBinaryPath => Err(SkipStep(format!(
"{}",
t!(
"Cannot find {binary_name} in PATH",
binary_name = format!("{:?}", &binary_name)
)
))
.into()),
_ => {
panic!("Detecting {:?} failed: {}", &binary_name, e);
}
@@ -123,7 +133,7 @@ pub fn hostname() -> Result<String> {
match nix::unistd::gethostname() {
Ok(os_str) => Ok(os_str
.into_string()
.map_err(|_| SkipStep("Failed to get a UTF-8 encoded hostname".into()))?),
.map_err(|_| SkipStep(t!("Failed to get a UTF-8 encoded hostname").into()))?),
Err(e) => Err(e.into()),
}
}
@@ -132,7 +142,7 @@ pub fn hostname() -> Result<String> {
pub fn hostname() -> Result<String> {
Command::new("hostname")
.output_checked_utf8()
.map_err(|err| SkipStep(format!("Failed to get hostname: {err}")).into())
.map_err(|err| SkipStep(t!("Failed to get hostname: {err}", err = err).to_string()).into())
.map(|output| output.stdout.trim().to_owned())
}
@@ -191,7 +201,9 @@ pub mod merge_strategies {
// Skip causes
// TODO: Put them in a better place when we have more of them
pub const REQUIRE_SUDO: &str = "Require sudo or counterpart but not found, skip";
pub fn get_require_sudo_string() -> String {
t!("Require sudo or counterpart but not found, skip").to_string()
}
/// Return `Err(SkipStep)` if `python` is a Python 2 or shim.
///
@@ -218,11 +230,11 @@ pub fn check_is_python_2_or_shim(python: PathBuf) -> Result<PathBuf> {
.parse::<u32>()
.expect("Major version should be a valid number");
if major_version == 2 {
return Err(SkipStep(format!("{} is a Python 2, skip.", python.display())).into());
return Err(SkipStep(t!("{python} is a Python 2, skip.", python = python.display()).to_string()).into());
}
} else {
// No version number, is a shim
return Err(SkipStep(format!("{} is a Python shim, skip.", python.display())).into());
return Err(SkipStep(t!("{python} is a Python shim, skip.", python = python.display()).to_string()).into());
}
Ok(python)