Compare commits

..

393 Commits

Author SHA1 Message Date
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
SteveLauC
1ebcc9beee chore: prepare for v15.0.0 (#843) 2024-07-01 09:45:20 +08:00
SteveLauC
55e1bbf2b9 feat: new step Lensfun's database update (#839)
* feat: new step Lensfun's database update

* refactor: take 1 as a success exit code
2024-06-30 22:41:09 +08:00
SteveLauC
f2dfa1e475 fix: consider TMUX_PLUGIN_MANAGER_PATH when searching tpm binary (#835)
* fix: consider TMUX_PLUGIN_MANAGER_PATH when searching tpm binary

* fix: correct update_plugins path when env var is present
2024-06-30 19:17:30 +08:00
SteveLauC
fcd53e772a chore: collect --dry-run and --yes opts info in feature request template (#838)
chore: collect --dry-run and --yes opts info in feature request template
2024-06-30 14:17:45 +08:00
dependabot[bot]
8b9d7ef8f3 chore(deps): bump curve25519-dalek from 4.1.2 to 4.1.3 (#827)
Bumps [curve25519-dalek](https://github.com/dalek-cryptography/curve25519-dalek) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/dalek-cryptography/curve25519-dalek/releases)
- [Commits](https://github.com/dalek-cryptography/curve25519-dalek/compare/curve25519-4.1.2...curve25519-4.1.3)

---
updated-dependencies:
- dependency-name: curve25519-dalek
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 09:49:20 +08:00
SteveLauC
d8406a8cfe style: allow unused ExecutorChild (#829)
* style: allow unused ExecutorChild

* style: remove duplicate cfg on windows
2024-06-19 09:43:26 +08:00
SteveLauC
4a9ef581e5 chore: bump deps (#823) 2024-06-13 09:21:42 +08:00
Tamás Demeter-Haludka
a52db1f261 Run MasonUpdate as part of the vim updates (#821)
feat(vim): add mason update
2024-06-13 09:00:15 +08:00
Yaroslav Markin
8e16174ce7 fix(RubyGems): support no-sudo updating for rbenv and rvm (#820) 2024-06-06 19:37:06 +08:00
huajingyun
c748bb5d7a deps: bump libc from 0.2.153 to 0.2.155 (#818) 2024-05-28 09:23:10 +08:00
lachsdachs
3cc8f0d818 Add linux mint support (#817)
Update linux.rs
2024-05-26 16:26:11 +08:00
SteveLauC
f96eeeda6b chore: build binary for both macOS aarch64 and amd64 (#816) 2024-05-25 20:26:21 +08:00
SteveLauC
d1d8904376 ci: replace deprecated gh actions with alternatives (#814) 2024-05-25 19:29:17 +08:00
SteveLauC
3b329fe687 chore: update PR template (#815) 2024-05-25 17:35:46 +08:00
SteveLauC
9eb1b4ac9f ci: remove code coverage test & uniform file names (#811) 2024-05-24 09:02:05 +08:00
lachsdachs
c4c0bd7383 add upgrade stuff for bedrock linuxmint strata (#813) 2024-05-24 09:01:46 +08:00
alice
1e9de5832d feat: add support for chimera linux (#808)
since it also uses apk the update/upgrade is identical to alpine/wolfi
2024-05-19 18:48:51 +08:00
dependabot[bot]
f2b17cdd9d chore(deps): bump mio from 0.8.10 to 0.8.11 (#729)
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.10 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.10...v0.8.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-19 09:55:30 +08:00
dependabot[bot]
7bfd6c2439 chore(deps): bump h2 from 0.3.24 to 0.3.26 (#766)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.24 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.24...v0.3.26)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-19 09:55:05 +08:00
dependabot[bot]
0e8d5f0266 chore(deps): bump rustls from 0.21.10 to 0.21.12 (#804)
Bumps [rustls](https://github.com/rustls/rustls) from 0.21.10 to 0.21.12.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.21.10...v/0.21.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-19 09:54:27 +08:00
Nils
32add8f046 Dependatbot Updates (#802)
* chore(deps): bump actions/checkout from 3 to 4

Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump github/codeql-action from 2 to 3

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump softprops/action-gh-release from 1 to 2

Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-18 19:48:09 +08:00
SteveLauC
f661f00277 feat: support auto-cpufreq (#800) 2024-05-18 16:34:03 +08:00
Alok Singh
2a1999fe20 Add rye support (#799)
Rye is a new cargo-like package manager for python by @mitsuhiko.
2024-05-13 20:52:13 +08:00
SteveLauC
4d66431aad fix: Fedora Sway Atomic should be recognized as FedoraImmutable (#795)
* fix: Fedora Sway Atomic should be recognized as FedoraImmutable

* style: fmt
2024-05-11 11:20:43 +08:00
SteveLauC
767f0d91f4 refactor: 2 clippy warnings (#789) 2024-05-06 20:37:55 +08:00
edi
a3428e3477 Always display windows update step (#781)
* always display windows update step

* remove extra comma

* i guess format wants the comma
2024-05-06 20:24:57 +08:00
David C
614131b7bf fix(os): detect Fedora IoT Edition as immutable Fedora variant (#774)
Without this change, it is detected as a regular Fedora variant and
updating fails because neither `dnf` nor `yum` is found.
2024-04-17 09:05:54 +08:00
Dan Sully
9b0681f3b8 Add config flag to toggle verbose Git repository output. (#763)
* Add config flag to toggle verbose Git repository output.

If `true`: the default, no change.

If `false`: Only show repositories that have been updated or have an error.

Minor tweak to output (removed colon) so that copy and paste for 'cd' is nicer.
2024-04-14 10:28:03 +08:00
Andre Toerien
ecf8fb7a47 fix: better dotnet tool list header parsing (#772)
fix: better dotnet tool list header parsing
2024-04-14 09:10:08 +08:00
Andrew Barchuk
04bfb45a97 Fix local host detection for remotes with user (#755) 2024-04-08 19:43:32 +08:00
SteveLauC
d90ce30452 feat: support update PlatformIO Core (#759) 2024-04-07 11:03:33 +08:00
Ricardo Torres
ab21600ca6 feat: add support for mise (#757)
Add support for mise-en-place (or mise). Mise is a tool like asdf (already supported). https://mise.jdx.dev/
2024-03-30 18:40:16 +08:00
λP.(P izzy)
728ea26204 FIXES #708: add config directive for pkg_* cleanup on OpenBSD (#753)
FIXEs #708: add config directive for pkg_* cleanup on OpenBSD
2024-03-26 11:07:39 +08:00
SteveLauC
373cd3b3ae fix: don't use Command::new(bin_name) as it won't work on Windows (#750) 2024-03-24 11:48:17 +08:00
SteveLauC
f4e0258b09 style: fix 2 clippy lint unless_vec & unused_io_amount (#751) 2024-03-24 11:24:39 +08:00
SteveLauC
d50360a69a feat: support update ClamAV databases (#747) 2024-03-19 14:10:47 +08:00
SteveLauC
351922c81f feat: put step logs in a span (#746) 2024-03-16 14:17:19 +08:00
Alok Singh
9518f43866 Add support for Lean 4's elan (#742) 2024-03-16 09:35:47 +08:00
SteveLauC
2c1ce3d4e6 refactor: make GitSteps a dedicated step (#737) 2024-03-09 17:57:33 +08:00
SteveLauC
12116c3261 fix: use env BUN_INSTALL to locate package.json (#734) 2024-03-07 14:12:16 +08:00
Gerald Chen
fbc84e8aa1 fix(pipx): adds --include-injected argument to pipx (#726) 2024-03-01 15:06:23 +08:00
Brent Monning
6dab1e4f37 feat: adds xcodes step (#643) 2024-03-01 07:58:24 +08:00
Lucas Parzianello
650a143602 Adds pyenv step (#724) 2024-02-27 09:25:18 +08:00
Nils
9b6027fe78 Update GitHub Actions workflow for Codecov integration (#718)
- Refine the testing matrix to include only stable and nightly versions of Rust
- Add 'fail_ci_if_error' option to Codecov step for stricter CI checks
- Ensure newline at end of file
2024-02-25 11:19:09 +08:00
Nils
0e30e05ce8 Add GitHub Actions Workflow for Build and Test (#717)
* "Add *.profraw files to .gitignore

*.profraw files are generated by LLVM's Clang compiler when using the -fprofile-instr-generate option for Profile Guided Optimization. These files contain raw profiling data and should not be version controlled."

* Remove redundant import of TryFrom trait

The TryFrom trait was being imported explicitly in src\steps\os\windows.rs, even though it's already part of the Rust prelude and automatically imported into every Rust program. This was causing a compiler warning. This commit comments out the redundant import to resolve the warning.

* Add GitHub Actions workflow for Rust build and test

This commit adds a new GitHub Actions workflow for building and testing the Rust project across multiple operating systems (Ubuntu, Windows, macOS) and Rust versions (stable, beta, nightly). It also includes caching for dependencies and build artifacts, and uploads code coverage reports to Codecov.

* Update Codecov action and add token for coverage report upload

This commit updates the version of the Codecov GitHub Action used to upload coverage reports from v4 to v4.0.1. It also adds a token from the repository secrets to authenticate the upload. This ensures secure and authorized communication with the Codecov service.

* "Fix misuse of --jobs flag in cargo test command"

* "Fix grcov command in GitHub Actions workflow

The grcov command was previously prefixed with './', which caused an error because grcov was not found in the current directory. This commit removes the './' prefix to call grcov from the global path, where it is installed."

* Update GitHub Actions workflow for cross-platform compatibility

This commit modifies the 'build-and-test.yml' GitHub Actions workflow to ensure it works correctly across different operating systems (Ubuntu, Windows, MacOS). The RUSTFLAGS environment variable is now set in a cross-platform compatible way. The workflow will run the build and test process on every pull request and push to the main branch, generate a coverage report, and upload it to Codecov.

* Changed workflow trigger event to 'workflow_run' completion of 'Build and test' workflow

* "Updated GitHub Actions workflow to correctly set environment variables for code coverage"

* Renamed build and test workflow

* Update GitHub Actions workflow trigger

Change the trigger of the 'Test with Code Coverage' workflow to run when the 'build-and-test' workflow is completed. This ensures that code coverage is only calculated after successful build and test runs.

* Update workflow_run trigger in code-coverage.yml

* Fix CODECOV_TOKEN in code-coverage.yml workflow

* Update code-coverage workflow to trigger on pull requests and pushes to main branch

* Update .gitignore file to exclude LLVM profiling output

* Add empty line at the end

* Remove unused import in windows.rs

* Update .github/workflows/build-and-test.yml

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

* Update .github/workflows/build-and-test.yml

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

* Remove code coverage workflow

---------

Co-authored-by: SteveLauC <stevelauc@outlook.com>
2024-02-25 10:35:56 +08:00
Nils
eea952fa78 Create devskim.yml to enable GitHub code scanning for this repository (#700) 2024-02-24 18:53:10 +08:00
SteveLauC
6071a1ee3b chore: git ignore more (#715) 2024-02-24 13:45:53 +08:00
SteveLauC
a801b7b9f4 chore: bump deps (#714) 2024-02-24 13:14:53 +08:00
SteveLauC
c6e3f0ae0a revert: revert 614 to remove the -p option (#713) 2024-02-24 11:26:41 +08:00
SteveLauC
a43b03d3db feat: also detect Helix step with bin name hx (#710) 2024-02-23 07:39:31 +08:00
Md Isfarul Haque
12b0fa57ad fix: fetch and build Helix grammar as a regular user (#698) 2024-02-23 07:26:08 +08:00
Nils
d9e304f0ef Add .vs to .gitignore (#706)
* Added .vs vode to .gitignore

* Adjust .vs to .vs/
2024-02-22 09:47:37 +08:00
dependabot[bot]
842b92cca7 chore(deps): bump actions/download-artifact from 3 to 4 (#704)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 09:39:05 +08:00
dependabot[bot]
485f0ec9c8 chore(deps): bump EnricoMi/publish-unit-test-result-action from 1 to 2 (#705)
Bumps [EnricoMi/publish-unit-test-result-action](https://github.com/enricomi/publish-unit-test-result-action) from 1 to 2.
- [Release notes](https://github.com/enricomi/publish-unit-test-result-action/releases)
- [Commits](https://github.com/enricomi/publish-unit-test-result-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: EnricoMi/publish-unit-test-result-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 09:38:40 +08:00
λP.(P izzy)
5e3b5fc9a7 Fix OpenBSD Step failing to build with E0599 (#707)
* fix openbsd support failing with error E0599

* clean up a little formatting in src/os/openbsd.os
2024-02-21 21:10:34 +08:00
SteveLauC
7c63541cad fix: zinit default install location (#625) 2024-02-17 13:15:53 +08:00
SteveLauC
238e089d74 docs: document brew config entries[skip ci] (#696) 2024-02-17 13:14:39 +08:00
luciodaou
8991bc9f62 feat(brew): adds "greedy-latest" option to Brew (#636) 2024-02-17 11:45:57 +08:00
SteveLauC
7a3f3a8905 feat: support waydroid (#687) 2024-02-16 11:57:53 +08:00
dependabot[bot]
e4085e03eb chore(deps): bump actions/checkout from 2 to 4 (#688)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-15 16:19:58 +08:00
dependabot[bot]
4b0c366e5f chore(deps): bump actions/upload-artifact from 3 to 4 (#689)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-15 16:19:18 +08:00
dependabot[bot]
ea97240d09 chore(deps): bump actions/cache from 1 to 4 (#690)
Bumps [actions/cache](https://github.com/actions/cache) from 1 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v1...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-15 16:18:47 +08:00
dependabot[bot]
12de531abb chore(deps): bump codecov/codecov-action from 1 to 4 (#691)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1 to 4.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v1...v4)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-15 16:18:24 +08:00
dependabot[bot]
c3876ce3bf chore(deps): bump katyo/publish-crates from 1 to 2 (#692)
Bumps [katyo/publish-crates](https://github.com/katyo/publish-crates) from 1 to 2.
- [Release notes](https://github.com/katyo/publish-crates/releases)
- [Commits](https://github.com/katyo/publish-crates/compare/v1...v2)

---
updated-dependencies:
- dependency-name: katyo/publish-crates
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-15 16:18:00 +08:00
SteveLauC
cbbfc3a114 docs: update install doc with Winget (#693) 2024-02-15 16:17:11 +08:00
Nils
ad2bfc9abd Keeping actions up to date with Dependabot (#685) 2024-02-15 16:04:51 +08:00
Nils
528461412e Publish new releases of topgrade to the Windows Package Manager with WinGet Releaser
Publish new releases of topgrade to the Windows Package Manager with WinGet Releaser (GitHb Action).
2024-02-15 16:04:11 +08:00
SteveLauC
64db679390 ci: add macOS aarch64 check (#680) 2024-02-06 16:28:01 +08:00
Wallunen
77a8b3b7d2 feat: add fetch_head configuration option into brew (#679) 2024-02-06 16:17:27 +08:00
Nils
7007e76ab5 Fix/winget (#670)
* cargo update

* Remove the check for 'winget_enable' set to 'true'. On my Windows 10 and 11 machines, there are no issues with Winget anymore. As far as I remember, it was disabled by default because it was buggy back then.

* remove print_warning

* Revert "cargo update"

This reverts commit 5f4e532bc1.

* Removed the `enable_winget = true` configuration as winget is now enabled by default.

* Removed the #[cfg(windows)] flag.

* Revised as Recommended

* Wrapping at 80
2024-02-03 09:09:47 +08:00
Andy Piper
3c970063a9 fix: correct typos in output (#677)
Corrects a grammatical issue and a typo in two of the step output messages.
2024-01-31 09:07:38 +08:00
SteveLauC
b70830015e docs: fix a wrong preposition[skip ci] (#676) 2024-01-30 11:06:32 +08:00
SteveLauC
b43f2c8b3a ci: run cargo test in ci (#674) 2024-01-29 10:36:30 +08:00
RJ Trujillo
c311da16f3 feat: Add support for Wolfi (#672)
* feat: Add support for Wolfi

This adds support for updating Wolfi via Topgrade

* chore(wolfi): Add os release info and unit test

* chore(wolfi): Don't check ID_LIKE as it is unique
2024-01-29 09:11:53 +08:00
Nils
37608a338c Fix/usoclient (#669)
* cargo update

* Implementing a check for Windows 11 and, if detected, skipping Windows Update via usoclient.exe. It is suggested to install PSWindowsUpdate.

* Revert "cargo update"

This reverts commit 43a4d321cf.

* Revert "Implementing a check for Windows 11 and, if detected, skipping Windows Update via usoclient.exe. It is suggested to install PSWindowsUpdate."

This reverts commit e1ef2e4bc5.

* Removed the usoclient step and added an error message.

* cargo fmt
2024-01-29 09:02:40 +08:00
Nils
b07288e674 Fix/pswindowsupdate (#671)
* cargo update

* An elevated PowerShell is required to run Install-WindowsUpdate on my system.

* Revert "cargo update"

This reverts commit fb58ce761a.
2024-01-29 09:01:38 +08:00
Nils
707698faab Update Cargo.lock (#673)
cargo update
2024-01-29 09:00:08 +08:00
SteveLauC
2e70d132d0 feat: certbot renew (#665) 2024-01-28 13:03:30 +08:00
Brent Monning
30c5b31e21 fix: softwareupdate under dry run (#668) 2024-01-27 14:57:10 +08:00
SteveLauC
77ff6cb714 feat: support wildcard in ignored_containers (#666) 2024-01-27 10:54:55 +08:00
SteveLauC
ea13c51b7d chore: release v14.0.1 (#662) 2024-01-25 15:40:52 +08:00
Cat Core
3ed763b884 Fix system updates for Nobara (#661)
* Fix system updates for Nobara

* fmt

* Add os-release test for Nobara

* Make requested changes

* cargo fmt
2024-01-24 19:29:20 +08:00
samhanic
10e1e170b7 fix vscode extensions update step (#650)
* fix vscode extensions update using the new update-extensions cli

* fix non-linux compilation
2024-01-24 10:32:00 +08:00
Sandro
ffa62afc66 Follow up to the follow up in #616 (#660) 2024-01-24 10:22:36 +08:00
SteveLauC
f794329913 feat: skip breaking changes notification with env var (#659)
* feat: skip breaking changes notification with env var

* ci: apply that env in ci
2024-01-23 14:50:35 +08:00
SteveLauC
f9a35c7661 docs: add doc on how to do a new release (#658) 2024-01-23 11:58:09 +08:00
SteveLauC
ed496f3462 chore: fix file name typo[skip ci] (#657)
chore: fix file name typo
2024-01-23 11:50:02 +08:00
Rui Chen
6accdae232 workflows(homebrew): replace Homebrew/actions/bump-formulae with Homebrew/actions/bump-packages (#656)
Signed-off-by: Rui Chen <rui@chenrui.dev>
2024-01-23 10:29:48 +08:00
SteveLauC
96efcc6c0d chore: release v14.0.0 (#652) 2024-01-22 11:13:33 +08:00
SteveLauC
bf72d7bb5a fix: oh-my-zsh step issue #646 (#647) 2024-01-22 09:18:27 +08:00
dependabot[bot]
dadffb1081 chore(deps): bump h2 from 0.3.22 to 0.3.24 (#645)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.22 to 0.3.24.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.22...v0.3.24)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-20 12:13:19 +08:00
Ned Wolpert
78dc567226 Added an Audit step for FreeBSD and DragonFly packagees. (#640)
* Added an Audit step for FreeBSD and DragonFly.

Allows for auditing the packages to be disabled since they are breaking steps.
Current behaivor is the default, where if the audit fails topgrade stops. Can
be disabled in the [misc] section independenly from other sections
2024-01-08 09:40:01 +08:00
Mike Wood
362ce4f4f9 fix(os) consider Fedora Kinoite and other immutable distros to be the FedoraImmutable (#638)
* fix(os) consider Fedora Kinoite to be the Fedora Silverblue distribution

* fix(os) support additional Fedora immutable variants

Rename FedoraSilverblue Distribution to FedoraImmutable.  Add test cases for Onyx, Sericea and Silverblue.  Rename upgrade method to match distribution.

Fixes #637
2024-01-08 08:48:48 +08:00
Carrol Cox
ab35cd7b10 feat(pipx-update): add quiet flag for pipx upgrade-all on version 1.4.0+ (#635)
This commit introduces conditional logic to the `run_pipx_update` function that checks the installed version of pipx. If the version is 1.4.0 or higher, the `--quiet` argument is added to the `pipx upgrade-all` command to suppress non-critical output during the upgrade process, adhering to the new feature introduced in pipx 1.4.0 as per the documentation (https://pipx.pypa.io/stable/docs/#pipx-upgrade-all). This change aims to make the upgrade process less verbose and more manageable in automated scripts or CI/CD pipelines where log brevity is beneficial.
2023-12-31 11:38:39 +08:00
SteveLauC
15f4ad7cd1 refactor: update pip if extern managed and global.break-system-packages is true (#634)
refactor: update pip if extern managed and global.break-system-packages is true
2023-12-30 18:23:33 +08:00
Rebecca Turner
cbfb92041f Skip nix upgrade-nix when Nix is installed in a nix profile (#622)
Make `nix upgrade-nix` a separate step

Also check that Nix can be upgraded before running `nix upgrade-nix` to
work around a bug.

See: <https://github.com/NixOS/nix/issues/5473>
2023-12-21 08:55:32 +08:00
SteveLauC
a506c67cac fix: remove deprecated brew option '--ignore-pinned' (#629) 2023-12-19 17:09:32 +08:00
SteveLauC
788e0412f6 feat: inform users of breaking changes on first run (#619) 2023-12-03 09:52:35 +08:00
Nils
18b37ce3e3 Update config.example.toml (#621)
Added WinGet setting:
enable_winget = true
2023-11-26 08:06:17 +08:00
Jakob Fels
a15e6748c7 Add option to ignore containers to pull (#613) 2023-11-24 16:44:52 +08:00
SteveLauC
c6d0539fd2 chore(deps): bump all deps (#618) 2023-11-24 07:50:41 +08:00
LeSnake
3eb3867944 Bun packages fixes (#617)
* fix running with --only

* fix error when no packages installed
2023-11-23 06:36:00 +08:00
DomGlusk
810315b0e2 Make zinit and zi use parallel updates (#614)
* Update zsh.rs to make zinit and zi use parallel

* run cargo fmt

---------

Co-authored-by: Dominic Gluskin <rhinoarmyleader@gmail.com>
2023-11-22 11:18:41 +08:00
SteveLauC
b461fc2536 refactor: cleanup for #615 (#616) 2023-11-22 09:34:21 +08:00
Sam Vente
7e63977ba0 revert git pushing functionalities (#615) 2023-11-22 09:04:19 +08:00
SteveLauC
78dec892cf docs: migration and breaking changes (#606) 2023-11-12 11:43:58 +08:00
pacjo
9ea6628b5c docs: fix typo in config.example.toml (#603)
docs(config): fix typo (dfault -> default)
2023-11-10 10:32:15 +08:00
LeSnake
465df2e9be feat: add Bun packages step (#599) 2023-11-05 10:34:21 +08:00
SteveLauC
61ef926849 chore: update issue template label (#596) 2023-11-01 08:57:57 +08:00
SteveLauC
7fa38c593e fix: omz remote execution if ZSH is not present (#592) 2023-10-29 18:05:20 +08:00
SteveLauC
41c6d1cd9a chore: release v13.0.0 (#579) 2023-10-20 08:07:11 +08:00
dependabot[bot]
cf3893dc49 chore(deps): bump rustix from 0.37.20 to 0.37.25 (#586) 2023-10-19 08:38:28 +08:00
SteveLauC
a2fbe92a25 refactor: make SelfUpdate a step (#585) 2023-10-18 12:19:53 +08:00
SteveLauC
e1754707d8 refactor: remove legacy deprecated macros (#583) 2023-10-18 11:13:14 +08:00
SteveLauC
cd380a53b3 docs: new demo video (#584) 2023-10-18 09:33:37 +08:00
SteveLauC
a8c29fd1a2 fix: make logger work while loading config file (#581) 2023-10-17 11:19:47 +08:00
Sam Vente
6b871e7949 switch git push and pull order (#578) 2023-10-15 17:06:40 +08:00
SteveLauC
1b5fdb6645 fix: shellexpand git.pull_only_repos & git.push_only_repos (#576) 2023-10-13 18:54:42 +08:00
Sam Vente
fe9d877cdf Add support for pushing custom git repositories (#574) 2023-10-13 17:01:35 +08:00
SteveLauC
60e7aa8f03 fix: disable dotnet greeting msg with DOTNET_NOLOGO=true (#573) 2023-10-12 14:37:52 +08:00
SteveLauC
18e2d3e59c chore: always use the latest stable toolchain for CI (#571) 2023-10-11 09:46:36 +08:00
Mylloon
d68fcb08b2 fix: Support yes option for opam upgrade (#570) 2023-10-10 08:08:46 +08:00
Zach Crownover
1f6baefdc3 Fix builds and runs on DragonFly BSD (#569) 2023-10-08 08:13:26 +08:00
SteveLauC
71efce32c1 chore: bump CI toolchain to 1.73.0 (#567) 2023-10-06 12:05:44 +08:00
dependabot[bot]
3626c9cdc8 chore(deps): bump webpki from 0.22.1 to 0.22.2 (#564)
Bumps [webpki](https://github.com/briansmith/webpki) from 0.22.1 to 0.22.2.
- [Commits](https://github.com/briansmith/webpki/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 08:21:00 +08:00
SteveLauC
a23b761304 fix: --yes option for protonup (#560) 2023-09-30 16:44:20 +08:00
SteveLauC
3fd27e4913 chore: add the check for the --yes opt in PR template (#561) 2023-09-30 12:46:01 +08:00
Sohum
b3f152b716 feat(wsl): pass verbose to topgrade-in-wsl (#556)
Closes #521
2023-09-26 11:11:19 +08:00
PabloMarcendo
df381f3a79 feat: add option for nix-env arguments (#555) 2023-09-21 09:05:03 +08:00
dependabot[bot]
2dec9db310 chore(deps): bump webpki from 0.22.0 to 0.22.1 (#554)
Bumps [webpki](https://github.com/briansmith/webpki) from 0.22.0 to 0.22.1.
- [Commits](https://github.com/briansmith/webpki/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-20 16:04:27 +08:00
SteveLauC
d50dc4c9f6 chore: bump CI toolchain (#553) 2023-09-20 15:57:35 +08:00
SteveLauC
ed8b563f20 fix: remote oh-my-zsh env var export issue (#528)
* fix: fix remove oh-my-zsh env export issue
2023-09-19 09:15:34 +08:00
Rebecca Turner
2a73aa731d Make error messages nicer (#551)
* Remove unhelpful information from errors

Before:

```
Git repositories failed:
   0: error: cannot pull with rebase: You have unstaged changes.
      error: Please commit or stash them.
   0:

Location:
   src/steps/git.rs:39

Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
```

After:

```
Git repositories failed:
   0: Failed to pull /Users/wiggles/.dotfiles
   1: error: cannot pull with rebase: You have unstaged changes.
      error: Please commit or stash them.

Location:
   src/steps/git.rs:39
```

* Improve git_repos errors

This removes the extra blank "0:" line at the end of the error, doesn't
print the error message twice, and provides the repo path in the error
message.
2023-09-19 09:09:58 +08:00
Rebecca Turner
4dd1c13bd8 fix: fix "Nix" step to use nix upgrade-nix in more situations (#550)
`nix upgrade-nix` can be used on any platform except NixOS where `nix`
is available.

Also use `nix profile upgrade --verbose` because the non-verbose mode
doesn't print anything on stdout.
2023-09-17 15:40:04 +08:00
Rebecca Turner
c1c9fe22df feat: allow setting misc.log_filters in config.toml (#552)
Allow setting `log_filters` in `config.toml`

This allows setting a list of `log_filters` in the `[misc]` section in
the `config.toml`. These filters are prepended to any filters listed
with `--log-filters`. Finally, `--verbose` can now be used with
`--log-filters`, and it will append `debug` to the list of filters
rather than replacing it entirely.
2023-09-17 15:04:46 +08:00
SteveLauC
06a6b7a2eb fix: skip needrestart when using nala on debian-based distro (#548) 2023-09-14 18:15:45 +08:00
SteveLauC
b814dd824f chore: bump ci toolchain (#544) 2023-09-01 14:42:12 +08:00
dependabot[bot]
ce234bdb59 chore(deps): bump rustls-webpki from 0.100.1 to 0.100.2 (#542)
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.100.1 to 0.100.2.
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.100.1...v/0.100.2)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-23 09:29:05 +08:00
SteveLauC
13a46a44a8 refactor: deprioritize please-sudo (#541)
refactor: deprioritze please-sudo
2023-08-22 09:14:29 +08:00
SteveLauC
dc78b00c3c feat: support LURE (#537) 2023-08-15 08:36:20 +08:00
samhanic
48ae4bf813 feat: support Vscode packages updates (#536)
feat: support vscode extensions update
2023-08-14 09:22:26 +08:00
SteveLauC
a50040e2d5 chore: add check for dry-run opt in PR template (#538)
chore: add test for dry-run opt in PR template
2023-08-13 10:24:39 +08:00
samhanic
2c9a56a8df feat: support miktex packages update (#535) 2023-08-13 10:05:07 +08:00
Sujay R
021320b292 Prioritize sudo steps to prevent sudo timeout (#532) 2023-08-06 11:32:20 +08:00
SteveLauC
9d3662c3ea chore: add ssh-related questions in issue template (#523) 2023-07-29 09:20:39 +08:00
SteveLauC
8e580457a5 chore: release v12.0.2 (#518) 2023-07-25 14:22:14 +08:00
SteveLauC
5350658dab fix: WSL detection (#508)
* fix: wSL detection
2023-07-25 14:02:13 +08:00
SteveLauC
1ec0ac50a5 fix: fix Linux and DragonFlyBSD yes option (#513) 2023-07-25 08:37:03 +08:00
SteveLauC
635bfce198 feat: extra arguments for Home Manager (#507)
* feat: extra arguments for Home Manager
2023-07-24 13:07:55 +08:00
6543
1307d2d7e8 feat: better error message on wrong os-release file (#511)
* enhancement: better error message when os-release parsing fails
2023-07-24 08:27:13 +08:00
SteveLauC
d21141fefe chore: release v12.0.1 (#510) 2023-07-23 20:06:31 +08:00
SteveLauC
0ec0e5a9dd chore: bump ci toolchain and MSRV (#506)
* chore: bump ci toolchain and MSRV

* fix clippy on macOS
2023-07-19 10:54:34 +08:00
SteveLauC
9415d7c61f fix(oh-my-zsh): fix remote oh-my-zsh issue (#496)
* fix(oh-my-zsh): fix remote oh-my-zsh issue
2023-07-18 13:59:55 +08:00
SteveLauC
42188af02b CI: release to PyPI (#500) 2023-07-18 08:11:36 +08:00
signed-log
e9581bcf15 feat: add assume-yes to more Linux managers (#501)
* Add assume-yes options to most Linux managers

Add `assume-yes` to :
- SUSE (Micro) - TW (`zypper`)
- PCLinux OS (`apt`)
- Solus (`eopkg`)
- `pacdef`
- Clear Linux (`swupd`)
2023-07-17 15:47:13 +08:00
SteveLauC
6afe4f51c6 test: unit test for Solus (#504) 2023-07-17 13:31:46 +08:00
signed-log
f623746d6c Fix clippy warning about non_minimal_cfg (#505)
Fix clippy::non_minimal_cfg warning
2023-07-17 13:30:55 +08:00
signed-log
1ce4d66e74 Ass assume-yes to DragonflyBSD (#502)
Add assume-yes to DragonflyBSD
2023-07-17 11:40:00 +08:00
har7an
3735d5c537 steps/toolbx: Don't self-update and don't send notifications (#503)
steps/toolbx: Don't send notification

after finishing execution in the toolbx step, and don't perform another
self-update (because the application will already have done that).
2023-07-17 09:08:44 +08:00
SteveLauC
f3b1d2dfb3 Merge pull request from GHSA-f2wx-xjfw-xjv6
chore: bump tempfile to ~3.6
2023-07-15 09:24:08 +08:00
Steve Lau
7f7d2633cd chore: bump tempfile to ~3.6 2023-07-15 09:17:47 +08:00
Harsh Shandilya
afd95e3d5c fix(generic): add alternate binary name for spicetify (#486) 2023-07-14 16:14:06 +00:00
SteveLauC
8f72545894 docs(config): document 4 missing sections in example config file (#485) 2023-07-14 16:13:44 +00:00
SteveLauC
d0d447deac fix: fix wrong path in oh-my-bash (#478) 2023-07-14 16:13:28 +00:00
SteveLauC
53a8683788 ci: separate code-coverage and test-config-creation (#488) 2023-07-14 16:12:53 +00:00
Janek
81491a8d03 docs: apply corrections in config.example.toml (#492)
* Fix Issues in config.example.toml

* Update config.example.toml
2023-07-14 16:12:32 +00:00
Janek
83504754ac docs: add Karma commit messages to CONTRIBUTING.md (#493)
Add Karma commit messages to CONTRIBUTING.md
2023-07-14 16:11:59 +00:00
Marcelo Duarte Trevisani
2068c2c169 Update only base conda env (#495) 2023-07-14 16:11:18 +00:00
SteveLauC
dbac121a90 refactor(config): move sudo_command to section misc (#484) 2023-07-01 13:58:39 +00:00
Thomas Schönauer
b974938a33 v12 Cargo files update (#441) 2023-06-27 10:02:27 +00:00
SteveLauC
06cb88a1a1 test: test for config file creation and default config file parsing (#459) 2023-06-23 09:04:05 +00:00
SteveLauC
a6195d284c feat: support Bob (#461) 2023-06-23 09:03:57 +00:00
SteveLauC
5b8850e8a3 chore: update bug report issue template (#474) 2023-06-23 09:03:29 +00:00
SteveLauC
57546a07fc fix(pip3): prefer python when available (#471) 2023-06-23 09:02:58 +00:00
slowsage
d7709490ce fix: Run AstroUpdate before Lazy sync (#473) 2023-06-23 09:01:55 +00:00
slowsage
3e6c6e513b fix: handle no topgrade.toml but files in topgrade.d (#460) 2023-06-13 14:17:27 +00:00
SteveLauC
30858780cf refactor: unify the behavior of the steps that require sudo (#467) 2023-06-13 14:15:57 +00:00
SteveLauC
a7ddf4575a fix: fix Mist (#466) 2023-06-05 06:38:14 +00:00
Thomas Schönauer
470231c9d1 Revert "fix: fix mist" (#465)
Revert "fix: fix mist (#464)"

This reverts commit 282e336ac4.
2023-06-03 21:22:23 +00:00
SteveLauC
282e336ac4 fix: fix mist (#464) 2023-06-03 21:20:57 +00:00
SteveLauC
658829e4ff refactor: make update fn take &ExectionContext & put update fn together (#457) 2023-06-02 20:20:42 +00:00
SteveLauC
a0ff565220 docs: update CONTRIBUTING.md & config.example.toml (#458) 2023-06-01 11:02:39 +00:00
SteveLauC
7e48c5dedc fix: warn user about bad pattern paths before skipping step git (#456) 2023-06-01 07:16:01 +00:00
slowsage
03436b7f8f fix: Handle '# [include]'. Update default config (#450) 2023-06-01 07:15:49 +00:00
SteveLauC
3f5eedb83d fix: run AM without sudo (#454) 2023-05-31 07:01:45 +00:00
SteveLauC
234ad4bdd7 docs: add config-related CONTRIBUTING doc (#452) 2023-05-30 10:03:22 +00:00
slowsage
c7923393be fix: Write to correct config path when none exists. (#449) 2023-05-30 07:07:02 +00:00
slowsage
d4548b2f9a feat: Add arguments to pipupgrade and fix enable_pipupgrade check (#448) 2023-05-30 07:04:23 +00:00
SteveLauC
f6e8af186c feat: support Vanilla Linux (#447) 2023-05-29 11:45:11 +00:00
SteveLauC
58153635da refactor: remove Anarchy and Antergos as they are discontinued (#446) 2023-05-28 12:44:49 +00:00
SteveLauC
5358509825 fix: fix panic during container update (#445) 2023-05-27 14:12:45 +00:00
SteveLauC
1ab0232d96 feat: support deepin OS (#444) 2023-05-27 09:41:51 +00:00
SteveLauC
66860f1848 refactor: remove unnecessary qualification (#443) 2023-05-27 09:41:42 +00:00
SteveLauC
625f823f46 refactor: rename update fn name & some cleanup (#442) 2023-05-27 09:37:51 +00:00
Thomas Schönauer
6263ab7e10 Allow apt-get update to continue to apt-get upgrade with error code 100 (#440)
Allow apt-get update to continue with error code 100
2023-05-26 19:57:05 +00:00
Kevin Gavrois
7db991db9d Merge code for desktop notification between MacOS and Linux (#438) 2023-05-26 10:07:14 +02:00
SteveLauC
d75782892e docs: CONTRIBUTING.md (#439) 2023-05-26 09:34:20 +02:00
PolpOnline
cb7adc8ced Added ability to include directories as an extension of the config file (#421) 2023-05-25 12:22:11 +02:00
SteveLauC
7c3ba80270 fix: fix .NET language issue (#437)
Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2023-05-25 09:24:53 +02:00
SteveLauC
76c39edc8b refactor: make all step functions take &ExectutionContext (#436) 2023-05-25 09:09:23 +02:00
SteveLauC
c20a300eea fix: use --platform opt when pulling containers (#435) 2023-05-23 08:47:47 +02:00
SteveLauC
de3902a9c9 fix: use env ZSH to compose oh-my-zsh install dir (#434) 2023-05-22 14:06:19 +02:00
SteveLauC
8bca671e9f fix: run deb-get without sudo (#430) 2023-05-20 19:35:17 +02:00
MonstrousOgre
54301a6a17 Adding local pip-review (#433) 2023-05-20 19:33:59 +02:00
Cat Core
f06b7c0807 Differentiate NPM and PNPM steps in name (#431) 2023-05-20 11:33:41 +02:00
SteveLauC
43c02cf7a7 feat: support maza (#427) 2023-05-17 19:18:03 +02:00
SteveLauC
3a1568e884 feat: support oh-my-bash (#425) 2023-05-17 19:17:37 +02:00
SteveLauC
14753a14e7 feat: support AppMan (#423) 2023-05-09 08:03:06 +02:00
Sourajyoti Basak
227e8dcc8d feat(shell): add packer.nu (#414)
* feat(shell): add `packer.nu`

* dependency update (#413)

* fix(main): move `packer.nu` step before linux package managers

---------

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2023-05-05 11:01:24 +02:00
signed-log
97fd2b2718 Make zypper dist-upgrade opt-in on SLE/Leap (#417)
Make zypper dist-upgrade opt-in on SLE/Leap

- Create a `suse_dup` config option
- Create a new `Distribution::OpenSuseTumbleweed` object along with `upgrade_opensuse_tumbleweed()`
    * The purpose of it is to ignore the config option on Tumblweed as
      zypper `dup` is the only way to update a Tumbleweed
2023-05-05 10:24:01 +02:00
SteveLauC
f30e36d7bb feat: support stew (#422) 2023-05-05 10:17:42 +02:00
SteveLauC
d640bc66f5 docs: update README for alternative config path (#419) 2023-05-04 08:36:36 +00:00
PolpOnline
a2331a2575 Add the ability to have the config file in $XDG_CONFIG_HOME/topgrade/topgrade.toml (#418) 2023-05-03 19:53:52 +00:00
Thomas Schönauer
26a2c3c266 v11.0.2 version bump (#416)
* dependency update

* Cargo.toml version bump
2023-05-01 18:26:18 +00:00
Thomas Schönauer
ceafcba88f dependency update (#413) 2023-05-01 15:02:16 +00:00
Thomas Schönauer
d7182b5a6e v11.0.0 bump (#410) 2023-04-30 19:02:26 +00:00
Thomas Schönauer
93ec1172fe Update README.md 2023-04-30 18:58:22 +00:00
Thomas Schönauer
609477a373 Update README.md 2023-04-30 18:57:23 +00:00
Thomas Schönauer
1d49af10a7 Update README.md 2023-04-30 18:57:07 +00:00
Utkarsh Gupta
327ed837c2 Replace directories with home & etcetera (#407)
* Use global lazy HOME_DIR

* Remove unused base_dirs

* Use `etcetera` instead of `directories`

---------

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2023-04-30 18:32:13 +00:00
Thomas Schönauer
d406e2aeab Assume Fedora Silverblue based on os-release and not on existence of rpm-ostree (#393)
* Do not assume silverblue if rpm-ostree is available

* Fix typo

* Fix config error
2023-04-30 18:22:08 +00:00
dependabot[bot]
0991cc8a6f Bump enumflags2 from 0.7.5 to 0.7.7 (#408) 2023-04-24 20:37:00 +00:00
Brian Riccardi
ac6330fac8 Added support to 'mamba' (alternative to 'conda' with the exact same commands/interface) (#395) 2023-04-17 14:19:59 +00:00
dependabot[bot]
29f0d229d3 Bump h2 from 0.3.16 to 0.3.17 (#404) 2023-04-17 14:19:48 +00:00
Roey Darwish Dror
3dd11f7b52 No need to run self-update in Rustup (#403) 2023-04-05 12:42:47 +00:00
Roey Darwish Dror
ddb1a021bb Display the preamble in Linux only if notify-send is installed (#401) 2023-04-05 12:34:47 +00:00
PolpOnline
565aa405be Add no-self-update config and flag (#388) 2023-03-22 21:05:21 +00:00
Utkarsh Gupta
907465f891 run_custom_command: allow using interactive shell on unix (#383) 2023-03-17 16:28:58 +00:00
Trevor Sullivan
250485c826 Add Scoop manifest link for Windows installation (#384) 2023-03-15 07:40:31 +00:00
Thomas Schönauer
3a3f22b4e5 V10 3 2 bugfix + revert #347 (#382)
* Revert "run_custom_command: use interactive shell on unix (#347)"

This reverts commit d767ef31a5.

* v10-3-3 + revert of #347
2023-03-13 19:27:33 +00:00
Thomas de Queiroz Barros
a3628d0d49 Add sudo_command option (#379)
This allows the user to specify the preferred sudo command to be used
instead of the command chosen by Sudo::detect
2023-03-13 19:23:37 +00:00
Thomas Schönauer
462016e51e 10.3.2 patch (#378)
* 10.3.2 patch

* Clippy
2023-03-12 20:37:41 +00:00
Thomas Schönauer
199b81183b Update check-and-lint.yaml to use Rust version 1.68.0 2023-03-12 20:22:58 +00:00
Thomas Schönauer
342d7f7209 skip skip-notify warning on Win (#362) 2023-03-03 11:58:58 +00:00
Isaac Tay
9c2d121fc9 cargo: add cleanup step (using cargo-cache) (#371) 2023-03-03 11:58:15 +00:00
Roey Darwish Dror
7728819133 Support antidote (#368) 2023-02-26 21:45:43 +00:00
arctic-penguin
a5d5d987d2 pacdef: support new version 1.x (#364) 2023-02-23 22:01:53 +00:00
TGRCDev
fae5d80f0a pip3: Check for EXTERNALLY-MANAGED (PEP 668) (#367) 2023-02-23 22:01:26 +00:00
Thomas Schönauer
2369e371be apt: Recognise mist (#351) 2023-02-18 21:22:02 +00:00
Jason Stelzer
e3b71b647f Silence misleading warning on other platforms. (#353)

2023-02-07 17:21:15 +00:00
Guilherme Silva
e224ea38b3 CI: Update cross to v0.2.5 (#354) 2023-02-07 17:19:46 +00:00
Thomas Schönauer
8ec37bcd44 vim: Adds Astrovim support (#352) 2023-02-03 13:46:09 +00:00
Thomas Schönauer
6b7f6f4cc7 ruby_gems: Fixes asdf (#350) 2023-02-02 21:48:48 +00:00
Utkarsh Gupta
d767ef31a5 run_custom_command: use interactive shell on unix (#347) 2023-02-02 19:46:11 +00:00
Dan Sully
fcf776fe07 Add support for please (access elevation) (#310)
* Add support for please (access elevation)

Please is a sudo-like tool written in Rust.

https://gitlab.com/edneville/please

* Fixes code typo

---------

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
Co-authored-by: Thomas Schönauer <t.schoenauer@hgs-wt.at>
2023-02-02 19:22:56 +00:00
edi
58060dda09 use documented way of updating (#344) 2023-01-31 22:19:01 +00:00
Thomas Schönauer
8cfc8d66be v10.3.1 patch (#342) 2023-01-30 21:24:06 +00:00
edi
9dcc8fdd0d (neo)vim: topgrade should only invoke plugin managers not plugins (#341)
* fix upgrade order of (n)vim plugins

* treesitter should use the synchronous cmd

* add lazy pkg manager for neovim

* fix lazy cmd

* change calls

* add autocmd, remove ts and coc

* fix vimscript err invalid range

---------

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2023-01-30 18:42:13 +00:00
Thomas Schönauer
828477b255 Update README.md 2023-01-30 18:41:48 +00:00
arctic-penguin
4eae1fedf7 fix ignored config display_preamble = false (#340)
Bug was introduced in f1e4009. Fixes #337.
2023-01-30 18:41:00 +00:00
Thomas Schönauer
1051e4cf47 AM fix + version bump (#335) 2023-01-29 21:53:26 +00:00
Thomas Schönauer
80a95cb404 Clippy (#331) 2023-01-29 19:31:37 +00:00
Thomas Schönauer
ab630cfbc6 v10.2.5 release (#330)
* Don't show desktop notification on error (if `skip_notify = true`) (#275)

* Use ─ (U+2500) to draw borders (#282)

* Adds Pclinuxos support (#283)

* Add Devkitpro Pacman support (#291)

* Added support for Neovim package manager lazy.nvim (#293)

* Added support for lazy.nvim

From https://github.com/folke/lazy.nvim
Authored-by: Jacob Lane Ledbetter <jledbetter460@gmail.com>

* Make garuda-update update AUR packages by default (#296)

* fix(#298): Don't throw error if no Helm repository found (#305)

* Skip .NET when `dotnet tool list` is not successful (#302)

* feat(pacstall): add `-y` flag variant (#312)

* Add openSUSE MicroOS support (#315)

* Adds notify-send timeout of 10s (#318)

* Don't run yum when rpm-ostree is available (#313)

* don't run yum when rpm-ostree is available

* Clippy fix

* rpm-ostree: set default value to true

* Fixes if loop error

* Fixes gem update --system requires sudo now (#317)

* Fixes gem update --system requires sudo now

* rubygem: Adds arg -EH to sudo

* Use fixed nala path instead of which(nala) (#314)

* Adds notify-send bug warning when topgrade is run (#324)

* Adds notify-send bug warning when topgrade is run

* fix typo + clippy

* notify-send warning respects skip_notify flag

* nix: Adds additional arguments support (#325)

* Adds pip-review and pipupgrade support (#316)

* Adds pip-review and pipupgrade support

* Python: fixes pip_review and pipupgrade

* v10.2.5 patch (#329)

* WSL: Adds new wsl --update flags (#327)

* wsl: Updates available flags

* Clippy fix

* Add WslUpdate runner

* wsl: Code Typo

* wsl: Code Typos

* wsl: Code Typos

* wsl: Code Typo

* Adds AM Package Manager (#328)

* Adds AM Package Manager

* Clippy fixes

* Cargo fmt

* Moves am to linux only in main file

---------

Co-authored-by: Guilherme Silva <626206+guihkx@users.noreply.github.com>
Co-authored-by: Gabriel Augendre <gabriel@augendre.info>
Co-authored-by: Cat Core <34719527+arthurbambou@users.noreply.github.com>
Co-authored-by: Hugo Haas <hugoh@hugoh.net>
Co-authored-by: Baptiste <32563450+BapRx@users.noreply.github.com>
Co-authored-by: bbx0 <39773919+bbx0@users.noreply.github.com>
Co-authored-by: Sourajyoti Basak <wiz28@protonmail.com>
2023-01-29 19:19:27 +00:00
edi
c13e14080c Add Lazy, a Neovim plugin manager (#326)
* fix upgrade order of (n)vim plugins

* treesitter should use the synchronous cmd

* add lazy pkg manager for neovim

* fix lazy cmd

---------

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2023-01-29 18:49:56 +00:00
edi
4abbee99cc fix upgrade order of (n)vim plugins (#322)
* fix upgrade order of (n)vim plugins

* treesitter should use the synchronous cmd

---------

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2023-01-27 21:41:48 +00:00
Giovanni Merlino
45d935eda3 Fix missing separator for Pkgin (pkgsrc) (#307)
* Fix missing separator for Pkgin (pkgsrc)

* Fix whitespace (giving issues with cargo fmt?)

---------

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2023-01-27 21:34:27 +00:00
pwygab
b4c5efde50 Fix small typo in warning when notify-send fails (#319)
Fix small typo
2023-01-27 20:52:20 +00:00
Roey Darwish Dror
cba9dc1c2c Fix windows build (#303) 2023-01-09 08:50:21 +01:00
dependabot[bot]
938647123c Bump tokio from 1.8.5 to 1.18.4 (#301) 2023-01-06 23:13:01 +00:00
Jacob Lane Ledbetter
9f24f6474e Add dnf config to config example (#292) 2022-12-30 22:52:04 +00:00
Thomas Schönauer
814e39644c fixes dotnet update + version bump 10.2.4 (#274) 2022-12-18 15:37:10 +00:00
Thomas Schönauer
eb51be0732 10.2.3 release (#272) 2022-12-18 14:20:00 +00:00
Thomas Schönauer
56a717dcc6 Fixes dotnet if dotnet tool is not available (#229)
* Fixes dotnet if dotnet tool si not available

* dotnet: multi-lang support

* Fixes dotnet for non-english terminals
2022-12-18 14:12:36 +00:00
Thomas Schönauer
ee353ccb66 adds fish_update_completions to fish step (#270) 2022-12-18 14:11:36 +00:00
Thomas Schönauer
33cea0e5b6 10.2.3 release (#271)
10.2.3
2022-12-18 14:07:31 +00:00
Thomas Schönauer
78a491a976 Fixes rubygems on .deb distros (#268)
Fixes rubygem on systems with the .deb ruby package
2022-12-15 20:27:27 +00:00
Thomas Schönauer
9553be04e4 Fixes vcpkg when installed as root (#266) 2022-12-15 11:39:25 +00:00
Thomas Schönauer
81928f55a2 Fix notify not available (#257) 2022-12-15 11:34:18 +00:00
Mark Nefedov
4e56bf07f3 Add options to disable Yarn and Pnpm individually. (#260)
* Add options to disable Yarn and Pnpm individualy.

* cargo fmt

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-12-14 21:42:48 +00:00
sandal
9f424f03c3 Added a step for fetching and building treesitter grammars for helix (#263) 2022-12-14 08:12:39 +00:00
Thomas Kosiewski
1e14b3bf28 Add helm (#255) (#256)
* Add `helm` (#255)

Signed-off-by: Thomas Kosiewski <thoma471@googlemail.com>

* Sorted steps alphabetically

Signed-off-by: Thomas Kosiewski <thoma471@googlemail.com>

Signed-off-by: Thomas Kosiewski <thoma471@googlemail.com>
2022-12-08 21:47:57 +00:00
Thomas Schönauer
51e7a31f48 10.2.2 (#253) 2022-12-06 19:48:46 +00:00
Thomas Schönauer
9847fd9d4d Merge branch 'master' into dev 2022-12-06 19:23:45 +00:00
Thomas Schönauer
24ef44291f 10.2.2 version bump (#252) 2022-12-06 19:22:06 +00:00
Julien ITARD
ebb0c5a6d8 Fix RubyGems step (#251)
* Bump tokio from 1.5.1 to 1.8.4 (#245)

Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.5.1 to 1.8.4.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.5.1...tokio-1.8.4)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix RubyGems step

* Fix style

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-06 19:13:07 +00:00
Armel Soro
1dee462175 Fix misleading log message when detecting rbenv (#249) 2022-12-05 17:42:26 +00:00
Armel Soro
dc82b8b766 Fix issue with RubyGems update command (#248) 2022-12-05 13:16:03 +00:00
dependabot[bot]
73888e7669 Bump tokio from 1.5.1 to 1.8.4 (#245)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.5.1 to 1.8.4.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.5.1...tokio-1.8.4)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-30 21:56:52 +00:00
dependabot[bot]
46a010cc8f Bump tokio from 1.5.1 to 1.8.4 (#147)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.5.1 to 1.8.4.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.5.1...tokio-1.8.4)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-11-30 21:29:39 +00:00
Thomas Schönauer
96e4de4594 10.2.1 release (#244) 2022-11-30 21:08:15 +00:00
Thomas Schönauer
70616b2ec5 Version bump (#237) 2022-11-29 15:17:20 +00:00
Thomas Schönauer
22ab07d88e Fixes paru/yay -Pw return code error (#228) 2022-11-29 06:49:26 +00:00
Guilherme Silva
70045fca29 CI: Force color support for Rustfmt (#230)
Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-11-27 00:21:29 +01:00
Marcin Puc
f6ec6c76db Add shell completion and manpage generation (#233) 2022-11-26 19:42:35 +00:00
Jason Stelzer
37b900c56a Add garuda-update (#227) 2022-11-26 19:38:18 +00:00
Guilherme Silva
e26ec4d9e0 Fix clippy warning for DragonFly BSD (#231) 2022-11-26 17:52:14 +00:00
Rebecca Turner
b31778bdd8 Add Sudo type (#221)
* Create `Sudo` type and `SudoKind` enum

* Fix build

* reformat

* Fix choco on windows

* Fix linux

* Fix linux more

* more fix stuff hehe hoho hahaha

* more fix stuff hehe hoho hahaha

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-11-25 22:19:32 +00:00
Thomas Schönauer
526c4c9a58 Fixes typo 2022-11-25 18:44:36 +00:00
Julien ITARD
3c1dda0c39 Add RubyGems update (#217) 2022-11-24 19:21:03 +00:00
Ruben Molina
25c5057171 Add support for juliaup (#208)
* Add support for juliaup

* Update config.rs

* Change the position for Juliaup Step.

* Update generic.rs
2022-11-24 19:17:58 +00:00
Rebecca Turner
e456155562 Add pre_sudo option (#219)
* Add `pre_sudo` option
2022-11-24 19:15:43 +00:00
Guilherme Silva
f2c7e4848e CI: Install only necessary components (#218)
* CI: Install only necessary components
2022-11-24 19:15:13 +00:00
Guilherme Silva
b4407963ad Fix compilation on DragonFly BSD (#210) 2022-11-23 15:24:58 +00:00
Thomas Schönauer
85a4691229 10.2.0 dependency + version bump (#209) 2022-11-23 15:24:58 +00:00
Rebecca Turner
8e9c3cc56a Add gup (#203) 2022-11-23 15:24:58 +00:00
Thomas Schönauer
7cdaefe3b0 --yes flag support on freebsd (#198)
* Adds freebsd yes flag support

* Fixes freebsd update functions

* Missing semicolon

* Adds Step dependency to freebsd.rs

* Change freebsd to status_checked
2022-11-23 15:24:58 +00:00
Thomas Schönauer
aedb25cde6 Npm rework (#199) 2022-11-23 15:24:58 +00:00
Rebecca Turner
582bc737cb Fix a bug with status_checked_with_codes (#202) 2022-11-23 15:24:58 +00:00
Thomas Schönauer
c4091584c3 Fixes NPM failure under sudo (#197)
Fixes npm under sudo
2022-11-23 15:24:58 +00:00
Guilherme Silva
22ed1ef50a CI: Add NetBSD target (#180)
* Remove the `sys-info` crate

It offers much more features than we currently use.

Additionally, it was preventing me to cross-compile for NetBSD.

Since we were just using the `hostname()` function from the crate,
I went ahead and stole it.

* Add NetBSD target

* Fix FreeBSD clippy warnings
2022-11-23 15:24:58 +00:00
Rebecca Turner
41e2321b93 Use tracing (#174) 2022-11-23 15:24:58 +00:00
Rebecca Turner
d8add139e1 Add Treesitter support to Vim upgrade (#184)
* Add `:TSUpdate` to Vim upgrade

* Reformat `freebsd.rs`

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-11-23 15:24:58 +00:00
Thomas Schönauer
04a8d960a9 Revert " Fix unattended package upgrades on FreeBSD" (#188)
Revert " Fix unattended package upgrades on FreeBSD (#181)"

This reverts commit a0ad83a58b.
2022-11-23 15:24:58 +00:00
Yonas Yanfa
2c6a8f73fa Fix unattended package upgrades on FreeBSD (#181)
* Fix unattended package upgrades on FreeBSD

* Fix unattended package upgrades on FreeBSD

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-11-23 15:24:58 +00:00
Rebecca Turner
71883d7164 Fix tmux panic (#165)
Fix `tmux` sessions

This will create a new session named `topgrade`, `topgrade-1`,
`topgrade-2`, using the first nonexistent session name it finds. That
session will have a window in it named `topgrade` in which `topgrade` is
run. If `topgrade --tmux` is being run from within tmux, it won't attach
to the new tmux session. If the user is not currently in tmux, it will
attach to the newly-created session.

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
2022-11-23 15:24:58 +00:00
Rebecca Turner
d4fe748814 Run fisher if fish -c 'type -t fisher' is OK (#172) 2022-11-23 15:24:58 +00:00
LeSnake04
369d923532 Added configuration for cargo-deb and cargo-generate-rpm (#178) 2022-11-23 15:24:58 +00:00
Guilherme Silva
6be4a4a48d CI improvements + Android and FreeBSD targets (#177)
* Bump minimum Rust version to 1.60

As required by the `time` crate (`notify-rust` > `mac-notification-sys` > `time`).

* Simplify CI

Changes:

- Bump `actions/checkout` to v3, fixing a bunch of warnings.
- Replace unmaintained `actions-rs/cargo` by `dtolnay/rust-toolchain`.
- Run Rustfmt only once.
- Add support for cached dependencies (via `Swatinem/rust-cache`).

* Add Android target

Use the awesome `cross` tool for cross-compiling!

* Add FreeBSD target
2022-11-23 15:24:58 +00:00
Guilherme Silva
7442ddd386 Further clippy fixes (#176) 2022-11-23 15:24:58 +00:00
Guilherme Silva
761ffac127 Fix compilation on FreeBSD (#175) 2022-11-23 15:24:58 +00:00
Guilherme Silva
5ca1dc3703 Fix compilation on Android (#166)
Co-authored-by: guihkx <guihkx@users.noreply.github.com>
2022-11-23 15:24:58 +00:00
Rebecca Turner
a18c6e815c Fix lack of separators for pnpm/npm/yarn (#170) 2022-11-23 15:24:58 +00:00
Rebecca Turner
022afab1ca Print errors when steps fail (#171) 2022-11-23 15:24:58 +00:00
Rebecca Turner
2cbb7db66d Use color_eyre (#173) 2022-11-23 15:24:58 +00:00
Rebecca Turner
e84173be8f Add CommandExt trait (#146)
* Color CI output

* Improve `CommandExt`

* Add comments explaining `#[allow]`s

* Remove useless `dead_code` annotation

* Improve error messages

* Print errors when running a shell errors

* fixup! Remove useless `dead_code` annotation
2022-11-23 15:24:58 +00:00
Thomas Schönauer
bd34a3bcd4 Revert "10.2.0 release " (#215)
Revert "10.2.0 release  (#213)"

This reverts commit 13076fcef6.
2022-11-23 16:23:00 +01:00
Thomas Schönauer
13076fcef6 10.2.0 release (#213) 2022-11-23 15:18:09 +00:00
0xMRTT
6f48017761 Create SECURITY.md (#142) 2022-11-16 19:40:17 +01:00
Marcin Puc
7fb07879bb Add repology badge (#191) 2022-11-16 19:39:36 +01:00
Pascal Jufer
ecbaf52156 Enhance the README (#186) 2022-11-15 08:41:35 +01:00
Thomas Schönauer
6a6a84b0c5 Update update_homebrew.yml 2022-11-06 15:28:57 +00:00
Thomas Schönauer
3486200b2c Update update_homebrew.yml 2022-11-06 15:08:35 +00:00
Thomas Schönauer
f6b3a8fdca Update update_homebrew.yml 2022-11-06 15:07:55 +00:00
Thomas Schönauer
058a6fd9c9 Update update_homebrew.yml 2022-11-06 15:01:20 +00:00
Thomas Schönauer
e1783e3af8 Create update_homebrew.yml 2022-11-06 14:58:47 +00:00
Thomas Schönauer
e161d3cd3c 10.1.2 (#162)
* Closes #150 please disable distrobox by default (#151)

* Check if distrobox exists before running step

* Improve help prompt value names (#153)

* 159 self update error message with standalone versions (#161)

* Rename back to topgrade

* Bugfix Version bump

* Changes reference to topgrade-rs in self-update

* Fixes distrobox errors (#160)

* Rename back to topgrade

* Bugfix Version bump

* Check if distrobox exists before running step

* Fixed sitrobox and version bump

* Version bump to 10.1.2

Co-authored-by: Marcin Puc <tranzystorek.io@protonmail.com>
2022-11-06 13:54:38 +00:00
Marcin Puc
318f935b43 Add info about Void Linux to README (#154) 2022-11-05 14:43:09 +00:00
Thomas Schönauer
3dc245245d bugfix Closing #150 (#151) (#152)
Closes #150 please disable distrobox by default (#151)

* Check if distrobox exists before running step
2022-11-05 10:50:55 +00:00
Thomas Schönauer
9de111aa0f Merge of bug-fixes with masters the second (#149) 2022-11-05 09:17:51 +00:00
Thomas Schönauer
18951c8447 Merge branch 'master' into bug-fixes 2022-11-05 09:17:28 +00:00
Thomas Schönauer
b6e50a38af Revert "Bug fixes" (#148)
Revert "Bug fixes (#145)"

This reverts commit 8acdfc8d1c.
2022-11-05 09:15:52 +00:00
Rebecca Turner
8acdfc8d1c Bug fixes (#145) 2022-11-05 09:11:31 +00:00
Dylan M. Taylor
a6da5181f2 Fix for gcloud snap issue (#144) 2022-11-04 13:48:05 +00:00
0xMRTT
60ff087048 feat: add coc (#140) 2022-11-04 12:08:23 +00:00
Thomas Schönauer
3ebaac3a1d Fix windows clippy errors (#135)
* Changes windows get_wsl_distribution argument

* changes in get_wsl_distributions

* changes in run_wsl_topgrade due to clippy errors

* Resolves needless borrow
2022-11-03 21:43:45 +00:00
Thomas Schönauer
e4f8488837 Revert "Get windows to finish clippy without errors" (#134)
Revert "Get windows to finish clippy without errors (#133)"

This reverts commit 16953409fd.
2022-11-03 19:49:01 +00:00
Thomas Schönauer
d8748b004b Update check-and-lint.yaml 2022-11-03 19:40:24 +00:00
Thomas Schönauer
2a11df40ee Update check-and-lint.yaml 2022-11-03 19:40:02 +00:00
Thomas Schönauer
16953409fd Get windows to finish clippy without errors (#133)
* Changes windows get_wsl_distribution argument

* changes in get_wsl_distributions

* changes in run_wsl_topgrade due to clippy errors
2022-11-03 19:34:27 +00:00
Thomas Schönauer
c85adb8980 Update check-and-lint.yaml 2022-11-03 19:19:00 +00:00
Thomas Schönauer
0f0cbc1453 Update check-and-lint.yaml 2022-11-03 19:18:45 +00:00
Thomas Schönauer
91554cac56 GitHub action cleanup2 (#132)
* Changed clippy args for PR pipeline

* changes crates-publish toolchain version to stable

* Enhanced clippy for PRs

* Fixes typo
2022-11-03 19:09:44 +00:00
Thomas Schönauer
7256aaffc8 Resolve clippy errors (#131)
* Resolves clippy errors

* Fixes clippy errors

* Changes get_wsl_distributions arguments from pointer to value
2022-11-03 19:04:06 +00:00
99 changed files with 9846 additions and 3744 deletions

View File

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

View File

@@ -2,16 +2,20 @@
name: Feature request name: Feature request
about: Can you please support...? about: Can you please support...?
title: '' title: ''
labels: '' labels: 'C-feature request'
assignees: '' assignees: ''
--- ---
## I want to suggest a new step ## I want to suggest a new step
### Which tool is this about? Where is its repository?
### Which operating systems are supported by this tool? * Which tool is this about? Where is its repository?
### What should Topgrade do to figure out if the tool needs to be invoked? * Which operating systems are supported by this tool?
### Which exact commands should Topgrade run? * What should Topgrade do to figure out if the tool needs to be invoked?
* Which exact commands should Topgrade run?
* Does it have a `--dry-run` option? i.e., print what should be done and exit
* Does it need the user to confirm the execution? And does it provide a `--yes`
option to skip this step?
## I want to suggest some general feature ## I want to suggest some general feature
Topgrade should... Topgrade should...

View File

@@ -1,12 +1,18 @@
## Standards checklist: ## What does this PR do
## Standards checklist
- [ ] The PR title is descriptive. - [ ] The PR title is descriptive.
- [ ] The code compiles (`cargo build`) - [ ] I have read `CONTRIBUTING.md`
- [ ] The code passes rustfmt (`cargo fmt`)
- [ ] The code passes clippy (`cargo clippy`)
- [ ] The code passes tests (`cargo test`)
- [ ] *Optional:* I have tested the code myself - [ ] *Optional:* I have tested the code myself
- [ ] I also tested that Topgrade skips the step where needed
## For new steps
- [ ] *Optional:* Topgrade skips this step where needed
- [ ] *Optional:* The `--dry-run` option works with this step
- [ ] *Optional:* The `--yes` option works with this step if it is supported by
the underlying command
If you developed a feature or a bug fix for someone else and you do not have the If you developed a feature or a bug fix for someone else and you do not have the
means to test it, please tag this person here. means to test it, please tag this person here.

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
# Set update schedule for GitHub Actions
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
# Check for updates to GitHub Actions every week
interval: "weekly"

View File

@@ -1,66 +0,0 @@
on:
pull_request:
push:
branches:
- main
name: Check and Lint
jobs:
check:
name: Check
matrix:
platform: [ ubuntu-latest, macos-latest, windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: check
fmt:
name: Rustfmt
matrix:
platform: [ ubuntu-latest, macos-latest, windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Clippy
matrix:
platform: [ ubuntu-latest, macos-latest, windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-targets --locked -- -D warnings
name: Clippy Output
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-targets --locked --all-features -- -D warnings
name: Clippy (All features) Output

View File

@@ -0,0 +1,21 @@
name: Check config file creation if not exists
on:
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
TestConfig:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
CONFIG_PATH=~/.config/topgrade.toml;
if [ -f "$CONFIG_PATH" ]; then rm $CONFIG_PATH; fi
cargo build;
TOPGRADE_SKIP_BRKC_NOTIFY=true ./target/debug/topgrade --dry-run --only system;
stat $CONFIG_PATH;

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

@@ -0,0 +1,32 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Check Security Vulnerability
on:
pull_request:
push:
branches:
- main
jobs:
lint:
name: DevSkim
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run DevSkim scanner
uses: microsoft/DevSkim-Action@v1
- name: Upload DevSkim scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: devskim-results.sarif

View File

@@ -8,7 +8,7 @@ jobs:
prepare: prepare:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
toolchain: nightly-2022-08-03 toolchain: nightly-2022-08-03

87
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
on:
pull_request:
push:
branches:
- main
name: CI
env:
CROSS_VER: '0.2.5'
CARGO_NET_RETRY: 3
jobs:
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run cargo fmt
env:
TERM: xterm-256color
run: |
cargo fmt --all -- --check
main:
needs: fmt
name: ${{ matrix.target_name }} (check, clippy)
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-linux-android
target_name: Android
use_cross: true
os: ubuntu-latest
- target: x86_64-unknown-freebsd
target_name: FreeBSD
use_cross: true
os: ubuntu-latest
- target: x86_64-unknown-linux-gnu
target_name: Linux
os: ubuntu-latest
- target: x86_64-apple-darwin
target_name: macOS-x86_64
os: macos-13
- target: aarch64-apple-darwin
target_name: macOS-aarch64
os: macos-latest
- target: x86_64-unknown-netbsd
target_name: NetBSD
use_cross: true
os: ubuntu-latest
- target: x86_64-pc-windows-msvc
target_name: Windows
os: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Rust Cache
uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.target }}
- name: Setup cross
if: matrix.use_cross == true
run: curl -fL --retry 3 https://github.com/cross-rs/cross/releases/download/v${{ env.CROSS_VER }}/cross-x86_64-unknown-linux-musl.tar.gz | tar vxz -C /usr/local/bin
- name: Run cargo/cross check
run: ${{ matrix.use_cross == true && 'cross' || 'cargo' }} check --locked --target ${{ matrix.target }}
- name: Run cargo/cross clippy
run: ${{ matrix.use_cross == true && 'cross' || 'cargo' }} clippy --locked --target ${{ matrix.target }} --all-features -- -D warnings
- name: Run cargo test
# ONLY run test with cargo
if: matrix.use_cross == false
run: cargo test --locked --target ${{ matrix.target }}

View File

@@ -0,0 +1,88 @@
name: Publish release files for CD native environments
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
release:
types: [ created ]
jobs:
build:
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-latest, macos-latest, macos-13, windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Install cargo-deb
run: cargo install cargo-deb
if: ${{ matrix.platform == 'ubuntu-latest' }}
shell: bash
- name: Check format
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --all-targets --locked -- -D warnings
- name: Run clippy (All features)
run: cargo clippy --all-targets --locked --all-features -- -D warnings
- name: Run tests
run: cargo test
- name: Build in Release profile with all features enabled
run: cargo build --release --all-features
- name: Rename Release (Unix)
run: |
cargo install default-target
mkdir -p assets
FILENAME=topgrade-${{github.event.release.tag_name}}-$(default-target)
mv target/release/topgrade assets
cd assets
tar --format=ustar -czf $FILENAME.tar.gz topgrade
rm topgrade
ls .
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
mkdir assets
FILENAME=topgrade-${{github.event.release.tag_name}}-$(default-target)
mv target/release/topgrade.exe assets/topgrade.exe
cd assets
powershell Compress-Archive -Path * -Destination ${FILENAME}.zip
rm topgrade.exe
ls .
if: ${{ matrix.platform == 'windows-latest' }}
shell: bash
- name: Release
uses: softprops/action-gh-release@v2
with:
files: assets/*

View File

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

View File

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

View File

@@ -1,77 +0,0 @@
name: Publish release files for CD native environments
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
release:
types: [ created ]
jobs:
build:
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-latest, macos-latest, windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
components: rustfmt, clippy
- uses: actions-rs/cargo@v1.0.1
name: Check format
with:
command: fmt
args: --all -- --check
- uses: actions-rs/cargo@v1.0.1
name: Run clippy
with:
command: clippy
args: --all-targets --locked -- -D warnings
- uses: actions-rs/cargo@v1.0.1
name: Run clippy (All features)
with:
command: clippy
args: --all-targets --locked --all-features -- -D warnings
- uses: actions-rs/cargo@v1.0.1
name: Run tests
with:
command: test
- uses: actions-rs/cargo@v1.0.1
name: Build
with:
command: build
args: --release --all-features
- name: Rename Release (Unix)
run: |
cargo install default-target
mkdir assets
FILENAME=topgrade-${{github.event.release.tag_name}}-$(default-target)
mv target/release/topgrade assets
cd assets
tar --format=ustar -czf $FILENAME.tar.gz topgrade
rm topgrade
ls .
if: ${{ matrix.platform != 'windows-latest' }}
shell: bash
- name: Rename Release (Windows)
run: |
cargo install default-target
mkdir assets
FILENAME=topgrade-${{github.event.release.tag_name}}-$(default-target)
mv target/release/topgrade.exe assets/topgrade.exe
cd assets
powershell Compress-Archive -Path * -Destination ${FILENAME}.zip
rm topgrade.exe
ls .
if: ${{ matrix.platform == 'windows-latest' }}
shell: bash
- name: Release
uses: softprops/action-gh-release@v1
with:
files: assets/*

View File

@@ -14,8 +14,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Publish AUR package - name: Publish AUR package
uses: ATiltedTree/create-aur-release@v1 uses: aksh1618/update-aur-package@v1.0.5
with: with:
tag_version_prefix: v
package_name: topgrade package_name: topgrade
commit_username: "Thomas Schönauer" commit_username: "Thomas Schönauer"
commit_email: t.schoenauer@hgs-wt.at commit_email: t.schoenauer@hgs-wt.at

View File

@@ -4,7 +4,7 @@ on:
# types: # types:
# - completed # - completed
release: release:
types: [published, edited] types: [published]
name: Publish to crates.io on release name: Publish to crates.io on release
@@ -12,7 +12,7 @@ jobs:
prepare: prepare:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
toolchain: stable toolchain: stable
@@ -21,7 +21,7 @@ jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: katyo/publish-crates@v1 - uses: katyo/publish-crates@v2
with: with:
dry-run: true dry-run: true
check-repo: ${{ github.event_name == 'push' }} check-repo: ${{ github.event_name == 'push' }}

View File

@@ -0,0 +1,39 @@
name: Publish to Homebrew
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
workflow_dispatch:
push:
tags:
- "v*"
jobs:
homebrew-publish:
runs-on: ubuntu-latest
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Cache Homebrew Bundler RubyGems
id: cache
uses: actions/cache@v4
with:
path: ${{ steps.set-up-homebrew.outputs.gems-path }}
key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}
restore-keys: ${{ runner.os }}-rubygems-
- name: Install Homebrew Bundler RubyGems
if: steps.cache.outputs.cache-hit != 'true'
run: brew install-bundler-gems
- name: Bump formulae
uses: Homebrew/actions/bump-packages@master
continue-on-error: true
with:
# Custom GitHub access token with only the 'public_repo' scope enabled
token: ${{secrets.HOMEBREW_ACCESS_TOKEN}}
# Bump only these formulae if outdated
formulae: |
topgrade

99
.github/workflows/release_to_pypi.yml vendored Normal file
View File

@@ -0,0 +1,99 @@
name: Update PyPi
on:
release:
types: [published]
permissions:
contents: read
jobs:
linux:
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64, x86, aarch64]
steps:
- uses: actions/checkout@v4
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist
sccache: 'true'
manylinux: auto
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
windows:
runs-on: windows-latest
strategy:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@v4
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
macos:
runs-on: macos-latest
strategy:
matrix:
target: [x86_64, aarch64]
steps:
- uses: actions/checkout@v4
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
- name: Upload sdist
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
release:
name: Release
runs-on: ubuntu-latest
if: "startsWith(github.ref, 'refs/tags/')"
needs: [linux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v3
with:
name: wheels
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
command: upload
args: --skip-existing *

13
.github/workflows/release_to_winget.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Publish to WinGet
on:
release:
types: [released]
jobs:
publish:
runs-on: windows-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@main
with:
identifier: topgrade-rs.topgrade
max-versions-to-keep: 5 # keep only latest 5 versions
token: ${{ secrets.WINGET_TOKEN }}

View File

@@ -1,59 +0,0 @@
on:
pull_request:
push:
branches:
- main
env:
CARGO_TERM_COLOR: always
name: Test with Code Coverage
jobs:
test:
name: Test
env:
PROJECT_NAME_UNDERSCORE: topgrade
CARGO_INCREMENTAL: 0
RUSTFLAGS: -Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort
RUSTDOCFLAGS: -Cpanic=abort
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
- name: Cache dependencies
uses: actions/cache@v2
env:
cache-name: cache-dependencies
with:
path: |
~/.cargo/.crates.toml
~/.cargo/.crates2.json
~/.cargo/bin
~/.cargo/registry/index
~/.cargo/registry/cache
target
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('Cargo.lock') }}
- name: Generate test result and coverage report
run: |
cargo install cargo2junit grcov;
cargo test $CARGO_OPTIONS -- -Z unstable-options --format json | cargo2junit > results.xml;
zip -0 ccov.zip `find . \( -name "$PROJECT_NAME_UNDERSCORE*.gc*" \) -print`;
grcov ccov.zip -s . -t lcov --llvm --ignore-not-existing --ignore "/*" --ignore "tests/*" -o lcov.info;
- name: Upload test results
uses: EnricoMi/publish-unit-test-result-action@v1
with:
check_name: Test Results
github_token: ${{ secrets.GITHUB_TOKEN }}
files: results.xml
- name: Upload to CodeCov
uses: codecov/codecov-action@v1
with:
# required for private repositories:
# token: ${{ secrets.CODECOV_TOKEN }}
files: ./lcov.info
fail_ci_if_error: true

18
.gitignore vendored
View File

@@ -1,4 +1,20 @@
# JetBrains IDEs
.idea/
/target # Visual Studio
.vs/
# Visual Studio Code
.vscode/
# Generic build outputs
/build /build
# Specific for some languages like Rust
/target
# LLVM profiling output
*.profraw
# Backup files for any .rs files in the project
**/*.rs.bk **/*.rs.bk

38
.vscode/launch.json vendored
View File

@@ -1,38 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Topgrade",
"console": "integratedTerminal",
"cargo": {
"args": [
"build",
"--bin=topgrade-rs",
"--package=topgrade-rs"
],
"filter": {
"name": "topgrade-rs",
"kind": "bin"
}
},
"args": [
"--only",
"${input:step}",
"-v"
],
"cwd": "${workspaceFolder}"
},
],
"inputs": [
{
"type": "promptString",
"id": "step",
"description": "step name",
}
]
}

14
.vscode/tasks.json vendored
View File

@@ -1,14 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "cargo",
"command": "clippy",
"problemMatcher": [
"$rustc"
],
"group": "test",
"label": "rust: cargo clippy"
}
]
}

View File

@@ -1,50 +0,0 @@
{
// Place your topgrade workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"Skip Step": {
"scope": "rust",
"prefix": "skipstep",
"body": [
"return Err(SkipStep(format!(\"$1\")).into());"
]
},
"Step": {
"scope": "rust",
"prefix": "step",
"body": [
"pub fn $1(ctx: &ExecutionContext) -> Result<()> {",
" $0",
" Ok(())",
"}"
]
},
"Require Binary": {
"scope": "rust",
"prefix": "req",
"description": "Require a binary to be installed",
"body": [
"let ${1:binary} = require(\"${1:binary}\")?;"
]
},
"macos": {
"scope": "rust",
"prefix": "macos",
"body": [
"#[cfg(target_os = \"macos\")]"
]
}
}

6
BREAKINGCHANGES.md Normal file
View File

@@ -0,0 +1,6 @@
# Containers step
* New default behavior: In the previous versions, if you have both Docker and
Podman installed, Podman will be used by Topgrade. Now the default option
has been changed to Docker. This can be overridden by setting the
`containers.runtime` option in the configuration TOML to "podman".

0
BREAKINGCHANGES_dev.md Normal file
View File

128
CODE_OF_CONDUCT.md Normal file
View File

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

152
CONTRIBUTING.md Normal file
View File

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

2974
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,12 @@ name = "topgrade"
description = "Upgrade all the things" description = "Upgrade all the things"
categories = ["os"] categories = ["os"]
keywords = ["upgrade", "update"] keywords = ["upgrade", "update"]
license-file = "LICENSE" license = "GPL-3.0"
repository = "https://github.com/topgrade-rs/topgrade" repository = "https://github.com/topgrade-rs/topgrade"
version = "10.1.0" rust-version = "1.76.0"
version = "16.0.1"
authors = ["Roey Darwish Dror <roey.ghost@gmail.com>", "Thomas Schönauer <t.schoenauer@hgs-wt.at>"] authors = ["Roey Darwish Dror <roey.ghost@gmail.com>", "Thomas Schönauer <t.schoenauer@hgs-wt.at>"]
exclude = ["doc/screenshot.gif"] exclude = ["doc/screenshot.gif", "BREAKINGCHANGES_dev.md"]
edition = "2021" edition = "2021"
readme = "README.md" readme = "README.md"
@@ -21,41 +22,63 @@ path = "src/main.rs"
[dependencies] [dependencies]
home = "~0.5" home = "~0.5"
directories = "~4.0" etcetera = "~0.8"
once_cell = "~1.19"
serde = { version = "~1.0", features = ["derive"] } serde = { version = "~1.0", features = ["derive"] }
toml = "0.5" toml = "0.8"
which_crate = { version = "~4.1", package = "which" } which_crate = { version = "~6.0", package = "which" }
shellexpand = "~2.1" shellexpand = "~3.1"
clap = { version = "~3.1", features = ["cargo", "derive"] } clap = { version = "~4.5", features = ["cargo", "derive"] }
log = "~0.4" clap_complete = "~4.5"
walkdir = "~2.3" clap_mangen = "~0.2"
walkdir = "~2.5"
console = "~0.15" console = "~0.15"
lazy_static = "~1.4" lazy_static = "~1.4"
chrono = "~0.4" chrono = "~0.4"
pretty_env_logger = "~0.4"
glob = "~0.3" glob = "~0.3"
strum = { version = "~0.24", features = ["derive"] } strum = { version = "~0.26", features = ["derive"] }
thiserror = "~1.0" thiserror = "~1.0"
anyhow = "~1.0" tempfile = "~3.10"
tempfile = "~3.2"
cfg-if = "~1.0" cfg-if = "~1.0"
tokio = { version = "~1.5", features = ["process", "rt-multi-thread"] } tokio = { version = "~1.38", features = ["process", "rt-multi-thread"] }
futures = "~0.3" futures = "~0.3"
regex = "~1.5" regex = "~1.10"
sys-info = "~0.9"
semver = "~1.0" semver = "~1.0"
shell-words = "~1.1" shell-words = "~1.1"
color-eyre = "~0.6"
tracing = { version = "~0.1", features = ["attributes", "log"] }
tracing-subscriber = { version = "~0.3", features = ["env-filter", "time"] }
merge = "~0.1"
regex-split = "~0.1"
notify-rust = "~4.11"
wildmatch = "2.3.0"
rust-i18n = "3.0.1"
sys-locale = "0.3.1"
[target.'cfg(target_os = "macos")'.dependencies] [package.metadata.generate-rpm]
notify-rust = "~4.5" assets = [{ source = "target/release/topgrade", dest = "/usr/bin/topgrade" }]
[package.metadata.generate-rpm.requires]
git = "*"
[package.metadata.deb]
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] [target.'cfg(unix)'.dependencies]
nix = "~0.24" nix = { version = "~0.29", features = ["hostname", "signal", "user"] }
rust-ini = "~0.18" rust-ini = "~0.21"
self_update_crate = { version = "~0.30", default-features = false, optional = true, package = "self_update", features = ["archive-tar", "compression-flate2", "rustls"] } self_update_crate = { version = "~0.40", default-features = false, optional = true, package = "self_update", features = ["archive-tar", "compression-flate2", "rustls"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
self_update_crate = { version = "~0.30", default-features = false, optional = true, package = "self_update", features = ["archive-zip", "compression-zip-deflate", "rustls"] } self_update_crate = { version = "~0.40", default-features = false, optional = true, package = "self_update", features = ["archive-zip", "compression-zip-deflate", "rustls"] }
winapi = "~0.3" winapi = "~0.3"
parselnk = "~0.1" parselnk = "~0.1"

View File

@@ -1,16 +1,17 @@
<div align="center"> <div align="center">
<img alt="Topgrade" src="doc/topgrade.png" width="850px"> <h1>
<img alt="Topgrade" src="doc/topgrade_transparent.png" width="850px">
</h1>
<!-- <a href="https://github.com/topgrade-rs/topgrade/releases"><img alt="GitHub Release" src="https://img.shields.io/github/release/topgrade-rs/topgrade.svg"></a>
<a href="https://github.com/topgrade-rs/topgrade/releases"><img alt="GitHub Release" src="https://img.shields.io/github/release/r-darwish/topgrade.svg"></a>
<a href="https://crates.io/crates/topgrade"><img alt="crates.io" src="https://img.shields.io/crates/v/topgrade.svg"></a> <a href="https://crates.io/crates/topgrade"><img alt="crates.io" src="https://img.shields.io/crates/v/topgrade.svg"></a>
<a href="https://aur.archlinux.org/packages/topgrade"><img alt="AUR" src="https://img.shields.io/aur/version/topgrade.svg"></a> <a href="https://aur.archlinux.org/packages/topgrade"><img alt="AUR" src="https://img.shields.io/aur/version/topgrade.svg"></a>
<a href="https://formulae.brew.sh/formula/topgrade"><img alt="Homebrew" src="https://img.shields.io/homebrew/v/topgrade.svg"></a> <a href="https://formulae.brew.sh/formula/topgrade"><img alt="Homebrew" src="https://img.shields.io/homebrew/v/topgrade.svg"></a>
-->
<img alt="Demo" src="doc/screenshot.gif" width="550px"> <img alt="Demo" src="doc/topgrade_demo.gif">
</div> </div>
## Introduction ## Introduction
> **Note** > **Note**
@@ -22,48 +23,78 @@ To remedy this, **Topgrade** detects which tools you use and runs the appropriat
## Installation ## Installation
- Arch Linux: [AUR](https://aur.archlinux.org/packages/topgrade) package. [![Packaging status](https://repology.org/badge/vertical-allrepos/topgrade.svg)](https://repology.org/project/topgrade/versions)
- NixOS: _topgrade_ package in `nixpkgs`.
- macOS: [Homebrew](https://formulae.brew.sh/formula/topgrade) or [MacPorts](https://ports.macports.org/port/topgrade/).
Other systems users can either use `cargo install` or use the compiled binaries from the release page. - Arch Linux: [AUR](https://aur.archlinux.org/packages/topgrade)
- NixOS: [Nixpkgs](https://search.nixos.org/packages?show=topgrade)
- Void Linux: [XBPS](https://voidlinux.org/packages/?arch=x86_64&q=topgrade)
- macOS: [Homebrew](https://formulae.brew.sh/formula/topgrade) or [MacPorts](https://ports.macports.org/port/topgrade/)
- Windows: [Chocolatey][choco], [Scoop][scoop] or [Winget][winget]
- PyPi: [pip](https://pypi.org/project/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. The compiled binaries contain a self-upgrading feature.
Topgrade requires Rust 1.51 or above.
## Documentation
> **Warning**
> Work in Progress
You can visit the documentation at [topgrade-rs.github.io](https://topgrade-rs.github.io/) .
## Usage ## Usage
Just run `topgrade`. Just run `topgrade`.
See [the documentation](https://topgrade-rs.github.io/) for the list of things Topgrade supports.
## Customization ## Configuration
See `config.example.toml` for an example configuration file. See `config.example.toml` for an example configuration file.
### Configuration path ## Migration and Breaking Changes
The configuration should be placed in the following paths depending by the operating system: Whenever there is a **breaking change**, the major version number will be bumped,
and we will document these changes in the release note, please take a look at
it when updated to a major release.
- **Windows** - `%APPDATA%/topgrade.toml` > Got a question? Feel free to open an issue or discussion!
- **macOS** and **other Unix systems** - `${XDG_CONFIG_HOME:-~/.config}/topgrade.toml`
### Configuration Path
#### `CONFIG_DIR` on each platform
- **Windows**: `%APPDATA%`
- **macOS** and **other Unix systems**: `${XDG_CONFIG_HOME:-~/.config}`
`topgrade` will look for the configuration file in the following places, in order of priority:
1. `CONFIG_DIR/topgrade.toml`
2. `CONFIG_DIR/topgrade/topgrade.toml`
If the file with higher priority is present, no matter it is valid or not, the other configuration files will be ignored.
On the first run(no configuration file exists), `topgrade` will create a configuration file at `CONFIG_DIR/topgrade.toml` for you.
### Custom Commands
Custom commands can be defined in the config file which can be run before, during, or after the inbuilt commands, as required.
By default, the custom commands are run using a new shell according to the `$SHELL` environment variable on unix (falls back to `sh`) or `pwsh` on windows (falls back to `powershell`).
On unix, if you want to run your command using an interactive shell, for example to source your shell's rc files, you can add `-i` at the start of your custom command.
But note that this requires the command to exit the shell correctly or else the shell will hang indefinitely.
## Remote Execution
You can specify a key called `remote_topgrades` in the configuration file.
This key should contain a list of hostnames that have Topgrade installed on them.
Topgrade will use `ssh` to run `topgrade` on remote hosts before acting locally.
To limit the execution only to specific hosts use the `--remote-host-limit` parameter.
## Contribution ## Contribution
### Problems or missing features? ### Problems or missing features?
Open a new Issue describing your problem and if possible with a possible solution. Open a new issue describing your problem and if possible provide a solution.
### Missing a feature or found an unsupported tool/distro? ### Missing a feature or found an unsupported tool/distro?
Just let us now what you are missing by opening an issue. Just let us now what you are missing by opening an issue.
For tools please open an Issue describing the tool, which platforms it supports and if possible, give us an example of its usage. For tools, please open an issue describing the tool, which platforms it supports and if possible, give us an example of its usage.
### Want to contribute to the code? ### Want to contribute to the code?
@@ -71,18 +102,9 @@ Just fork the repository and start coding.
### Contribution Guidelines ### Contribution Guidelines
- Check if your code passes `cargo fmt` and `cargo clippy`. See [CONTRIBUTING.md](https://github.com/topgrade-rs/topgrade/blob/master/CONTRIBUTING.md)
- Check if your code is self explanatory, if not it should be documented by comments.
- Make a Pull Request to the dev branch for new features or to the bug-fixes branch for bug fixes.
## Remote execution ## Roadmap
You can specify a key called `remote_topgrades` in the configuration file.
This key should contain a list of hostnames that have Topgrade installed on them.
Topgrade will use `ssh` to run `topgrade` on remote hosts before acting locally.
To limit the execution only to specific hosts use the `--remote-host-limit` parameter.
## ToDo
- [ ] Add a proper testing framework to the code base. - [ ] Add a proper testing framework to the code base.
- [ ] Add unit tests for package managers. - [ ] Add unit tests for package managers.

65
RELEASE_PROCEDURE.md Normal file
View File

@@ -0,0 +1,65 @@
> This document lists the steps that lead to a successful release of Topgrade.
1. Open a PR that:
> Here is an [Example PR](https://github.com/topgrade-rs/topgrade/pull/652)
> that you can refer to.
1. bumps the version number.
> If there are breaking changes, the major version number should be increased.
2. Overwrite [`BREAKINGCHANGES`][breaking_changes] with
[`BREAKINGCHANGES_dev`][breaking_changes_dev], and create a new dev file:
```sh'
$ cd topgrade
$ mv BREAKINGCHANGES_dev.md BREAKINGCHANGES.md
$ touch BREAKINGCHANGES_dev.md
```
[breaking_changes_dev]: https://github.com/topgrade-rs/topgrade/blob/main/BREAKINGCHANGES_dev.md
[breaking_changes]: https://github.com/topgrade-rs/topgrade/blob/main/BREAKINGCHANGES.md
2. Check and merge that PR.
3. Go to the [release](https://github.com/topgrade-rs/topgrade/releases) page
and click the [Draft a new release button](https://github.com/topgrade-rs/topgrade/releases/new)
4. Write the release notes
We usually use GitHub's [Automatically generated release notes][auto_gen_release_notes]
functionality to generate release notes, but you write your own one instead.
[auto_gen_release_notes]: https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes
5. Attaching binaries
You don't need to do this as our CI will automatically do it for you,
binaries for Linux, macOS and Windows will be created and attached.
And the CI will publish the new binary to:
1. AUR
2. PyPi
3. Homebrew (seems that this is not working correctly)
4. Winget
6. Manually release it to Crates.io
> Yeah, this is unfortunate, our CI won't do this for us. We should probably add one.
1. `cd` to the Topgrade directory, make sure that it is the latest version
(i.e., including the PR that bumps the version number).
2. Set up your token with `cargo login`.
3. Dry-run the publish `cargo publish --dry-run`.
4. If step 3 works, then do the final release `cargo publish`.
> You can also take a look at the official tutorial [Publishing on crates.io][doc]
>
> [doc]: https://doc.rust-lang.org/cargo/reference/publishing.html

11
SECURITY.md Normal file
View File

@@ -0,0 +1,11 @@
# Security Policy
## Supported Versions
We only support the latest major version and each subversion.
| Version | Supported |
| -------- | ------------------ |
| 15.0.x | :white_check_mark: |
| < 15.0 | :x: |

5
clippy.toml Normal file
View File

@@ -0,0 +1,5 @@
disallowed-methods = [
{ path = "std::process::Command::output", reason = "Use `output_checked[_with][_utf8]`" },
{ path = "std::process::Command::spawn", reason = "Use `spawn_checked`" },
{ path = "std::process::Command::status", reason = "Use `status_checked`" },
]

View File

@@ -1,113 +1,267 @@
# Don't ask for confirmations # Include any additional configuration file(s)
#assume_yes = true # [include] sections are processed in the order you write them
# Files in $CONFIG_DIR/topgrade.d/ are automatically included before this file
[include]
# paths = ["/etc/topgrade.toml"]
[misc]
# Run `sudo -v` to cache credentials at the start of the run
# This avoids a blocking password prompt in the middle of an unattended run
# (default: false)
# pre_sudo = false
# Sudo command to be used
# sudo_command = "sudo"
# Disable specific steps - same options as the command line flag # Disable specific steps - same options as the command line flag
#disable = ["system", "emacs"] # disable = ["system", "emacs"]
# Ignore failures for these steps # Ignore failures for these steps
#ignore_failures = ["powershell"] # ignore_failures = ["powershell"]
# Run specific steps - same options as the command line flag
#only = ["system", "emacs"]
# Do not ask to retry failed steps (default: false)
#no_retry = true
# Run inside tmux
#run_in_tmux = true
# List of remote machines with Topgrade installed on them # List of remote machines with Topgrade installed on them
#remote_topgrades = ["toothless", "pi", "parnas"] # remote_topgrades = ["toothless", "pi", "parnas"]
# Arguments to pass SSH when upgrading remote systems
#ssh_arguments = "-o ConnectTimeout=2"
# Path to Topgrade executable on remote machines # Path to Topgrade executable on remote machines
#remote_topgrade_path = ".cargo/bin/topgrade" # remote_topgrade_path = ".cargo/bin/topgrade"
# Arguments to pass to SSH when upgrading remote systems
# ssh_arguments = "-o ConnectTimeout=2"
# Arguments to pass tmux when pulling Repositories # Arguments to pass tmux when pulling Repositories
#tmux_arguments = "-S /var/tmux.sock" # tmux_arguments = "-S /var/tmux.sock"
# Do not set the terminal title # Do not set the terminal title (default: true)
#set_title = false # set_title = true
# Display the time in step titles # Display the time in step titles (default: true)
# display_time = true # display_time = true
# Cleanup temporary or old files # Don't ask for confirmations (no default value)
#cleanup = true # assume_yes = true
# Skip sending a notification at the end of a run # Do not ask to retry failed steps (default: false)
#skip_notify = true # no_retry = true
[git] # Run inside tmux (default: false)
#max_concurrency = 5 # run_in_tmux = true
# Additional git repositories to pull
#repos = [
# "~/src/*/",
# "~/.config/something"
#]
# Don't pull the predefined git repos # Changes the way topgrade interacts with
#pull_predefined = false # 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"
# Arguments to pass Git when pulling Repositories # Cleanup temporary or old files (default: false)
#arguments = "--rebase --autostash" # cleanup = true
# Send a notification for every step (default: false)
# notify_each_step = false
# Skip sending a notification at the end of a run (default: false)
# skip_notify = true
# The Bash-it branch to update (default: "stable")
# bashit_branch = "stable"
# Run specific steps - same options as the command line flag
# only = ["system", "emacs"]
# Whether to self update
#
# this will be ignored if the binary is built without self update support
#
# available also via setting the environment variable TOPGRADE_NO_SELF_UPGRADE)
# no_self_update = true
# Extra tracing filter directives
# These are prepended to the `--log-filter` argument
# See: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives
# log_filters = ["topgrade::command=debug", "warn"]
[composer]
#self_update = true
# Commands to run before anything # Commands to run before anything
[pre_commands] [pre_commands]
#"Emacs Snapshot" = "rm -rf ~/.emacs.d/elpa.bak && cp -rl ~/.emacs.d/elpa ~/.emacs.d/elpa.bak" # "Emacs Snapshot" = "rm -rf ~/.emacs.d/elpa.bak && cp -rl ~/.emacs.d/elpa ~/.emacs.d/elpa.bak"
# Commands to run after anything
[post_commands]
# "Emacs Snapshot" = "rm -rf ~/.emacs.d/elpa.bak && cp -rl ~/.emacs.d/elpa ~/.emacs.d/elpa.bak"
# Custom commands # Custom commands
[commands] [commands]
#"Python Environment" = "~/dev/.env/bin/pip install -i https://pypi.python.org/simple -U --upgrade-strategy eager jupyter" # "Python Environment" = "~/dev/.env/bin/pip install -i https://pypi.python.org/simple -U --upgrade-strategy eager jupyter"
# "Custom command using interactive shell (unix)" = "-i vim_upgrade"
[python]
# enable_pip_review = true ###disabled by default
# enable_pip_review_local = true ###disabled by default
# enable_pipupgrade = true ###disabled by default
# pipupgrade_arguments = "-y -u --pip-path pip" ###disabled by default
[composer]
# self_update = true
[brew] [brew]
#greedy_cask = true # For the BrewCask step
#autoremove = true # If `Repo Cask Upgrade` exists, then use the `-a` option.
# Otherwise, use the `--greedy` option.
# greedy_cask = true
# For the BrewCask step
# If `Repo Cask Upgrade` does not exist, then use the `--greedy_latest` 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_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
# For the BrewFormula step
# Upgrade formulae built from the HEAD branch; `brew upgrade --fetch-HEAD`
# fetch_head = true
[linux] [linux]
# Arch Package Manager to use. Allowed values: autodetect, trizen, aura, paru, yay, pikaur, pacman, pamac. # Arch Package Manager to use.
#arch_package_manager = "pacman" # Allowed values:
# autodetect, aura, garuda_update, pacman, pamac, paru, pikaur, trizen, yay
# arch_package_manager = "pacman"
# Arguments to pass yay (or paru) when updating packages # Arguments to pass yay (or paru) when updating packages
#yay_arguments = "--nodevel" # yay_arguments = "--nodevel"
#aura_aur_arguments = "-kx"
#aura_pacman_arguments = "" # Arguments to pass dnf when updating packages
#show_arch_news = true # dnf_arguments = "--refresh"
#trizen_arguments = "--devel"
#pikaur_arguments = "" # aura_aur_arguments = "-kx"
#pamac_arguments = "--no-devel"
#enable_tlmgr = true # aura_pacman_arguments = ""
#emerge_sync_flags = "-q" # garuda_update_arguments = ""
#emerge_update_flags = "-uDNa --with-bdeps=y world"
#redhat_distro_sync = false # show_arch_news = true
#rpm_ostree = false
# trizen_arguments = "--devel"
# pikaur_arguments = ""
# pamac_arguments = "--no-devel"
# enable_tlmgr = true
# emerge_sync_flags = "-q"
# emerge_update_flags = "-uDNa --with-bdeps=y world"
# redhat_distro_sync = false
# suse_dup = false
# rpm_ostree = false
# nix_arguments = "--flake"
# nix_env_arguments = "--prebuilt-only"
# Extra Home Manager arguments
# home_manager_arguments = ["--flake", "file"]
[git]
# How many repos to pull at max in parallel
# max_concurrency = 5
# Additional git repositories to pull
# repos = [
# "~/src/*/",
# "~/.config/something"
# ]
# Don't pull the predefined git repos
# pull_predefined = false
# Arguments to pass Git when pulling Repositories
# arguments = "--rebase --autostash"
[windows] [windows]
# Manually select Windows updates # Manually select Windows updates
#accept_all_updates = false # accept_all_updates = false
#open_remotes_in_new_terminal = true
# open_remotes_in_new_terminal = true
# wsl_update_pre_release = true
# wsl_update_use_web_download = true
# Causes Topgrade to rename itself during the run to allow package managers # 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 # to upgrade it. Use this only if you installed Topgrade by using a package
# manager such as Scoop or Cargo # manager such as Scoop or Cargo
#self_rename = true # self_rename = true
[npm] [npm]
# Use sudo if the NPM directory isn't owned by the current user # Use sudo if the NPM directory isn't owned by the current user
#use_sudo = true # use_sudo = true
[yarn]
# Run `yarn global upgrade` with `sudo`
# use_sudo = true
[vim]
# For `vim-plug`, execute `PlugUpdate!` instead of `PlugUpdate`
# force_plug_update = true
[firmware] [firmware]
# Offer to update firmware; if false just check for and display available updates # Offer to update firmware; if false just check for and display available updates
#upgrade = true # upgrade = true
[vagrant]
# Vagrant directories
# directories = []
# power on vagrant boxes if needed
# power_on = true
# Always suspend vagrant boxes instead of powering off
# always_suspend = true
[flatpak] [flatpak]
# Use sudo for updating the system-wide installation # Use sudo for updating the system-wide installation
#use_sudo = true # use_sudo = true
[distrobox] [distrobox]
#use_root = false # use_root = false
#containers = ["archlinux-latest"]
# containers = ["archlinux-latest"]
[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,
# 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 718 KiB

BIN
doc/topgrade_demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

464
locales/app.yml Normal file
View File

@@ -0,0 +1,464 @@
_version: 2
"Current system locale is {system_locale}":
en: "Current system locale is %{system_locale}"
zh_TW: "目前語言為 %{system_locale}"
"Dry running: {program_name} {arguments}":
en: "Dry running: %{program_name} %{arguments}"
zh_TW: "正在模擬 %{program_name} %{arguments} 的執行過程"
"in {directory}":
en: "in %{directory}"
zh_TW: "在 %{directory}"
"Rebooting...":
en: "Rebooting..."
zh_TW: "正在重新啟動..."
"Plugins upgraded":
en: "Plugins upgraded"
zh_TW: "已更新所有擴充功能"
"Would self-update":
en: "Would self-update"
zh_TW: "將自行更新"
"Pulling":
en: "Pulling"
zh_TW: "正在拉取"
"No Breaking changes":
en: "No Breaking changes"
zh_TW: "無重大更改"
"Dropping you to shell. Fix what you need and then exit the shell.":
en: "Dropping you to shell. Fix what you need and then exit the shell."
zh_TW: "已切換到終端殼層。修復完畢後請退出殼層以繼續。"
"Topgrade launched in a new tmux session":
en: "Topgrade launched in a new tmux session"
zh_TW: "Topgrade 已啟動新 tmux 程序"
'Topgrade upgraded to {version}:\n':
en: 'Topgrade upgraded to %{version}:\n'
zh_TW: '已將 Topgrade 更新至 %{version}\n'
"Topgrade is up-to-date":
en: "Topgrade is up-to-date"
zh_TW: "Topgrade 為最新版本"
"Updating modules...":
en: "Updating modules..."
zh_TW: "正在更新模組..."
"Powershell Modules Update":
en: "Powershell Modules Update"
zh_TW: "Powershell 模組更新"
"Powershell is not installed":
en: "Powershell is not installed"
zh_TW: "未安裝 Powershell"
"Error detecting current distribution: {error}":
en: "Error detecting current distribution: %{error}"
zh_TW: "無法偵測作業系統:%{error}"
"Error: {error}":
en: "Error: %{error}"
zh_TW: "錯誤:%{error}"
"Failed":
en: "Failed"
zh_TW: "失敗"
"pulling":
en: "pulling"
zh_TW: "正在拉取"
"Changed":
en: "Changed"
zh_TW: "已更改"
"Up-to-date":
en: "Up-to-date"
zh_TW: "已為最新版本"
"Self update":
en: "Self update"
zh_TW: "自行更新"
# The following 2 strings are used in the same sentence
# i.e. *Only* updated repositories will be shown...
# Note that the text *Only* is highlighted in green
"Only":
en: "Only"
zh_TW: "將僅"
"updated repositories will be shown...":
en: "updated repositories will be shown..."
zh_TW: "顯示被更新的 git 來源..."
"because it has no remotes":
en: "because it has no remotes"
zh_TW: "因為其沒有遠端來源"
"Skipping":
en: "Skipping"
zh_TW: "正在略過"
"Aura(<0.4.6) requires sudo installed to work with AUR packages":
en: "Aura(<0.4.6) requires sudo installed to work with AUR packages"
zh_TW: "Aura<0.4.6)依賴 sudo 安裝 AUR 套件"
"Pacman backup configuration files found:":
en: "Pacman backup configuration files found:"
zh_TW: "找到 Pacman 設定備份檔:"
"The package audit was successful, but vulnerable packages still remain on the system":
en: "The package audit was successful, but vulnerable packages still remain on the system"
zh_TW: "雖然套件檢測成功,但系統仍然包含危險套件"
"Syncing portage":
en: "Syncing portage"
zh_TW: "正在同步 portage"
"Finding available software":
en: "Finding available software"
zh_TW: "正在尋找軟體"
"A system update is available. Do you wish to install it?":
en: "A system update is available. Do you wish to install it?"
zh_TW: "系統更新已就緒。是否現在安裝?"
"No new software available.":
en: "No new software available."
zh_TW: "沒有新軟體。"
"No Xcode releases installed.":
en: "No Xcode releases installed."
zh_TW: "尚未安裝 Xcode 發行版。"
"Would you like to move the former Xcode release to the trash?":
en: "Would you like to move the former Xcode release to the trash?"
zh_TW: "是否將舊版 Xcode 移至垃圾桶?"
"New Xcode release detected:":
en: "New Xcode release detected:"
zh_TW: "有新的 Xcode 版本:"
"Would you like to install it?":
en: "Would you like to install it?"
zh_TW: "是否現在安裝?"
"No global packages installed":
en: "No global packages installed"
zh_TW: "尚未安裝全域套件"
"Remote Topgrade launched in Tmux":
en: "Remote Topgrade launched in Tmux"
zh_TW: "已在 Tmux 中啟動遠端 Topgrade 程序"
"Remote Topgrade launched in an external terminal":
en: "Remote Topgrade launched in an external terminal"
zh_TW: "已在新終端機視窗中啟動遠端 Topgrade"
"Collecting Vagrant boxes":
en: "Collecting Vagrant boxes"
zh_TW: "正在蒐集 Vagrant 容器"
"No Vagrant directories were specified in the configuration file":
en: "No Vagrant directories were specified in the configuration file"
zh_TW: "尚未在設定檔中指定 Vagrant 資料夾"
"Vagrant boxes":
en: "Vagrant boxes"
zh_TW: "Vagrant 容器"
"No outdated boxes":
en: "No outdated boxes"
zh_TW: "未有需要更新的容器"
"Summary":
en: "Summary"
zh_TW: "結果"
"Topgrade finished with errors":
en: "Topgrade finished with errors"
zh_TW: "Topgrade 執行部分成功"
"Topgrade finished successfully":
en: "Topgrade finished successfully"
zh_TW: "Topgrade 執行成功"
"Topgrade {version_str} Breaking Changes":
en: "Topgrade %{version_str} Breaking Changes"
zh_TW: "Topgrade %{version_str} 重大更改"
"Path {path} expanded to {expanded}":
en: "Path %{path} expanded to %{expanded}"
zh_TW: "已擴展 %{path} 至 %{expanded}"
"Path {path} doesn't exist":
en: "Path %{path} doesn't exist"
zh_TW: "路徑 %{path} 不存在"
"Cannot find {binary_name} in PATH":
en: "Cannot find %{binary_name} in PATH"
zh_TW: "在 $PATH 中找不到 %{binary_name} 執行檔"
"Failed to get a UTF-8 encoded hostname":
en: "Failed to get a UTF-8 encoded hostname"
zh_TW: "無法取得 UTF-8 編碼的主機名稱"
"Failed to get hostname: {err}":
en: "Failed to get hostname: %{err}"
zh_TW: "無法取得主機名稱:%{err}"
"{python} is a Python 2, skip.":
en: "%{python} is a Python 2, skip."
zh_TW: "%{python} 是 Python 2略過。"
"{python} is a Python shim, skip.":
en: "%{python} is a Python shim, skip."
zh_TW: "%{python} 是 Python shim略過。"
"{key} failed:":
en: "%{key} failed:"
zh_TW: "%{key} 失敗:"
"{step_name} failed":
en: "%{step_name} failed"
zh_TW: "%{step_name} 失敗"
"DragonFly BSD Packages":
en: "DragonFly BSD Packages"
zh_TW: "DragonFly BSD 套件"
"DragonFly BSD Audit":
en: "DragonFly BSD Audit"
zh_TW: "DragonFly BSD 紀錄"
"FreeBSD Update":
en: "FreeBSD Update"
zh_TW: "FreeBSD 更新"
"FreeBSD Packages":
en: "FreeBSD Packages"
zh_TW: "FreeBSD 套件"
"FreeBSD Audit":
en: "FreeBSD Audit"
zh_TW: "FreeBSD 紀錄"
"System update":
en: "System update"
zh_TW: "系統更新"
"needrestart will be ran by the package manager":
en: "needrestart will be ran by the package manager"
zh_TW: "needrestart 將被套件管理員執行"
"Check for needed restarts":
en: "Check for needed restarts"
zh_TW: "正在檢查是否需要重新啟動系統"
"Should not run in WSL":
en: "Should not run in WSL"
zh_TW: "不該在 WSL 中執行"
"Firmware upgrades":
en: "Firmware upgrades"
zh_TW: "韌體更新"
"Flatpak System Packages":
en: "Flatpak System Packages"
zh_TW: "Flatpak 系統套件"
"Snapd socket does not exist":
en: "Snapd socket does not exist"
zh_TW: "找不到 Snapd 程序"
"You need to specify at least one container":
en: "You need to specify at least one container"
zh_TW: "必須指定至少一個容器"
"Skipped in --yes":
en: "Skipped in --yes"
zh_TW: "指定 --yes略過"
"Configuration update":
en: "Configuration update"
zh_TW: "設定更新"
"Going to execute `waydroid upgrade`, which would STOP the running container, is this ok?":
en: "Going to execute `waydroid upgrade`, which would STOP the running container, is this ok?"
zh_TW: "將略過 `waydroid upgrade`,並且「停止」執行容器。是否繼續?"
"Skip the Waydroid step because the user don't want to proceed":
en: "Skip the Waydroid step because the user don't want to proceed"
zh_TW: "使用者指定略過 Waydroid 程序"
"macOS App Store":
en: "macOS App Store"
zh_TW: "macOS App Store"
"macOS system update":
en: "macOS system update"
zh_TW: "macOS 系統更新"
"OpenBSD Update":
en: "OpenBSD Update"
zh_TW: "OpenBSD 更新"
"OpenBSD Packages":
en: "OpenBSD Packages"
zh_TW: "OpenBSD 套件"
"`fisher` is not defined in `fish`":
en: "`fisher` is not defined in `fish`"
zh_TW: "`fisher` 未在 `fish` 中指定"
"`fish_plugins` path doesn't exist: {err}":
en: "`fish_plugins` path doesn't exist: %{err}"
zh_TW: "不存在 `fish_plugins` 路徑:%{err}"
"`fish_update_completions` is not available":
en: "`fish_update_completions` is not available"
zh_TW: "無法使用 `fish_update_completions`"
"Desktop doest not appear to be gnome":
en: "Desktop doest not appear to be gnome"
zh_TW: "桌面環境不是 Gnome"
"Gnome shell extensions are unregistered in DBus":
en: "Gnome shell extensions are unregistered in DBus"
zh_TW: "Gnome Shell 擴充功能在 DBus 中未被註冊"
"Gnome Shell extensions":
en: "Gnome Shell extensions"
zh_TW: "Gnome Shell 擴充功能"
"Not a custom brew for macOS":
en: "Not a custom brew for macOS"
zh_TW: "不是專門的 macOS brew"
"Guix Pull Failed, Skipping":
en: "Guix Pull Failed, Skipping"
zh_TW: "Guix 拉取失敗,略過"
"Nix-darwin on macOS must be upgraded via darwin-rebuild switch":
en: "Nix-darwin on macOS must be upgraded via darwin-rebuild switch"
zh_TW: "Nix-darwin 在 macOS 上必須使用 darwin-rebuild 更新"
"`nix upgrade-nix` can only be used on macOS or non-NixOS Linux":
en: "`nix upgrade-nix` can only be used on macOS or non-NixOS Linux"
zh_TW: "`nix upgrade-nix` 僅能在 macOS 或非 NixOS 的 Linux 上使用"
"`nix upgrade-nix` cannot be run when Nix is installed in a profile":
en: "`nix upgrade-nix` cannot be run when Nix is installed in a profile"
zh_TW: "`nix upgrade-nix` 無法在已安裝 Nix 使用者環境的系統上使用"
"Nix (self-upgrade)":
en: "Nix (self-upgrade)"
zh_TW: "Nix自行更新"
"Pyenv is installed, but $PYENV_ROOT is not set correctly":
en: "Pyenv is installed, but $PYENV_ROOT is not set correctly"
zh_TW: "已安裝 Pyenv 但尚未正確設定 $PYENV_ROOT"
"pyenv is not a git repository":
en: "pyenv is not a git repository"
zh_TW: "pyenv 不是 git 來源"
"Bun Packages":
en: "Bun Packages"
zh_TW: "Bun 套件"
"WSL not installed":
en: "WSL not installed"
zh_TW: "未安裝 WSL"
"Update WSL":
en: "Update WSL"
zh_TW: "更新 WSL"
"Could not find Topgrade installed in WSL":
en: "Could not find Topgrade installed in WSL"
zh_TW: "尚未在 WSL 內安裝 Topgrade"
"Consider installing PSWindowsUpdate as the use of Windows Update via USOClient is not supported.":
en: "Consider installing PSWindowsUpdate as the use of Windows Update via USOClient is not supported."
zh_TW: "目前不支援使用 USOClient 管理 Windows 更新。建議安裝 PSWindowsUpdate。"
"USOClient not supported.":
en: "USOClient not supported."
zh_TW: "不支援 USOClient。"
"Connecting to {hostname}...":
en: "Connecting to %{hostname}..."
zh_TW: "正在連接 %{hostname}..."
"Skipping powered off box {vagrant_box}":
en: "Skipping powered off box %{vagrant_box}"
zh_TW: "正在略過已關機的容器 %{vagrant_box}"
"`{repo_tag}` for `{platform}`":
en: "`%{repo_tag}` for `%{platform}`"
zh_TW: "`%{repo_tag}` 給 `%{platform}`"
"Containers":
en: "Containers"
zh_TW: "容器"
"Emacs directory does not exist":
en: "Emacs directory does not exist"
zh_TW: "找不到 Emacs 資料夾"
"Error getting the composer directory: {error}":
en: "Error getting the composer directory: %{error}"
zh_TW: "無法取得 composer 資料夾:%{error}"
"Composer directory {composer_home} isn't a descendant of the user's home directory":
en: "Composer directory %{composer_home} isn't a descendant of the user's home directory"
zh_TW: "Composer 資料夾 %{composer_home} 不在家目錄下"
"Composer":
en: "Composer"
zh_TW: "Composer"
"Error running `dotnet tool list`. This is expected when a dotnet runtime is installed but no SDK.":
en: "Error running `dotnet tool list`. This is expected when a dotnet runtime is installed but no SDK."
zh_TW: "執行 `dotnet tool list` 失敗。已安裝 dotnet 執行環境但未安裝 SDK"
"No dotnet global tools installed":
en: "No dotnet global tools installed"
zh_TW: "尚未安裝全域 dotnet 工具"
"Racket Package Manager":
en: "Racket Package Manager"
zh_TW: "Racket 套件管理員"
"GH failed":
en: "GH failed"
zh_TW: "GH 失敗"
"GitHub CLI Extensions":
en: "GitHub CLI Extensions"
zh_TW: "GitHub CLI 擴充功能"
"Julia Packages":
en: "Julia Packages"
zh_TW: "Julia 套件"
"Update ClamAV Database(FreshClam)":
en: "Update ClamAV Database(FreshClam)"
zh_TW: "更新 ClamAV 資料庫FreshClam"
"Path {pattern} did not contain any git repositories":
en: "Path %{pattern} did not contain any git repositories"
zh_TW: "路徑 %{pattern} 中沒有任何 git 來源"
"No repositories to pull":
en: "No repositories to pull"
zh_TW: "沒有來源可以拉取"
"Git repositories":
en: "Git repositories"
zh_TW: "Git 來源"
"Would pull {repo}":
en: "Would pull %{repo}"
zh_TW: "拉取 %{repo}"
# aka npm
"Node Package Manager":
en: "Node Package Manager"
zh_TW: "Node 套件管理員npm"
# aka pnpm
"Performant Node Package Manager":
en: "Performant Node Package Manager"
zh_TW: "效能 Node 套件管理員pnpm"
"Yarn Package Manager":
en: "Yarn Package Manager"
zh_TW: "Yarn 套件管理員"
"Deno installed outside of .deno directory":
en: "Deno installed outside of .deno directory"
zh_TW: "Deno 安裝在 .deno 資料夾外"
"The Ultimate vimrc":
en: "The Ultimate vimrc"
zh_TW: "終極 vimrcThe Ultimate vimrc"
"vim binary might be actually nvim":
en: "vim binary might be actually nvim"
zh_TW: "vim 執行檔可能為 nvim"
"`{process}` failed: {exit_status}":
en: "`%{process}` failed: %{exit_status}"
zh_TW: "`%{process}` 失敗:%{exit_status}"
"`{process}` failed: {exit_status} with {output}":
en: "`%{process}` failed: %{exit_status} with %{output}"
zh_TW: "`%{process}` 失敗:%{exit_status} 伴隨 %{output}"
"Unknown Linux Distribution":
en: "Unknown Linux Distribution"
zh_TW: "未知 Linux"
'File "/etc/os-release" does not exist or is empty':
en: 'File "/etc/os-release" does not exist or is empty'
zh_TW: '「/etc/os-release」不存在或為空'
"Failed getting the system package manager":
en: "Failed getting the system package manager"
zh_TW: "偵測系統套件管理員失敗"
"A step failed":
en: "A step failed"
zh_TW: "某步驟執行失敗"
"Dry running":
en: "Dry running"
zh_TW: "模擬執行"
"Topgrade Upgraded":
en: "Topgrade Upgraded"
zh_TW: "已更新 Topgrade"
# Summary texts
"OK":
en: "OK"
zh_TW: "成功"
"FAILED":
en: "FAILED"
zh_TW: "失敗"
"IGNORED":
en: "IGNORED"
zh_TW: "忽略"
"SKIPPED":
en: "SKIPPED"
zh_TW: "略過"
# 'Y' and 'N' have to stay the same characters. Eg for German the translation
# would look sth like "(Y) Ja / (N) Nein"
"(Y)es/(N)o":
en: "(Y)es/(N)o"
zh_TW: "(Y)是/(N)否"
# 'y', 'N', 's', 'q' have to stay the same throughout all translations.
# Eg German would look like "(y) Wiederholen / (N) Nein / (s) Konsole / (q) Beenden"
"Retry? (y)es/(N)o/(s)hell/(q)uit":
en: "Retry? (y)es/(N)o/(s)hell/(q)uit"
zh_TW: "再試一次? (y)是/(N)否/(s)殼層/(q)退出"
# 'R', 'S', 'Q' have to stay the same throughout all translations. Eg German would look like "\n(R) Neustarten\n(S) Konsole\n(Q) Beenden"
'\n(R)eboot\n(S)hell\n(Q)uit':
en: '\n(R)eboot\n(S)hell\n(Q)uit'
zh_TW: '\n(R)重新啟動\n(S)殼層\n(Q)退出'
"Require sudo or counterpart but not found, skip":
en: "Require sudo or counterpart but not found, skip"
zh_TW: "找不到權限管理程式sudo 等),略過"
"sudo as user '{user}'":
en: "sudo as user '%{user}'"
zh_TW: "sudo 以使用者 '%{user}'"
"Updating aqua ...":
en: "Updating aqua ..."
zh_TW: "正在更新 aqua..."
"Updating aqua installed cli tools ...":
en: "Updating aqua installed cli tools ..."
zh_TW: "正在更新 aqua 安裝的命令行介面工具..."
"Updating Volta packages...":
en: "Updating Volta packages..."
zh_TW: "正在更新 Volta 套件..."
"No packages installed with Volta":
en: "No packages installed with Volta"
zh_TW: "沒有任何 Volta 套件"
"pyenv-update plugin is not installed":
en: "pyenv-update plugin is not installed"
zh_TW: "尚未安裝 pyenv-update 擴充功能"
"Respawning...":
en: "Respawning..."
zh_TW: "正在重新生成..."
"Could not find Topgrade in any WSL disribution":
en: "Could not find Topgrade in any WSL disribution"
zh_TW: "在所有 WSL 中找不到 Topgrade"
"Windows Update":
en: "Windows Update"
zh_TW: "Windows 更新"

16
pyproject.toml Normal file
View File

@@ -0,0 +1,16 @@
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[project]
name = "topgrade"
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
[tool.maturin]
bindings = "bin"

2
rust-toolchain.toml Normal file
View File

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

171
src/breaking_changes.rs Normal file
View File

@@ -0,0 +1,171 @@
//! Inform the users of the breaking changes introduced in this major release.
//!
//! Print the breaking changes and possibly a migration guide when:
//! 1. The Topgrade being executed is a new major release
//! 2. This is the first launch of that major release
use crate::terminal::print_separator;
#[cfg(windows)]
use crate::WINDOWS_DIRS;
#[cfg(unix)]
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},
io::Write,
path::PathBuf,
str::FromStr,
};
/// Version string x.y.z
static VERSION_STR: &str = env!("CARGO_PKG_VERSION");
/// Version info
#[derive(Debug)]
pub(crate) struct Version {
_major: u64,
minor: u64,
patch: u64,
}
impl FromStr for Version {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
const NOT_SEMVER: &str = "Topgrade version is not semantic";
const NOT_NUMBER: &str = "Topgrade version is not dot-separated numbers";
let mut iter = s.split('.').take(3);
let major = iter.next().expect(NOT_SEMVER).parse().expect(NOT_NUMBER);
let minor = iter.next().expect(NOT_SEMVER).parse().expect(NOT_NUMBER);
let patch = iter.next().expect(NOT_SEMVER).parse().expect(NOT_NUMBER);
// They cannot be all 0s
assert!(
!(major == 0 && minor == 0 && patch == 0),
"Version numbers cannot be all 0s"
);
Ok(Self {
_major: major,
minor,
patch,
})
}
}
impl Version {
/// True if this version is a new major release.
pub(crate) fn is_new_major_release(&self) -> bool {
// We have already checked that they cannot all be zeros, so `self.major`
// is guaranteed to be non-zero.
self.minor == 0 && self.patch == 0
}
}
/// Topgrade's breaking changes
///
/// We store them in the compiled binary.
pub(crate) static BREAKINGCHANGES: &str = include_str!("../BREAKINGCHANGES.md");
/// Return platform's data directory.
fn data_dir() -> PathBuf {
#[cfg(unix)]
return XDG_DIRS.data_dir();
#[cfg(windows)]
return WINDOWS_DIRS.data_dir();
}
/// Return Topgrade's keep file path.
///
/// keep file is a file under the data directory containing a major version
/// number, it will be created on first run and is used to check if an execution
/// of Topgrade is the first run of a major release, for more details, see
/// `first_run_of_major_release()`.
fn keep_file_path() -> PathBuf {
let keep_file = "topgrade_keep";
data_dir().join(keep_file)
}
/// If environment variable `TOPGRADE_SKIP_BRKC_NOTIFY` is set to `true`, then
/// we won't notify the user of the breaking changes.
pub(crate) fn should_skip() -> bool {
if let Ok(var) = var("TOPGRADE_SKIP_BRKC_NOTIFY") {
return var.as_str() == "true";
}
false
}
/// True if this is the first execution of a major release.
pub(crate) fn first_run_of_major_release() -> Result<bool> {
let version = VERSION_STR.parse::<Version>().expect("should be a valid version");
let keep_file = keep_file_path();
// disable this lint here as the current code has better readability
#[allow(clippy::collapsible_if)]
if version.is_new_major_release() {
if !keep_file.exists() || read_to_string(&keep_file)? != VERSION_STR {
return Ok(true);
}
}
Ok(false)
}
/// Print breaking changes to the user.
pub(crate) fn print_breaking_changes() {
let header = format!(
"{}",
t!("Topgrade {version_str} Breaking Changes", version_str = VERSION_STR)
);
print_separator(header);
let contents = if BREAKINGCHANGES.is_empty() {
t!("No Breaking changes").to_string()
} else {
BREAKINGCHANGES.to_string()
};
println!("{contents}\n");
}
/// This function will be ONLY executed when the user has confirmed the breaking
/// changes, once confirmed, we write the keep file, which means the first run
/// of this major release is finished.
pub(crate) fn write_keep_file() -> Result<()> {
std::fs::create_dir_all(data_dir())?;
let keep_file = keep_file_path();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(keep_file)?;
let _ = file.write(VERSION_STR.as_bytes())?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn is_new_major_release_works() {
let first_major_release: Version = "1.0.0".parse().unwrap();
let under_dev: Version = "0.1.0".parse().unwrap();
assert!(first_major_release.is_new_major_release());
assert!(!under_dev.is_new_major_release());
}
#[test]
#[should_panic(expected = "Version numbers cannot be all 0s")]
fn invalid_version() {
let all_0 = "0.0.0";
all_0.parse::<Version>().unwrap();
}
}

246
src/command.rs Normal file
View File

@@ -0,0 +1,246 @@
//! Utilities for running commands and providing user-friendly error messages.
use std::fmt::Display;
use std::process::Child;
use std::process::{Command, ExitStatus, Output};
use color_eyre::eyre;
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use crate::error::TopgradeError;
use tracing::debug;
/// Like [`Output`], but UTF-8 decoded.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Utf8Output {
pub status: ExitStatus,
pub stdout: String,
pub stderr: String,
}
impl TryFrom<Output> for Utf8Output {
type Error = eyre::Error;
fn try_from(Output { status, stdout, stderr }: Output) -> Result<Self, Self::Error> {
let stdout = String::from_utf8(stdout).map_err(|err| {
eyre!(
"Stdout contained invalid UTF-8: {}",
String::from_utf8_lossy(err.as_bytes())
)
})?;
let stderr = String::from_utf8(stderr).map_err(|err| {
eyre!(
"Stderr contained invalid UTF-8: {}",
String::from_utf8_lossy(err.as_bytes())
)
})?;
Ok(Utf8Output { status, stdout, stderr })
}
}
impl TryFrom<&Output> for Utf8Output {
type Error = eyre::Error;
fn try_from(Output { status, stdout, stderr }: &Output) -> Result<Self, Self::Error> {
let stdout = String::from_utf8(stdout.to_vec()).map_err(|err| {
eyre!(
"Stdout contained invalid UTF-8: {}",
String::from_utf8_lossy(err.as_bytes())
)
})?;
let stderr = String::from_utf8(stderr.to_vec()).map_err(|err| {
eyre!(
"Stderr contained invalid UTF-8: {}",
String::from_utf8_lossy(err.as_bytes())
)
})?;
let status = *status;
Ok(Utf8Output { status, stdout, stderr })
}
}
impl Display for Utf8Output {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.stdout)
}
}
/// Extension trait for [`Command`], adding helpers to gather output while checking the exit
/// status.
///
/// These also give us significantly better error messages, which include:
///
/// 1. The command and arguments that were executed, escaped with familiar `sh` syntax.
/// 2. The exit status of the command or the signal that killed it.
/// 3. If we were capturing the output of the command, rather than forwarding it to the user's
/// stdout/stderr, the error message includes the command's stdout and stderr output.
///
/// Additionally, executing commands with these methods will log the command at debug-level,
/// useful when gathering error reports.
pub trait CommandExt {
type Child;
/// Like [`Command::output`], but checks the exit status and provides nice error messages.
///
/// Returns an `Err` if the command failed to execute or returned a non-zero exit code.
#[track_caller]
fn output_checked(&mut self) -> eyre::Result<Output> {
self.output_checked_with(|output: &Output| if output.status.success() { Ok(()) } else { Err(()) })
}
/// Like [`output_checked`], but also decodes Stdout and Stderr as UTF-8.
///
/// Returns an `Err` if the command failed to execute, returned a non-zero exit code, or if the
/// output contains invalid UTF-8.
#[track_caller]
fn output_checked_utf8(&mut self) -> eyre::Result<Utf8Output> {
let output = self.output_checked()?;
output.try_into()
}
/// Like [`output_checked`] but a closure determines if the command failed instead of
/// [`ExitStatus::success`].
///
/// Returns an `Err` if the command failed to execute or if `succeeded` returns an `Err`.
/// (This lets the caller substitute their own notion of "success" instead of assuming
/// non-zero exit codes indicate success.)
#[track_caller]
fn output_checked_with(&mut self, succeeded: impl Fn(&Output) -> Result<(), ()>) -> eyre::Result<Output>;
/// Like [`output_checked_with`], but also decodes Stdout and Stderr as UTF-8.
///
/// Returns an `Err` if the command failed to execute, if `succeeded` returns an `Err`, or if
/// the output contains invalid UTF-8.
#[track_caller]
fn output_checked_with_utf8(
&mut self,
succeeded: impl Fn(&Utf8Output) -> Result<(), ()>,
) -> eyre::Result<Utf8Output> {
// This decodes the Stdout and Stderr as UTF-8 twice...
let output =
self.output_checked_with(|output| output.try_into().map_err(|_| ()).and_then(|o| succeeded(&o)))?;
output.try_into()
}
/// Like [`Command::status`], but gives a nice error message if the status is unsuccessful
/// rather than returning the [`ExitStatus`].
///
/// Returns `Ok` if the command executes successfully, returns `Err` if the command fails to
/// execute or returns a non-zero exit code.
#[track_caller]
fn status_checked(&mut self) -> eyre::Result<()> {
self.status_checked_with(|status| if status.success() { Ok(()) } else { Err(()) })
}
/// Like [`status_checked`], but gives a nice error message if the status is unsuccessful
/// rather than returning the [`ExitStatus`].
///
/// Returns `Ok` if the command executes successfully, returns `Err` if the command fails to
/// execute or if `succeeded` returns an `Err`.
/// (This lets the caller substitute their own notion of "success" instead of assuming
/// non-zero exit codes indicate success.)
#[track_caller]
fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> eyre::Result<()>;
/// Like [`Command::spawn`], but gives a nice error message if the command fails to
/// execute.
#[track_caller]
fn spawn_checked(&mut self) -> eyre::Result<Self::Child>;
}
impl CommandExt for Command {
type Child = Child;
fn output_checked_with(&mut self, succeeded: impl Fn(&Output) -> Result<(), ()>) -> eyre::Result<Output> {
let command = log(self);
// This is where we implement `output_checked`, which is what we prefer to use instead of
// `output`, so we allow `Command::output` here.
#[allow(clippy::disallowed_methods)]
let output = self
.output()
.with_context(|| format!("Failed to execute `{command}`"))?;
if succeeded(&output).is_ok() {
Ok(output)
} else {
let mut message = format!("Command failed: `{command}`");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let stdout_trimmed = stdout.trim();
if !stdout_trimmed.is_empty() {
message.push_str(&format!("\n\nStdout:\n{stdout_trimmed}"));
}
let stderr_trimmed = stderr.trim();
if !stderr_trimmed.is_empty() {
message.push_str(&format!("\n\nStderr:\n{stderr_trimmed}"));
}
let (program, _) = get_program_and_args(self);
let err = TopgradeError::ProcessFailedWithOutput(program, output.status, stderr.into_owned());
let ret = Err(err).with_context(|| message);
debug!("Command failed: {ret:?}");
ret
}
}
fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> eyre::Result<()> {
let command = log(self);
let message = format!("Failed to execute `{command}`");
// This is where we implement `status_checked`, which is what we prefer to use instead of
// `status`, so we allow `Command::status` here.
#[allow(clippy::disallowed_methods)]
let status = self.status().with_context(|| message.clone())?;
if succeeded(status).is_ok() {
Ok(())
} else {
let (program, _) = get_program_and_args(self);
let err = TopgradeError::ProcessFailed(program, status);
let ret = Err(err).with_context(|| format!("Command failed: `{command}`"));
debug!("Command failed: {ret:?}");
ret
}
}
fn spawn_checked(&mut self) -> eyre::Result<Self::Child> {
let command = log(self);
let message = format!("Failed to execute `{command}`");
// This is where we implement `spawn_checked`, which is what we prefer to use instead of
// `spawn`, so we allow `Command::spawn` here.
#[allow(clippy::disallowed_methods)]
{
self.spawn().with_context(|| message.clone())
}
}
}
fn get_program_and_args(cmd: &Command) -> (String, String) {
// We're not doing anything weird with commands that are invalid UTF-8 so this is fine.
let program = cmd.get_program().to_string_lossy().into_owned();
let args = shell_words::join(cmd.get_args().map(|arg| arg.to_string_lossy()));
(program, args)
}
fn format_program_and_args(cmd: &Command) -> String {
let (program, args) = get_program_and_args(cmd);
if args.is_empty() {
program
} else {
format!("{program} {args}")
}
}
fn log(cmd: &Command) -> String {
let command = format_program_and_args(cmd);
debug!("Executing command `{command}`");
command
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
//! SIGINT handling in Unix systems. //! SIGINT handling in Unix systems.
use crate::ctrlc::interrupted::set_interrupted; use crate::ctrlc::interrupted::set_interrupted;
use nix::sys::signal; use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal};
/// Handle SIGINT. Set the interruption flag. /// Handle SIGINT. Set the interruption flag.
extern "C" fn handle_sigint(_: i32) { extern "C" fn handle_sigint(_: i32) {
@@ -10,12 +10,8 @@ extern "C" fn handle_sigint(_: i32) {
/// Set the necessary signal handlers. /// Set the necessary signal handlers.
/// The function panics on failure. /// The function panics on failure.
pub fn set_handler() { pub fn set_handler() {
let sig_action = signal::SigAction::new( let sig_action = SigAction::new(SigHandler::Handler(handle_sigint), SaFlags::empty(), SigSet::empty());
signal::SigHandler::Handler(handle_sigint),
signal::SaFlags::empty(),
signal::SigSet::empty(),
);
unsafe { unsafe {
signal::sigaction(signal::SIGINT, &sig_action).unwrap(); sigaction(Signal::SIGINT, &sig_action).unwrap();
} }
} }

View File

@@ -1,5 +1,6 @@
//! A stub for Ctrl + C handling. //! A stub for Ctrl + C handling.
use crate::ctrlc::interrupted::set_interrupted; use crate::ctrlc::interrupted::set_interrupted;
use tracing::error;
use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE}; use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE};
use winapi::um::consoleapi::SetConsoleCtrlHandler; use winapi::um::consoleapi::SetConsoleCtrlHandler;
use winapi::um::wincon::CTRL_C_EVENT; use winapi::um::wincon::CTRL_C_EVENT;
@@ -16,6 +17,6 @@ extern "system" fn handler(ctrl_type: DWORD) -> BOOL {
pub fn set_handler() { pub fn set_handler() {
if 0 == unsafe { SetConsoleCtrlHandler(Some(handler), TRUE) } { if 0 == unsafe { SetConsoleCtrlHandler(Some(handler), TRUE) } {
log::error!("Cannot set a control C handler") error!("Cannot set a control C handler")
} }
} }

View File

@@ -1,41 +1,98 @@
use std::process::ExitStatus; use std::{fmt::Display, process::ExitStatus};
use rust_i18n::t;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug, PartialEq, Eq)] #[derive(Error, Debug, PartialEq, Eq)]
pub enum TopgradeError { pub enum TopgradeError {
#[error("{0}")] ProcessFailed(String, ExitStatus),
ProcessFailed(ExitStatus),
#[error("{0}: {1}")] ProcessFailedWithOutput(String, ExitStatus, String),
ProcessFailedWithOutput(ExitStatus, String),
#[error("Sudo is required for this step")]
#[allow(dead_code)]
SudoRequired,
#[error("Unknown Linux Distribution")]
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
UnknownLinuxDistribution, UnknownLinuxDistribution,
#[error("Failed getting the system package manager")] #[cfg(target_os = "linux")]
EmptyOSReleaseFile,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
FailedGettingPackageManager, 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)] #[derive(Error, Debug)]
#[error("A step failed")]
pub struct StepFailed; pub struct StepFailed;
#[derive(Error, Debug)] impl Display for StepFailed {
#[error("Dry running")] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
pub struct DryRun(); 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)] #[derive(Error, Debug)]
#[error("{0}")]
pub struct SkipStep(pub String); 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"))] #[cfg(all(windows, feature = "self-update"))]
#[derive(Error, Debug)] #[derive(Error, Debug)]
#[error("Topgrade Upgraded")]
pub struct Upgraded(pub ExitStatus); 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,70 +1,63 @@
#![allow(dead_code)] #![allow(dead_code)]
use crate::executor::RunType; use crate::executor::RunType;
use crate::git::Git; use crate::sudo::Sudo;
use crate::utils::require_option; use crate::utils::{get_require_sudo_string, require_option};
use crate::{config::Config, executor::Executor}; use crate::{config::Config, executor::Executor};
use anyhow::Result; use color_eyre::eyre::Result;
use directories::BaseDirs; use std::env::var;
use std::path::{Path, PathBuf}; use std::path::Path;
use std::sync::Mutex;
pub struct ExecutionContext<'a> { pub struct ExecutionContext<'a> {
run_type: RunType, run_type: RunType,
sudo: &'a Option<PathBuf>, sudo: Option<Sudo>,
git: &'a Git,
config: &'a Config, config: &'a Config,
base_dirs: &'a BaseDirs, /// Name of a tmux session to execute commands in, if any.
/// This is used in `./steps/remote/ssh.rs`, where we want to run `topgrade` in a new
/// tmux window for each remote.
tmux_session: Mutex<Option<String>>,
/// True if topgrade is running under ssh.
under_ssh: bool,
} }
impl<'a> ExecutionContext<'a> { impl<'a> ExecutionContext<'a> {
pub fn new( pub fn new(run_type: RunType, sudo: Option<Sudo>, config: &'a Config) -> Self {
run_type: RunType, let under_ssh = var("SSH_CLIENT").is_ok() || var("SSH_TTY").is_ok();
sudo: &'a Option<PathBuf>, Self {
git: &'a Git,
config: &'a Config,
base_dirs: &'a BaseDirs,
) -> ExecutionContext<'a> {
ExecutionContext {
run_type, run_type,
sudo, sudo,
git,
config, config,
base_dirs, tmux_session: Mutex::new(None),
under_ssh,
} }
} }
pub fn execute_elevated(&self, command: &Path, interactive: bool) -> Result<Executor> { pub fn execute_elevated(&self, command: &Path, interactive: bool) -> Result<Executor> {
let sudo = require_option(self.sudo.clone(), "Sudo is required for this operation".into())?; let sudo = require_option(self.sudo.as_ref(), get_require_sudo_string())?;
let mut cmd = self.run_type.execute(&sudo); Ok(sudo.execute_elevated(self, command, interactive))
if sudo.ends_with("sudo") {
cmd.arg("--preserve-env=DIFFPROG");
}
if interactive {
cmd.arg("-i");
}
cmd.arg(command);
Ok(cmd)
} }
pub fn run_type(&self) -> RunType { pub fn run_type(&self) -> RunType {
self.run_type self.run_type
} }
pub fn git(&self) -> &Git { pub fn sudo(&self) -> &Option<Sudo> {
self.git &self.sudo
}
pub fn sudo(&self) -> &Option<PathBuf> {
self.sudo
} }
pub fn config(&self) -> &Config { pub fn config(&self) -> &Config {
self.config self.config
} }
pub fn base_dirs(&self) -> &BaseDirs { pub fn under_ssh(&self) -> bool {
self.base_dirs self.under_ssh
}
pub fn set_tmux_session(&self, session_name: String) {
self.tmux_session.lock().unwrap().replace(session_name);
}
pub fn get_tmux_session(&self) -> Option<String> {
self.tmux_session.lock().unwrap().clone()
} }
} }

View File

@@ -1,11 +1,14 @@
//! Utilities for command execution //! Utilities for command execution
use crate::error::{DryRun, TopgradeError};
use crate::utils::{Check, CheckWithCodes};
use anyhow::Result;
use log::{debug, trace};
use std::ffi::{OsStr, OsString}; use std::ffi::{OsStr, OsString};
use std::path::Path; use std::path::Path;
use std::process::{Child, Command, ExitStatus}; use std::process::{Child, Command, ExitStatus, Output};
use color_eyre::eyre::Result;
use rust_i18n::t;
use tracing::debug;
use crate::command::CommandExt;
use crate::error::DryRun;
/// An enum telling whether Topgrade should perform dry runs or actually perform the steps. /// An enum telling whether Topgrade should perform dry runs or actually perform the steps.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@@ -56,6 +59,16 @@ pub enum Executor {
} }
impl Executor { impl Executor {
/// Get the name of the program being run.
///
/// Will give weird results for non-UTF-8 programs; see `to_string_lossy()`.
pub fn get_program(&self) -> String {
match self {
Executor::Wet(c) => c.get_program().to_string_lossy().into_owned(),
Executor::Dry(c) => c.program.to_string_lossy().into_owned(),
}
}
/// See `std::process::Command::arg` /// See `std::process::Command::arg`
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Executor { pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Executor {
match self { match self {
@@ -139,7 +152,7 @@ impl Executor {
let result = match self { let result = match self {
Executor::Wet(c) => { Executor::Wet(c) => {
debug!("Running {:?}", c); debug!("Running {:?}", c);
c.spawn().map(ExecutorChild::Wet)? c.spawn_checked().map(ExecutorChild::Wet)?
} }
Executor::Dry(c) => { Executor::Dry(c) => {
c.dry_run(); c.dry_run();
@@ -153,7 +166,7 @@ impl Executor {
/// See `std::process::Command::output` /// See `std::process::Command::output`
pub fn output(&mut self) -> Result<ExecutorOutput> { pub fn output(&mut self) -> Result<ExecutorOutput> {
match self { match self {
Executor::Wet(c) => Ok(ExecutorOutput::Wet(c.output()?)), Executor::Wet(c) => Ok(ExecutorOutput::Wet(c.output_checked()?)),
Executor::Dry(c) => { Executor::Dry(c) => {
c.dry_run(); c.dry_run();
Ok(ExecutorOutput::Dry) Ok(ExecutorOutput::Dry)
@@ -161,23 +174,28 @@ impl Executor {
} }
} }
/// A convinence method for `spawn().wait().check()`. /// An extension of `status_checked` that allows you to set a sequence of codes
/// Returns an error if something went wrong during the execution or if the
/// process exited with failure.
pub fn check_run(&mut self) -> Result<()> {
self.spawn()?.wait()?.check()
}
/// An extension of `check_run` that allows you to set a sequence of codes
/// that can indicate success of a script /// that can indicate success of a script
#[allow(dead_code)] #[allow(dead_code)]
pub fn check_run_with_codes(&mut self, codes: &[i32]) -> Result<()> { pub fn status_checked_with_codes(&mut self, codes: &[i32]) -> Result<()> {
self.spawn()?.wait()?.check_with_codes(codes) match self {
Executor::Wet(c) => c.status_checked_with(|status| {
if status.success() || status.code().as_ref().map(|c| codes.contains(c)).unwrap_or(false) {
Ok(())
} else {
Err(())
}
}),
Executor::Dry(c) => {
c.dry_run();
Ok(())
}
}
} }
} }
pub enum ExecutorOutput { pub enum ExecutorOutput {
Wet(std::process::Output), Wet(Output),
Dry, Dry,
} }
@@ -192,17 +210,20 @@ pub struct DryCommand {
impl DryCommand { impl DryCommand {
fn dry_run(&self) { fn dry_run(&self) {
print!( print!(
"Dry running: {} {}", "{}",
self.program.to_string_lossy(), t!(
shell_words::join( "Dry running: {program_name} {arguments}",
self.args program_name = self.program.to_string_lossy(),
.iter() arguments = shell_words::join(
.map(|a| String::from(a.to_string_lossy())) self.args
.collect::<Vec<String>>() .iter()
.map(|a| String::from(a.to_string_lossy()))
.collect::<Vec<String>>()
)
) )
); );
match &self.directory { match &self.directory {
Some(dir) => println!(" in {}", dir.to_string_lossy()), Some(dir) => println!(" {}", t!("in {directory}", directory = dir.to_string_lossy())),
None => println!(), None => println!(),
}; };
} }
@@ -210,82 +231,38 @@ impl DryCommand {
/// The Result of spawn. Contains an actual `std::process::Child` if executed by a wet command. /// The Result of spawn. Contains an actual `std::process::Child` if executed by a wet command.
pub enum ExecutorChild { pub enum ExecutorChild {
#[allow(unused)] // this type has not been used
Wet(Child), Wet(Child),
Dry, Dry,
} }
impl ExecutorChild {
/// See `std::process::Child::wait`
pub fn wait(&mut self) -> Result<ExecutorExitStatus> {
let result = match self {
ExecutorChild::Wet(c) => c.wait().map(ExecutorExitStatus::Wet)?,
ExecutorChild::Dry => ExecutorExitStatus::Dry,
};
Ok(result)
}
}
/// The Result of wait. Contains an actual `std::process::ExitStatus` if executed by a wet command.
pub enum ExecutorExitStatus {
Wet(ExitStatus),
Dry,
}
impl CheckWithCodes for ExecutorExitStatus {
fn check_with_codes(self, codes: &[i32]) -> Result<()> {
match self {
ExecutorExitStatus::Wet(e) => e.check_with_codes(codes),
ExecutorExitStatus::Dry => Ok(()),
}
}
}
/// Extension methods for `std::process::Command`
pub trait CommandExt {
/// Run the command, wait for it to complete, check the return code and decode the output as UTF-8.
fn check_output(&mut self) -> Result<String>;
fn string_output(&mut self) -> Result<String>;
}
impl CommandExt for Command {
fn check_output(&mut self) -> Result<String> {
let output = self.output()?;
trace!("Output of {:?}: {:?}", self, output);
let status = output.status;
if !status.success() {
let stderr = String::from_utf8(output.stderr).unwrap_or_default();
return Err(TopgradeError::ProcessFailedWithOutput(status, stderr).into());
}
Ok(String::from_utf8(output.stdout)?)
}
fn string_output(&mut self) -> Result<String> {
let output = self.output()?;
trace!("Output of {:?}: {:?}", self, output);
Ok(String::from_utf8(output.stdout)?)
}
}
impl CommandExt for Executor { impl CommandExt for Executor {
fn check_output(&mut self) -> Result<String> { type Child = ExecutorChild;
let output = match self.output()? {
ExecutorOutput::Wet(output) => output, // TODO: It might be nice to make `output_checked_with` return something that has a
ExecutorOutput::Dry => return Err(DryRun().into()), // variant for wet/dry runs.
};
let status = output.status; fn output_checked_with(&mut self, succeeded: impl Fn(&Output) -> Result<(), ()>) -> Result<Output> {
if !status.success() { match self {
let stderr = String::from_utf8(output.stderr).unwrap_or_default(); Executor::Wet(c) => c.output_checked_with(succeeded),
return Err(TopgradeError::ProcessFailedWithOutput(status, stderr).into()); Executor::Dry(c) => {
c.dry_run();
Err(DryRun().into())
}
} }
Ok(String::from_utf8(output.stdout)?)
} }
fn string_output(&mut self) -> Result<String> { fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> Result<()> {
let output = match self.output()? { match self {
ExecutorOutput::Wet(output) => output, Executor::Wet(c) => c.status_checked_with(succeeded),
ExecutorOutput::Dry => return Err(DryRun().into()), Executor::Dry(c) => {
}; c.dry_run();
Ok(String::from_utf8(output.stdout)?) Ok(())
}
}
}
fn spawn_checked(&mut self) -> Result<Self::Child> {
self.spawn()
} }
} }

View File

@@ -2,14 +2,24 @@
use std::env; use std::env;
use std::io; use std::io;
use std::path::PathBuf;
use std::process::exit; use std::process::exit;
use std::time::Duration;
use anyhow::{anyhow, Result}; use crate::breaking_changes::{first_run_of_major_release, print_breaking_changes, should_skip, write_keep_file};
use clap::CommandFactory;
use clap::{crate_version, Parser}; use clap::{crate_version, Parser};
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use console::Key; use console::Key;
use log::debug; use etcetera::base_strategy::BaseStrategy;
use log::LevelFilter; #[cfg(windows)]
use pretty_env_logger::formatted_timed_builder; 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::config::{CommandLineArgs, Config, Step};
use self::error::StepFailed; use self::error::StepFailed;
@@ -18,6 +28,10 @@ use self::error::Upgraded;
use self::steps::{remote::*, *}; use self::steps::{remote::*, *};
use self::terminal::*; use self::terminal::*;
use self::utils::{hostname, install_color_eyre, install_tracing, update_tracing};
mod breaking_changes;
mod command;
mod config; mod config;
mod ctrlc; mod ctrlc;
mod error; mod error;
@@ -30,15 +44,54 @@ mod self_renamer;
#[cfg(feature = "self-update")] #[cfg(feature = "self-update")]
mod self_update; mod self_update;
mod steps; mod steps;
mod sudo;
mod terminal; mod terminal;
mod utils; mod utils;
pub(crate) static HOME_DIR: Lazy<PathBuf> = Lazy::new(|| home::home_dir().expect("No home directory"));
#[cfg(unix)]
pub(crate) static XDG_DIRS: Lazy<Xdg> = Lazy::new(|| Xdg::new().expect("No home directory"));
#[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");
fn run() -> Result<()> { fn run() -> Result<()> {
install_color_eyre()?;
ctrlc::set_handler(); ctrlc::set_handler();
let base_dirs = directories::BaseDirs::new().ok_or_else(|| anyhow!("No base directories"))?;
let opt = CommandLineArgs::parse(); let opt = CommandLineArgs::parse();
// Set up the logger with the filter directives from:
// 1. CLI option `--log-filter`
// 2. `debug` if the `--verbose` option is present
// We do this because we need our logger to work while loading the
// configuration file.
//
// When the configuration file is loaded, update the logger with the full
// filter directives.
//
// For more info, see the comments in `CommandLineArgs::tracing_filter_directives()`
// 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());
return Ok(());
}
if opt.gen_manpage {
let man = clap_mangen::Man::new(CommandLineArgs::command());
man.render(&mut io::stdout())?;
return Ok(());
}
for env in opt.env_variables() { for env in opt.env_variables() {
let mut splitted = env.split('='); let mut splitted = env.split('=');
@@ -47,66 +100,74 @@ fn run() -> Result<()> {
env::set_var(var, value); env::set_var(var, value);
} }
let mut builder = formatted_timed_builder();
if opt.verbose {
builder.filter(Some("topgrade"), LevelFilter::Trace);
}
builder.init();
if opt.edit_config() { if opt.edit_config() {
Config::edit(&base_dirs)?; Config::edit()?;
return Ok(()); return Ok(());
}; };
if opt.show_config_reference() { if opt.show_config_reference() {
print!("{}", crate::config::EXAMPLE_CONFIG); print!("{}", config::EXAMPLE_CONFIG);
return Ok(()); return Ok(());
} }
let config = Config::load(&base_dirs, opt)?; let config = Config::load(opt)?;
terminal::set_title(config.set_title()); // Update the logger with the full filter directives.
terminal::display_time(config.display_time()); update_tracing(&reload_handle, &config.tracing_filter_directives())?;
terminal::set_desktop_notifications(config.notify_each_step()); set_title(config.set_title());
display_time(config.display_time());
set_desktop_notifications(config.notify_each_step());
debug!("Version: {}", crate_version!()); debug!("Version: {}", crate_version!());
debug!("OS: {}", env!("TARGET")); debug!("OS: {}", env!("TARGET"));
debug!("{:?}", std::env::args()); debug!("{:?}", std::env::args());
debug!("Binary path: {:?}", std::env::current_exe()); debug!("Binary path: {:?}", std::env::current_exe());
debug!("Self Update: {:?}", cfg!(feature = "self-update")); debug!("self-update Feature Enabled: {:?}", cfg!(feature = "self-update"));
debug!("Configuration: {:?}", config);
if config.run_in_tmux() && env::var("TOPGRADE_INSIDE_TMUX").is_err() { if config.run_in_tmux() && env::var("TOPGRADE_INSIDE_TMUX").is_err() {
#[cfg(unix)] #[cfg(unix)]
{ {
tmux::run_in_tmux(config.tmux_arguments()?); tmux::run_in_tmux(config.tmux_config()?)?;
return Ok(());
} }
} }
let git = git::Git::new(); let powershell = powershell::Powershell::new();
let mut git_repos = git::Repositories::new(&git); let should_run_powershell = powershell.profile().is_some() && config.should_run(Step::Powershell);
let emacs = emacs::Emacs::new();
#[cfg(target_os = "linux")]
let distribution = linux::Distribution::detect();
let sudo = utils::sudo(); let sudo = config.sudo_command().map_or_else(sudo::Sudo::detect, sudo::Sudo::new);
let run_type = executor::RunType::new(config.dry_run()); let run_type = executor::RunType::new(config.dry_run());
let ctx = execution_context::ExecutionContext::new(run_type, sudo, &config);
let ctx = execution_context::ExecutionContext::new(run_type, &sudo, &git, &config, &base_dirs);
let mut runner = runner::Runner::new(&ctx); let mut runner = runner::Runner::new(&ctx);
// If
//
// 1. the breaking changes notification shouldnot be skipped
// 2. this is the first execution of a major release
//
// inform user of breaking changes
if !should_skip() && first_run_of_major_release()? {
print_breaking_changes();
if prompt_yesno("Confirmed?")? {
write_keep_file()?;
} else {
exit(1);
}
}
// Self-Update step, this will execute only if:
// 1. the `self-update` feature is enabled
// 2. it is not disabled from configuration (env var/CLI opt/file)
#[cfg(feature = "self-update")] #[cfg(feature = "self-update")]
{ {
if !run_type.dry() && env::var("TOPGRADE_NO_SELF_UPGRADE").is_err() { let should_self_update = env::var("TOPGRADE_NO_SELF_UPGRADE").is_err() && !config.no_self_update();
let result = self_update::self_update();
if let Err(e) = &result { if should_self_update {
#[cfg(windows)] runner.execute(Step::SelfUpdate, "Self Update", || self_update::self_update(&ctx))?;
{
if e.downcast_ref::<Upgraded>().is_some() {
return result;
}
}
print_warning(format!("Self update error: {}", e));
}
} }
} }
@@ -123,45 +184,67 @@ fn run() -> Result<()> {
} }
} }
let powershell = powershell::Powershell::new(); if config.pre_sudo() {
let should_run_powershell = powershell.profile().is_some() && config.should_run(Step::Powershell); if let Some(sudo) = ctx.sudo() {
sudo.elevate(&ctx)?;
#[cfg(windows)] }
runner.execute(Step::Wsl, "WSL", || windows::run_wsl_topgrade(&ctx))?; }
if let Some(topgrades) = config.remote_topgrades() { if let Some(topgrades) = config.remote_topgrades() {
for remote_topgrade in topgrades.iter().filter(|t| config.should_execute_remote(t)) { for remote_topgrade in topgrades.iter().filter(|t| config.should_execute_remote(hostname(), t)) {
runner.execute(Step::Remotes, format!("Remote ({})", remote_topgrade), || { runner.execute(Step::Remotes, format!("Remote ({remote_topgrade})"), || {
remote::ssh::ssh_step(&ctx, remote_topgrade) ssh::ssh_step(&ctx, remote_topgrade)
})?; })?;
} }
} }
#[cfg(target_os = "linux")] #[cfg(windows)]
let distribution = linux::Distribution::detect();
#[cfg(target_os = r#"linux"#)]
{ {
runner.execute(Step::Wsl, "WSL", || windows::run_wsl_topgrade(&ctx))?;
runner.execute(Step::WslUpdate, "WSL", || windows::update_wsl(&ctx))?;
runner.execute(Step::Chocolatey, "Chocolatey", || windows::run_chocolatey(&ctx))?;
runner.execute(Step::Scoop, "Scoop", || windows::run_scoop(&ctx))?;
runner.execute(Step::Winget, "Winget", || windows::run_winget(&ctx))?;
runner.execute(Step::System, "Windows update", || windows::windows_update(&ctx))?;
}
#[cfg(target_os = "linux")]
{
// NOTE: Due to breaking `nu` updates, `packer.nu` needs to be updated before `nu` get updated
// by other package managers.
runner.execute(Step::Shell, "packer.nu", || linux::run_packer_nu(&ctx))?;
match &distribution { match &distribution {
Ok(distribution) => { Ok(distribution) => {
runner.execute(Step::System, "System update", || distribution.upgrade(&ctx))?; runner.execute(Step::System, "System update", || distribution.upgrade(&ctx))?;
} }
Err(e) => { 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))?; runner.execute(Step::ConfigUpdate, "config-update", || linux::run_config_update(&ctx))?;
runner.execute(Step::AM, "am", || linux::run_am(&ctx))?;
runner.execute(Step::AppMan, "appman", || linux::run_appman(&ctx))?;
runner.execute(Step::DebGet, "deb-get", || linux::run_deb_get(&ctx))?;
runner.execute(Step::Toolbx, "toolbx", || toolbx::run_toolbx(&ctx))?;
runner.execute(Step::Snap, "snap", || linux::run_snap(&ctx))?;
runner.execute(Step::Pacstall, "pacstall", || linux::run_pacstall(&ctx))?;
runner.execute(Step::Pacdef, "pacdef", || linux::run_pacdef(&ctx))?;
runner.execute(Step::Protonup, "protonup", || linux::run_protonup_update(&ctx))?;
runner.execute(Step::Distrobox, "distrobox", || linux::run_distrobox_update(&ctx))?;
runner.execute(Step::DkpPacman, "dkp-pacman", || linux::run_dkp_pacman_update(&ctx))?;
runner.execute(Step::System, "pihole", || linux::run_pihole_update(&ctx))?;
runner.execute(Step::Firmware, "Firmware upgrades", || linux::run_fwupdmgr(&ctx))?;
runner.execute(Step::Restarts, "Restarts", || linux::run_needrestart(&ctx))?;
runner.execute(Step::Flatpak, "Flatpak", || linux::run_flatpak(&ctx))?;
runner.execute(Step::BrewFormula, "Brew", || { runner.execute(Step::BrewFormula, "Brew", || {
unix::run_brew_formula(&ctx, unix::BrewVariant::Path) unix::run_brew_formula(&ctx, unix::BrewVariant::Path)
})?; })?;
} runner.execute(Step::Lure, "LURE", || linux::run_lure_update(&ctx))?;
runner.execute(Step::Waydroid, "Waydroid", || linux::run_waydroid(&ctx))?;
#[cfg(windows)] runner.execute(Step::AutoCpufreq, "auto-cpufreq", || linux::run_auto_cpufreq(&ctx))?;
{
runner.execute(Step::Chocolatey, "Chocolatey", || windows::run_chocolatey(&ctx))?;
runner.execute(Step::Scoop, "Scoop", || windows::run_scoop(config.cleanup(), run_type))?;
runner.execute(Step::Winget, "Winget", || windows::run_winget(&ctx))?;
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -185,133 +268,76 @@ fn run() -> Result<()> {
unix::run_brew_cask(&ctx, unix::BrewVariant::Path) unix::run_brew_cask(&ctx, unix::BrewVariant::Path)
})?; })?;
runner.execute(Step::Macports, "MacPorts", || macos::run_macports(&ctx))?; runner.execute(Step::Macports, "MacPorts", || macos::run_macports(&ctx))?;
runner.execute(Step::Xcodes, "Xcodes", || macos::update_xcodes(&ctx))?;
runner.execute(Step::Sparkle, "Sparkle", || macos::run_sparkle(&ctx))?;
runner.execute(Step::Mas, "App Store", || macos::run_mas(&ctx))?;
runner.execute(Step::System, "System upgrade", || macos::upgrade_macos(&ctx))?;
}
#[cfg(target_os = "dragonfly")]
{
runner.execute(Step::Pkg, "DragonFly BSD Packages", || {
dragonfly::upgrade_packages(&ctx)
})?;
runner.execute(Step::Audit, "DragonFly Audit", || dragonfly::audit_packages(&ctx))?;
}
#[cfg(target_os = "freebsd")]
{
runner.execute(Step::Pkg, "FreeBSD Packages", || freebsd::upgrade_packages(&ctx))?;
runner.execute(Step::System, "FreeBSD Upgrade", || freebsd::upgrade_freebsd(&ctx))?;
runner.execute(Step::Audit, "FreeBSD Audit", || freebsd::audit_packages(&ctx))?;
}
#[cfg(target_os = "openbsd")]
{
runner.execute(Step::Pkg, "OpenBSD Packages", || openbsd::upgrade_packages(&ctx))?;
runner.execute(Step::System, "OpenBSD Upgrade", || openbsd::upgrade_openbsd(&ctx))?;
}
#[cfg(target_os = "android")]
{
runner.execute(Step::Pkg, "Termux Packages", || android::upgrade_packages(&ctx))?;
} }
#[cfg(unix)] #[cfg(unix)]
{ {
runner.execute(Step::Yadm, "yadm", || unix::run_yadm(&ctx))?; runner.execute(Step::Yadm, "yadm", || unix::run_yadm(&ctx))?;
runner.execute(Step::Nix, "nix", || unix::run_nix(&ctx))?; runner.execute(Step::Nix, "nix", || unix::run_nix(&ctx))?;
runner.execute(Step::Nix, "nix upgrade-nix", || unix::run_nix_self_upgrade(&ctx))?;
runner.execute(Step::Guix, "guix", || unix::run_guix(&ctx))?; runner.execute(Step::Guix, "guix", || unix::run_guix(&ctx))?;
runner.execute(Step::HomeManager, "home-manager", || unix::run_home_manager(&ctx))?;
runner.execute(Step::HomeManager, "home-manager", || unix::run_home_manager(run_type))?; runner.execute(Step::Asdf, "asdf", || unix::run_asdf(&ctx))?;
runner.execute(Step::Asdf, "asdf", || unix::run_asdf(run_type))?; runner.execute(Step::Mise, "mise", || unix::run_mise(&ctx))?;
runner.execute(Step::Pkgin, "pkgin", || unix::run_pkgin(&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))?;
#[cfg(target_os = "dragonfly")] runner.execute(Step::Shell, "antidote", || zsh::run_antidote(&ctx))?;
runner.execute(Step::Pkg, "DragonFly BSD Packages", || { runner.execute(Step::Shell, "antigen", || zsh::run_antigen(&ctx))?;
dragonfly::upgrade_packages(sudo.as_ref(), run_type) runner.execute(Step::Shell, "zgenom", || zsh::run_zgenom(&ctx))?;
})?; runner.execute(Step::Shell, "zplug", || zsh::run_zplug(&ctx))?;
runner.execute(Step::Shell, "zinit", || zsh::run_zinit(&ctx))?;
#[cfg(target_os = "freebsd")] runner.execute(Step::Shell, "zi", || zsh::run_zi(&ctx))?;
runner.execute(Step::Pkg, "FreeBSD Packages", || { runner.execute(Step::Shell, "zim", || zsh::run_zim(&ctx))?;
freebsd::upgrade_packages(sudo.as_ref(), run_type)
})?;
#[cfg(target_os = "openbsd")]
runner.execute(Step::Pkg, "OpenBSD Packages", || {
openbsd::upgrade_packages(sudo.as_ref(), run_type)
})?;
#[cfg(target_os = "android")]
runner.execute(Step::Pkg, "Termux Packages", || android::upgrade_packages(&ctx))?;
let emacs = emacs::Emacs::new(&base_dirs);
if config.use_predefined_git_repos() {
if config.should_run(Step::Emacs) {
if !emacs.is_doom() {
if let Some(directory) = emacs.directory() {
git_repos.insert_if_repo(directory);
}
}
git_repos.insert_if_repo(base_dirs.home_dir().join(".doom.d"));
}
if config.should_run(Step::Vim) {
git_repos.insert_if_repo(base_dirs.home_dir().join(".vim"));
git_repos.insert_if_repo(base_dirs.home_dir().join(".config/nvim"));
}
git_repos.insert_if_repo(base_dirs.home_dir().join(".ideavimrc"));
git_repos.insert_if_repo(base_dirs.home_dir().join(".intellimacs"));
if config.should_run(Step::Rcm) {
git_repos.insert_if_repo(base_dirs.home_dir().join(".dotfiles"));
}
#[cfg(unix)]
{
git_repos.insert_if_repo(zsh::zshrc(&base_dirs));
if config.should_run(Step::Tmux) {
git_repos.insert_if_repo(base_dirs.home_dir().join(".tmux"));
}
git_repos.insert_if_repo(base_dirs.home_dir().join(".config/fish"));
git_repos.insert_if_repo(base_dirs.config_dir().join("openbox"));
git_repos.insert_if_repo(base_dirs.config_dir().join("bspwm"));
git_repos.insert_if_repo(base_dirs.config_dir().join("i3"));
git_repos.insert_if_repo(base_dirs.config_dir().join("sway"));
}
#[cfg(windows)]
git_repos.insert_if_repo(
base_dirs
.data_local_dir()
.join("Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState"),
);
#[cfg(windows)]
windows::insert_startup_scripts(&ctx, &mut git_repos).ok();
if let Some(profile) = powershell.profile() {
git_repos.insert_if_repo(profile);
}
}
if config.should_run(Step::GitRepos) {
if let Some(custom_git_repos) = config.git_repos() {
for git_repo in custom_git_repos {
git_repos.glob_insert(git_repo);
}
}
runner.execute(Step::GitRepos, "Git repositories", || {
git.multi_pull_step(&git_repos, &ctx)
})?;
}
if should_run_powershell {
runner.execute(Step::Powershell, "Powershell Modules Update", || {
powershell.update_modules(&ctx)
})?;
}
#[cfg(unix)]
{
runner.execute(Step::Shell, "zr", || zsh::run_zr(&base_dirs, run_type))?;
runner.execute(Step::Shell, "antibody", || zsh::run_antibody(run_type))?;
runner.execute(Step::Shell, "antigen", || zsh::run_antigen(&base_dirs, run_type))?;
runner.execute(Step::Shell, "zgenom", || zsh::run_zgenom(&base_dirs, run_type))?;
runner.execute(Step::Shell, "zplug", || zsh::run_zplug(&base_dirs, run_type))?;
runner.execute(Step::Shell, "zinit", || zsh::run_zinit(&base_dirs, run_type))?;
runner.execute(Step::Shell, "zi", || zsh::run_zi(&base_dirs, run_type))?;
runner.execute(Step::Shell, "zim", || zsh::run_zim(&base_dirs, run_type))?;
runner.execute(Step::Shell, "oh-my-zsh", || zsh::run_oh_my_zsh(&ctx))?; runner.execute(Step::Shell, "oh-my-zsh", || zsh::run_oh_my_zsh(&ctx))?;
runner.execute(Step::Shell, "fisher", || unix::run_fisher(&base_dirs, run_type))?; runner.execute(Step::Shell, "oh-my-bash", || unix::run_oh_my_bash(&ctx))?;
runner.execute(Step::Shell, "fisher", || unix::run_fisher(&ctx))?;
runner.execute(Step::Shell, "bash-it", || unix::run_bashit(&ctx))?; runner.execute(Step::Shell, "bash-it", || unix::run_bashit(&ctx))?;
runner.execute(Step::Shell, "oh-my-fish", || unix::run_oh_my_fish(&ctx))?; runner.execute(Step::Shell, "oh-my-fish", || unix::run_oh_my_fish(&ctx))?;
runner.execute(Step::Shell, "fish-plug", || unix::run_fish_plug(&ctx))?; runner.execute(Step::Shell, "fish-plug", || unix::run_fish_plug(&ctx))?;
runner.execute(Step::Shell, "fundle", || unix::run_fundle(&ctx))?; runner.execute(Step::Shell, "fundle", || unix::run_fundle(&ctx))?;
runner.execute(Step::Tmux, "tmux", || tmux::run_tpm(&base_dirs, run_type))?; runner.execute(Step::Tmux, "tmux", || tmux::run_tpm(&ctx))?;
runner.execute(Step::Tldr, "TLDR", || unix::run_tldr(run_type))?; runner.execute(Step::Tldr, "TLDR", || unix::run_tldr(&ctx))?;
runner.execute(Step::Pearl, "pearl", || unix::run_pearl(run_type))?; runner.execute(Step::Pearl, "pearl", || unix::run_pearl(&ctx))?;
#[cfg(not(any(target_os = "macos", target_os = "android")))] #[cfg(not(any(target_os = "macos", target_os = "android")))]
runner.execute(Step::GnomeShellExtensions, "Gnome Shell Extensions", || { runner.execute(Step::GnomeShellExtensions, "Gnome Shell Extensions", || {
unix::upgrade_gnome_extensions(&ctx) unix::upgrade_gnome_extensions(&ctx)
})?; })?;
runner.execute(Step::Sdkman, "SDKMAN!", || { runner.execute(Step::Pyenv, "pyenv", || unix::run_pyenv(&ctx))?;
unix::run_sdkman(&base_dirs, config.cleanup(), run_type) runner.execute(Step::Sdkman, "SDKMAN!", || unix::run_sdkman(&ctx))?;
})?;
runner.execute(Step::Rcm, "rcm", || unix::run_rcm(&ctx))?; runner.execute(Step::Rcm, "rcm", || unix::run_rcm(&ctx))?;
runner.execute(Step::Maza, "maza", || unix::run_maza(&ctx))?;
} }
#[cfg(not(any( #[cfg(not(any(
@@ -320,68 +346,97 @@ fn run() -> Result<()> {
target_os = "netbsd", target_os = "netbsd",
target_os = "dragonfly" target_os = "dragonfly"
)))] )))]
runner.execute(Step::Atom, "apm", || generic::run_apm(run_type))?; {
runner.execute(Step::Fossil, "fossil", || generic::run_fossil(run_type))?; runner.execute(Step::Atom, "apm", || generic::run_apm(&ctx))?;
runner.execute(Step::Rustup, "rustup", || generic::run_rustup(&base_dirs, run_type))?; }
// The following update function should be executed on all OSes.
runner.execute(Step::Fossil, "fossil", || generic::run_fossil(&ctx))?;
runner.execute(Step::Elan, "elan", || generic::run_elan(&ctx))?;
runner.execute(Step::Rye, "rye", || generic::run_rye(&ctx))?;
runner.execute(Step::Rustup, "rustup", || generic::run_rustup(&ctx))?;
runner.execute(Step::Juliaup, "juliaup", || generic::run_juliaup(&ctx))?;
runner.execute(Step::Dotnet, ".NET", || generic::run_dotnet_upgrade(&ctx))?; runner.execute(Step::Dotnet, ".NET", || generic::run_dotnet_upgrade(&ctx))?;
runner.execute(Step::Choosenim, "choosenim", || generic::run_choosenim(&ctx))?; runner.execute(Step::Choosenim, "choosenim", || generic::run_choosenim(&ctx))?;
runner.execute(Step::Cargo, "cargo", || generic::run_cargo_update(&ctx))?; runner.execute(Step::Cargo, "cargo", || generic::run_cargo_update(&ctx))?;
runner.execute(Step::Flutter, "Flutter", || generic::run_flutter_upgrade(run_type))?; runner.execute(Step::Flutter, "Flutter", || generic::run_flutter_upgrade(&ctx))?;
runner.execute(Step::Go, "Go", || generic::run_go(run_type))?; runner.execute(Step::Go, "go-global-update", || go::run_go_global_update(&ctx))?;
runner.execute(Step::Go, "gup", || go::run_go_gup(&ctx))?;
runner.execute(Step::Emacs, "Emacs", || emacs.upgrade(&ctx))?; runner.execute(Step::Emacs, "Emacs", || emacs.upgrade(&ctx))?;
runner.execute(Step::Opam, "opam", || generic::run_opam_update(&ctx))?; runner.execute(Step::Opam, "opam", || generic::run_opam_update(&ctx))?;
runner.execute(Step::Vcpkg, "vcpkg", || generic::run_vcpkg_update(run_type))?; runner.execute(Step::Vcpkg, "vcpkg", || generic::run_vcpkg_update(&ctx))?;
runner.execute(Step::Pipx, "pipx", || generic::run_pipx_update(run_type))?; runner.execute(Step::Pipx, "pipx", || generic::run_pipx_update(&ctx))?;
runner.execute(Step::Vscode, "Visual Studio Code extensions", || {
generic::run_vscode_extensions_update(&ctx)
})?;
runner.execute(Step::Conda, "conda", || generic::run_conda_update(&ctx))?; runner.execute(Step::Conda, "conda", || generic::run_conda_update(&ctx))?;
runner.execute(Step::Pip3, "pip3", || generic::run_pip3_update(run_type))?; runner.execute(Step::Mamba, "mamba", || generic::run_mamba_update(&ctx))?;
runner.execute(Step::Ghcup, "ghcup", || generic::run_ghcup_update(run_type))?; runner.execute(Step::Pixi, "pixi", || generic::run_pixi_update(&ctx))?;
runner.execute(Step::Stack, "stack", || generic::run_stack_update(run_type))?; 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))?;
runner.execute(Step::PipReviewLocal, "pip-review (local)", || {
generic::run_pip_review_local_update(&ctx)
})?;
runner.execute(Step::Pipupgrade, "pipupgrade", || generic::run_pipupgrade_update(&ctx))?;
runner.execute(Step::Ghcup, "ghcup", || generic::run_ghcup_update(&ctx))?;
runner.execute(Step::Stack, "stack", || generic::run_stack_update(&ctx))?;
runner.execute(Step::Tlmgr, "tlmgr", || generic::run_tlmgr_update(&ctx))?; runner.execute(Step::Tlmgr, "tlmgr", || generic::run_tlmgr_update(&ctx))?;
runner.execute(Step::Myrepos, "myrepos", || { runner.execute(Step::Myrepos, "myrepos", || generic::run_myrepos_update(&ctx))?;
generic::run_myrepos_update(&base_dirs, run_type) runner.execute(Step::Chezmoi, "chezmoi", || generic::run_chezmoi_update(&ctx))?;
})?; runner.execute(Step::Jetpack, "jetpack", || generic::run_jetpack(&ctx))?;
runner.execute(Step::Chezmoi, "chezmoi", || { runner.execute(Step::Vim, "vim", || vim::upgrade_vim(&ctx))?;
generic::run_chezmoi_update(&base_dirs, run_type) runner.execute(Step::Vim, "Neovim", || vim::upgrade_neovim(&ctx))?;
})?;
runner.execute(Step::Jetpack, "jetpack", || generic::run_jetpack(run_type))?;
runner.execute(Step::Vim, "vim", || vim::upgrade_vim(&base_dirs, &ctx))?;
runner.execute(Step::Vim, "Neovim", || vim::upgrade_neovim(&base_dirs, &ctx))?;
runner.execute(Step::Vim, "The Ultimate vimrc", || vim::upgrade_ultimate_vimrc(&ctx))?; runner.execute(Step::Vim, "The Ultimate vimrc", || vim::upgrade_ultimate_vimrc(&ctx))?;
runner.execute(Step::Vim, "voom", || vim::run_voom(&base_dirs, run_type))?; runner.execute(Step::Vim, "voom", || vim::run_voom(&ctx))?;
runner.execute(Step::Kakoune, "Kakoune", || kakoune::upgrade_kak_plug(&ctx))?; runner.execute(Step::Kakoune, "Kakoune", || kakoune::upgrade_kak_plug(&ctx))?;
runner.execute(Step::Helix, "helix", || generic::run_helix_grammars(&ctx))?;
runner.execute(Step::Node, "npm", || node::run_npm_upgrade(&ctx))?; runner.execute(Step::Node, "npm", || node::run_npm_upgrade(&ctx))?;
runner.execute(Step::Node, "yarn", || node::run_yarn_upgrade(&ctx))?; runner.execute(Step::Yarn, "yarn", || node::run_yarn_upgrade(&ctx))?;
runner.execute(Step::Node, "pnpm", || node::run_pnpm_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::Containers, "Containers", || containers::run_containers(&ctx))?;
runner.execute(Step::Deno, "deno", || node::deno_upgrade(&ctx))?; runner.execute(Step::Deno, "deno", || node::deno_upgrade(&ctx))?;
runner.execute(Step::Composer, "composer", || generic::run_composer_update(&ctx))?; runner.execute(Step::Composer, "composer", || generic::run_composer_update(&ctx))?;
runner.execute(Step::Krew, "krew", || generic::run_krew_upgrade(run_type))?; runner.execute(Step::Krew, "krew", || generic::run_krew_upgrade(&ctx))?;
runner.execute(Step::Gem, "gem", || generic::run_gem(&base_dirs, run_type))?; runner.execute(Step::Helm, "helm", || generic::run_helm_repo_update(&ctx))?;
runner.execute(Step::Gem, "gem", || generic::run_gem(&ctx))?;
runner.execute(Step::RubyGems, "rubygems", || generic::run_rubygems(&ctx))?;
runner.execute(Step::Julia, "julia", || generic::update_julia_packages(&ctx))?; runner.execute(Step::Julia, "julia", || generic::update_julia_packages(&ctx))?;
runner.execute(Step::Haxelib, "haxelib", || generic::run_haxelib_update(&ctx))?; runner.execute(Step::Haxelib, "haxelib", || generic::run_haxelib_update(&ctx))?;
runner.execute(Step::Sheldon, "sheldon", || generic::run_sheldon(&ctx))?; runner.execute(Step::Sheldon, "sheldon", || generic::run_sheldon(&ctx))?;
runner.execute(Step::Stew, "stew", || generic::run_stew(&ctx))?;
runner.execute(Step::Rtcl, "rtcl", || generic::run_rtcl(&ctx))?; runner.execute(Step::Rtcl, "rtcl", || generic::run_rtcl(&ctx))?;
runner.execute(Step::Bin, "bin", || generic::bin_update(&ctx))?; runner.execute(Step::Bin, "bin", || generic::bin_update(&ctx))?;
runner.execute(Step::Gcloud, "gcloud", || { runner.execute(Step::Gcloud, "gcloud", || generic::run_gcloud_components_update(&ctx))?;
generic::run_gcloud_components_update(run_type) runner.execute(Step::Micro, "micro", || generic::run_micro(&ctx))?;
})?; runner.execute(Step::Raco, "raco", || generic::run_raco_update(&ctx))?;
runner.execute(Step::Micro, "micro", || generic::run_micro(run_type))?;
runner.execute(Step::Raco, "raco", || generic::run_raco_update(run_type))?;
runner.execute(Step::Spicetify, "spicetify", || generic::spicetify_upgrade(&ctx))?; runner.execute(Step::Spicetify, "spicetify", || generic::spicetify_upgrade(&ctx))?;
runner.execute(Step::GithubCliExtensions, "GitHub CLI Extensions", || { runner.execute(Step::GithubCliExtensions, "GitHub CLI Extensions", || {
generic::run_ghcli_extensions_upgrade(&ctx) generic::run_ghcli_extensions_upgrade(&ctx)
})?; })?;
runner.execute(Step::Bob, "Bob", || generic::run_bob(&ctx))?;
runner.execute(Step::Certbot, "Certbot", || generic::run_certbot(&ctx))?;
runner.execute(Step::GitRepos, "Git Repositories", || git::run_git_pull(&ctx))?;
runner.execute(Step::ClamAvDb, "ClamAV Databases", || generic::run_freshclam(&ctx))?;
runner.execute(Step::PlatformioCore, "PlatformIO Core", || {
generic::run_platform_io(&ctx)
})?;
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))?;
#[cfg(target_os = "linux")] if should_run_powershell {
{ runner.execute(Step::Powershell, "Powershell Modules Update", || {
runner.execute(Step::DebGet, "deb-get", || linux::run_deb_get(&ctx))?; powershell.update_modules(&ctx)
runner.execute(Step::Toolbx, "toolbx", || toolbx::run_toolbx(&ctx))?; })?;
runner.execute(Step::Flatpak, "Flatpak", || linux::flatpak_update(&ctx))?;
runner.execute(Step::Snap, "snap", || linux::run_snap(sudo.as_ref(), run_type))?;
runner.execute(Step::Pacstall, "pacstall", || linux::run_pacstall(&ctx))?;
runner.execute(Step::Pacdef, "pacdef", || linux::run_pacdef(&ctx))?;
runner.execute(Step::Protonup, "protonup", || linux::run_protonup_update(&ctx))?;
runner.execute(Step::Distrobox, "distrobox", || linux::run_distrobox_update(&ctx))?;
} }
if let Some(commands) = config.commands() { if let Some(commands) = config.commands() {
@@ -394,37 +449,6 @@ fn run() -> Result<()> {
} }
} }
#[cfg(target_os = "linux")]
{
runner.execute(Step::System, "pihole", || {
linux::run_pihole_update(sudo.as_ref(), run_type)
})?;
runner.execute(Step::Firmware, "Firmware upgrades", || linux::run_fwupdmgr(&ctx))?;
runner.execute(Step::Restarts, "Restarts", || {
linux::run_needrestart(sudo.as_ref(), run_type)
})?;
}
#[cfg(target_os = "macos")]
{
runner.execute(Step::Sparkle, "Sparkle", || macos::run_sparkle(&ctx))?;
runner.execute(Step::Mas, "App Store", || macos::run_mas(run_type))?;
runner.execute(Step::System, "System upgrade", || macos::upgrade_macos(&ctx))?;
}
#[cfg(target_os = "freebsd")]
runner.execute(Step::System, "FreeBSD Upgrade", || {
freebsd::upgrade_freebsd(sudo.as_ref(), run_type)
})?;
#[cfg(target_os = "openbsd")]
runner.execute(Step::System, "OpenBSD Upgrade", || {
openbsd::upgrade_openbsd(sudo.as_ref(), run_type)
})?;
#[cfg(windows)]
runner.execute(Step::System, "Windows update", || windows::windows_update(&ctx))?;
if config.should_run(Step::Vagrant) { if config.should_run(Step::Vagrant) {
if let Ok(boxes) = vagrant::collect_boxes(&ctx) { if let Ok(boxes) = vagrant::collect_boxes(&ctx) {
for vagrant_box in boxes { for vagrant_box in boxes {
@@ -437,7 +461,7 @@ fn run() -> Result<()> {
runner.execute(Step::Vagrant, "Vagrant boxes", || vagrant::upgrade_vagrant_boxes(&ctx))?; runner.execute(Step::Vagrant, "Vagrant boxes", || vagrant::upgrade_vagrant_boxes(&ctx))?;
if !runner.report().data().is_empty() { if !runner.report().data().is_empty() {
print_separator("Summary"); print_separator(t!("Summary"));
for (key, result) in runner.report().data() { for (key, result) in runner.report().data() {
print_result(key, result); print_result(key, result);
@@ -449,12 +473,6 @@ fn run() -> Result<()> {
distribution.show_summary(); distribution.show_summary();
} }
} }
#[cfg(target_os = "freebsd")]
freebsd::audit_packages(&sudo).ok();
#[cfg(target_os = "dragonfly")]
dragonfly::audit_packages(&sudo).ok();
} }
let mut post_command_failed = false; let mut post_command_failed = false;
@@ -467,14 +485,14 @@ fn run() -> Result<()> {
} }
if config.keep_at_end() { 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 { loop {
match get_key() { match get_key() {
Ok(Key::Char('s')) | Ok(Key::Char('S')) => { Ok(Key::Char('s')) | Ok(Key::Char('S')) => {
run_shell(); run_shell().context("Failed to execute shell")?;
} }
Ok(Key::Char('r')) | Ok(Key::Char('R')) => { Ok(Key::Char('r')) | Ok(Key::Char('R')) => {
reboot(); reboot().context("Failed to reboot")?;
} }
Ok(Key::Char('q')) | Ok(Key::Char('Q')) => (), Ok(Key::Char('q')) | Ok(Key::Char('Q')) => (),
_ => { _ => {
@@ -488,13 +506,14 @@ fn run() -> Result<()> {
let failed = post_command_failed || runner.report().data().iter().any(|(_, result)| result.failed()); let failed = post_command_failed || runner.report().data().iter().any(|(_, result)| result.failed());
if !config.skip_notify() { if !config.skip_notify() {
terminal::notify_desktop( notify_desktop(
format!( if failed {
"Topgrade finished {}", t!("Topgrade finished with errors")
if failed { "with errors" } else { "successfully" } } else {
), t!("Topgrade finished successfully")
None, },
); Some(Duration::from_secs(10)),
)
} }
if failed { if failed {
@@ -524,10 +543,10 @@ fn main() {
.is_some()); .is_some());
if !skip_print { if !skip_print {
// The `Debug` implementation of `anyhow::Result` prints a multi-line // The `Debug` implementation of `eyre::Result` prints a multi-line
// error message that includes all the 'causes' added with // error message that includes all the 'causes' added with
// `.with_context(...)` calls. // `.with_context(...)` calls.
println!("Error: {:?}", error); println!("{}", t!("Error: {error}", error = format!("{:?}", error)));
} }
exit(1); exit(1);
} }

View File

@@ -34,7 +34,7 @@ impl<'a> Report<'a> {
if let Some((key, success)) = result { if let Some((key, success)) = result {
let key = key.into(); let key = key.into();
debug_assert!(!self.data.iter().any(|(k, _)| k == &key), "{} already reported", key); debug_assert!(!self.data.iter().any(|(k, _)| k == &key), "{key} already reported");
self.data.push((key, success)); self.data.push((key, success));
} }
} }

View File

@@ -2,11 +2,12 @@ use crate::ctrlc;
use crate::error::{DryRun, SkipStep}; use crate::error::{DryRun, SkipStep};
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::report::{Report, StepResult}; use crate::report::{Report, StepResult};
use crate::terminal::print_error;
use crate::{config::Step, terminal::should_retry}; use crate::{config::Step, terminal::should_retry};
use anyhow::Result; use color_eyre::eyre::Result;
use log::debug;
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt::Debug; use std::fmt::Debug;
use tracing::debug;
pub struct Runner<'a> { pub struct Runner<'a> {
ctx: &'a ExecutionContext<'a>, ctx: &'a ExecutionContext<'a>,
@@ -33,6 +34,14 @@ impl<'a> Runner<'a> {
let key = key.into(); let key = key.into();
debug!("Step {:?}", key); debug!("Step {:?}", key);
// alter the `func` to put it in a span
let func = || {
let span =
tracing::span!(parent: tracing::Span::none(), tracing::Level::TRACE, "step", step = ?step, key = %key);
let _guard = span.enter();
func()
};
loop { loop {
match func() { match func() {
Ok(()) => { Ok(()) => {
@@ -55,7 +64,12 @@ impl<'a> Runner<'a> {
let ignore_failure = self.ctx.config().ignore_failure(step); let ignore_failure = self.ctx.config().ignore_failure(step);
let should_ask = interrupted || !(self.ctx.config().no_retry() || ignore_failure); let should_ask = interrupted || !(self.ctx.config().no_retry() || ignore_failure);
let should_retry = should_ask && should_retry(interrupted, key.as_ref())?; let should_retry = if should_ask {
print_error(&key, format!("{e:?}"));
should_retry(interrupted, key.as_ref())?
} else {
false
};
if !should_retry { if !should_retry {
self.report.push_result(Some(( self.report.push_result(Some((

View File

@@ -1,8 +1,6 @@
#![cfg(windows)] use color_eyre::eyre::Result;
use anyhow::Result;
use log::{debug, error};
use std::{env::current_exe, fs, path::PathBuf}; use std::{env::current_exe, fs, path::PathBuf};
use tracing::{debug, error};
pub struct SelfRenamer { pub struct SelfRenamer {
exe_path: PathBuf, exe_path: PathBuf,

View File

@@ -1,63 +1,73 @@
use std::env;
#[cfg(unix)]
use std::os::unix::process::CommandExt as _;
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::*;
#[cfg(windows)] #[cfg(windows)]
use crate::error::Upgraded; use crate::error::Upgraded;
use anyhow::{bail, Result};
use self_update_crate::backends::github::Update;
use self_update_crate::update::UpdateStatus;
use std::env;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::process::Command;
pub fn self_update() -> Result<()> { use crate::execution_context::ExecutionContext;
print_separator("Self update");
let current_exe = env::current_exe();
let target = self_update_crate::get_target(); pub fn self_update(ctx: &ExecutionContext) -> Result<()> {
let result = Update::configure() print_separator(t!("Self update"));
.repo_owner("topgrade-rs")
.repo_name("topgrade")
.target(target)
.bin_name(if cfg!(windows) {
"topgrade-rs.exe"
} else {
"topgrade-rs"
})
.show_output(false)
.show_download_progress(true)
.current_version(self_update_crate::cargo_crate_version!())
.no_confirm(true)
.build()?
.update_extended()?;
if let UpdateStatus::Updated(release) = &result { if ctx.run_type().dry() {
println!("\nTopgrade upgraded to {}:\n", release.version); println!("{}", t!("Would self-update"));
if let Some(body) = &release.body { Ok(())
println!("{}", body);
}
} else { } else {
println!("Topgrade is up-to-date"); let assume_yes = ctx.config().yes(Step::SelfUpdate);
} let current_exe = env::current_exe();
{ let target = self_update_crate::get_target();
if result.updated() { let result = Update::configure()
print_warning("Respawning..."); .repo_owner("topgrade-rs")
let mut command = Command::new(current_exe?); .repo_name("topgrade")
command.args(env::args().skip(1)).env("TOPGRADE_NO_SELF_UPGRADE", ""); .target(target)
.bin_name(if cfg!(windows) { "topgrade.exe" } else { "topgrade" })
.show_output(true)
.show_download_progress(true)
.current_version(self_update_crate::cargo_crate_version!())
.no_confirm(assume_yes)
.build()?
.update_extended()?;
#[cfg(unix)] if let UpdateStatus::Updated(release) = &result {
{ println!("{}", t!("Topgrade upgraded to {version}:\n", version = release.version));
let err = command.exec(); if let Some(body) = &release.body {
bail!(err); println!("{body}");
} }
} else {
println!("{}", t!("Topgrade is up-to-date"));
}
#[cfg(windows)] {
{ if result.updated() {
let status = command.spawn().and_then(|mut c| c.wait())?; print_info(t!("Respawning..."));
bail!(Upgraded(status)); let mut command = Command::new(current_exe?);
command.args(env::args().skip(1)).env("TOPGRADE_NO_SELF_UPGRADE", "");
#[cfg(unix)]
{
let err = command.exec();
bail!(err);
}
#[cfg(windows)]
{
#[allow(clippy::disallowed_methods)]
let status = command.status()?;
bail!(Upgraded(status));
}
} }
} }
}
Ok(()) Ok(())
}
} }

View File

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

View File

@@ -1,10 +1,12 @@
#[cfg(any(windows, target_os = "macos"))] #[cfg(windows)]
use std::env; use std::env;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::Result; use color_eyre::eyre::Result;
use directories::BaseDirs; use etcetera::base_strategy::BaseStrategy;
use rust_i18n::t;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::terminal::print_separator; use crate::terminal::print_separator;
use crate::utils::{require, require_option, PathExt}; use crate::utils::{require, require_option, PathExt};
@@ -22,20 +24,12 @@ pub struct Emacs {
} }
impl Emacs { impl Emacs {
fn directory_path(base_dirs: &BaseDirs) -> Option<PathBuf> { fn directory_path() -> Option<PathBuf> {
#[cfg(unix)] #[cfg(unix)]
cfg_if::cfg_if! { return {
if #[cfg(target_os = "macos")] { let emacs_xdg_dir = crate::XDG_DIRS.config_dir().join("emacs").if_exists();
let emacs_xdg_dir = env::var("XDG_CONFIG_HOME") crate::HOME_DIR.join(".emacs.d").if_exists().or(emacs_xdg_dir)
.ok() };
.and_then(|config| PathBuf::from(config).join("emacs").if_exists())
.or_else(|| base_dirs.home_dir().join(".config/emacs").if_exists());
} else {
let emacs_xdg_dir = base_dirs.config_dir().join("emacs").if_exists();
}
}
#[cfg(unix)]
return base_dirs.home_dir().join(".emacs.d").if_exists().or(emacs_xdg_dir);
#[cfg(windows)] #[cfg(windows)]
return env::var("HOME") return env::var("HOME")
@@ -46,11 +40,11 @@ impl Emacs {
.if_exists() .if_exists()
.or_else(|| PathBuf::from(&home).join(".config\\emacs").if_exists()) .or_else(|| PathBuf::from(&home).join(".config\\emacs").if_exists())
}) })
.or_else(|| base_dirs.data_dir().join(".emacs.d").if_exists()); .or_else(|| crate::WINDOWS_DIRS.data_dir().join(".emacs.d").if_exists());
} }
pub fn new(base_dirs: &BaseDirs) -> Self { pub fn new() -> Self {
let directory = Emacs::directory_path(base_dirs); let directory = Emacs::directory_path();
let doom = directory.as_ref().and_then(|d| d.join(DOOM_PATH).if_exists()); let doom = directory.as_ref().and_then(|d| d.join(DOOM_PATH).if_exists());
Self { directory, doom } Self { directory, doom }
} }
@@ -73,7 +67,7 @@ impl Emacs {
command.args(["upgrade"]); command.args(["upgrade"]);
command.check_run() command.status_checked()
} }
pub fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> { pub fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
@@ -81,9 +75,12 @@ impl Emacs {
if let Some(doom) = &self.doom { if let Some(doom) = &self.doom {
Emacs::update_doom(doom, ctx)?; Emacs::update_doom(doom, ctx)?;
} }
let init_file = require_option(self.directory.as_ref(), String::from("Emacs directory does not exist"))? let init_file = require_option(
.join("init.el") self.directory.as_ref(),
.require()?; t!("Emacs directory does not exist").to_string(),
)?
.join("init.el")
.require()?;
print_separator("Emacs"); print_separator("Emacs");
@@ -105,6 +102,6 @@ impl Emacs {
#[cfg(not(unix))] #[cfg(not(unix))]
command.arg(EMACS_UPGRADE); command.arg(EMACS_UPGRADE);
command.check_run() command.status_checked()
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -3,141 +3,176 @@ use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio}; use std::process::{Command, Output, Stdio};
use anyhow::{anyhow, Result}; use color_eyre::eyre::Context;
use color_eyre::eyre::{eyre, Result};
use console::style; use console::style;
use futures::stream::{iter, FuturesUnordered}; use futures::stream::{iter, FuturesUnordered, StreamExt};
use futures::StreamExt;
use glob::{glob_with, MatchOptions}; use glob::{glob_with, MatchOptions};
use log::{debug, error};
use tokio::process::Command as AsyncCommand; use tokio::process::Command as AsyncCommand;
use tokio::runtime; use tokio::runtime;
use tracing::{debug, error};
use crate::command::CommandExt;
use crate::config::Step;
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, RunType}; use crate::steps::emacs::Emacs;
use crate::terminal::print_separator; use crate::terminal::print_separator;
use crate::utils::{which, PathExt}; use crate::utils::{require, PathExt};
use crate::{error::SkipStep, terminal::print_warning}; use crate::{error::SkipStep, terminal::print_warning, HOME_DIR};
use etcetera::base_strategy::BaseStrategy;
use rust_i18n::t;
#[cfg(unix)]
use crate::XDG_DIRS;
#[cfg(windows)]
use crate::WINDOWS_DIRS;
pub fn run_git_pull(ctx: &ExecutionContext) -> Result<()> {
let mut repos = RepoStep::try_new()?;
let config = ctx.config();
// handle built-in repos
if config.use_predefined_git_repos() {
// should be executed on all the platforms
{
if config.should_run(Step::Emacs) {
let emacs = Emacs::new();
if !emacs.is_doom() {
if let Some(directory) = emacs.directory() {
repos.insert_if_repo(directory);
}
}
repos.insert_if_repo(HOME_DIR.join(".doom.d"));
}
if config.should_run(Step::Vim) {
repos.insert_if_repo(HOME_DIR.join(".vim"));
repos.insert_if_repo(HOME_DIR.join(".config/nvim"));
}
repos.insert_if_repo(HOME_DIR.join(".ideavimrc"));
repos.insert_if_repo(HOME_DIR.join(".intellimacs"));
if config.should_run(Step::Rcm) {
repos.insert_if_repo(HOME_DIR.join(".dotfiles"));
}
let powershell = crate::steps::powershell::Powershell::new();
if let Some(profile) = powershell.profile() {
repos.insert_if_repo(profile);
}
}
#[cfg(unix)]
{
repos.insert_if_repo(crate::steps::zsh::zshrc());
if config.should_run(Step::Tmux) {
repos.insert_if_repo(HOME_DIR.join(".tmux"));
}
repos.insert_if_repo(HOME_DIR.join(".config/fish"));
repos.insert_if_repo(XDG_DIRS.config_dir().join("openbox"));
repos.insert_if_repo(XDG_DIRS.config_dir().join("bspwm"));
repos.insert_if_repo(XDG_DIRS.config_dir().join("i3"));
repos.insert_if_repo(XDG_DIRS.config_dir().join("sway"));
}
#[cfg(windows)]
{
repos.insert_if_repo(
WINDOWS_DIRS
.cache_dir()
.join("Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState"),
);
super::os::windows::insert_startup_scripts(&mut repos).ok();
}
}
// Handle user-defined repos
if let Some(custom_git_repos) = config.git_repos() {
for git_repo in custom_git_repos {
repos.glob_insert(git_repo);
}
}
// Warn the user about the bad patterns.
//
// NOTE: this should be executed **before** skipping the Git step or the
// user won't receive this warning in the cases where all the paths configured
// are bad patterns.
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(t!("No repositories to pull").to_string()).into());
}
print_separator(t!("Git repositories"));
repos.pull_repos(ctx)
}
#[cfg(windows)] #[cfg(windows)]
static PATH_PREFIX: &str = "\\\\?\\"; static PATH_PREFIX: &str = "\\\\?\\";
#[derive(Debug)] pub struct RepoStep {
pub struct Git { git: PathBuf,
git: Option<PathBuf>, repos: HashSet<PathBuf>,
}
pub struct Repositories<'a> {
git: &'a Git,
repositories: HashSet<String>,
glob_match_options: MatchOptions, glob_match_options: MatchOptions,
bad_patterns: Vec<String>, bad_patterns: Vec<String>,
} }
fn check_output(output: Output) -> Result<()> { #[track_caller]
fn output_checked_utf8(output: Output) -> Result<()> {
if !(output.status.success()) { if !(output.status.success()) {
let stderr = String::from_utf8(output.stderr).unwrap(); let stderr = String::from_utf8_lossy(&output.stderr);
Err(anyhow!(stderr)) let stderr = stderr.trim();
Err(eyre!("{stderr}"))
} else { } else {
Ok(()) Ok(())
} }
} }
async fn pull_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) -> Result<()> { fn get_head_revision<P: AsRef<Path>>(git: &Path, repo: P) -> Option<String> {
let path = repo.to_string();
let before_revision = get_head_revision(git, &repo);
println!("{} {}", style("Pulling").cyan().bold(), path);
let mut command = AsyncCommand::new(git);
command
.stdin(Stdio::null())
.current_dir(&repo)
.args(["pull", "--ff-only"]);
if let Some(extra_arguments) = ctx.config().git_arguments() {
command.args(extra_arguments.split_whitespace());
}
let pull_output = command.output().await?;
let submodule_output = AsyncCommand::new(git)
.args(["submodule", "update", "--recursive"])
.current_dir(&repo)
.stdin(Stdio::null())
.output()
.await?;
let result = check_output(pull_output).and_then(|_| check_output(submodule_output));
if let Err(message) = &result {
println!("{} pulling {}", style("Failed").red().bold(), &repo);
print!("{}", message);
} else {
let after_revision = get_head_revision(git, &repo);
match (&before_revision, &after_revision) {
(Some(before), Some(after)) if before != after => {
println!("{} {}:", style("Changed").yellow().bold(), &repo);
Command::new(git)
.stdin(Stdio::null())
.current_dir(&repo)
.args([
"--no-pager",
"log",
"--no-decorate",
"--oneline",
&format!("{}..{}", before, after),
])
.spawn()
.unwrap()
.wait()
.unwrap();
println!();
}
_ => {
println!("{} {}", style("Up-to-date").green().bold(), &repo);
}
}
}
result.map(|_| ())
}
fn get_head_revision(git: &Path, repo: &str) -> Option<String> {
Command::new(git) Command::new(git)
.stdin(Stdio::null()) .stdin(Stdio::null())
.current_dir(repo) .current_dir(repo.as_ref())
.args(["rev-parse", "HEAD"]) .args(["rev-parse", "HEAD"])
.check_output() .output_checked_utf8()
.map(|output| output.trim().to_string()) .map(|output| output.stdout.trim().to_string())
.map_err(|e| { .map_err(|e| {
error!("Error getting revision for {}: {}", repo, e); error!("Error getting revision for {}: {e}", repo.as_ref().display(),);
e e
}) })
.ok() .ok()
} }
fn has_remotes(git: &Path, repo: &str) -> Option<bool> { impl RepoStep {
Command::new(git) /// Try to create a `RepoStep`, fail if `git` is not found.
.stdin(Stdio::null()) pub fn try_new() -> Result<Self> {
.current_dir(repo) let git = require("git")?;
.args(["remote", "show"]) let mut glob_match_options = MatchOptions::new();
.check_output()
.map(|output| output.lines().count() > 0)
.map_err(|e| {
error!("Error getting remotes for {}: {}", repo, e);
e
})
.ok()
}
impl Git { if cfg!(windows) {
pub fn new() -> Self { glob_match_options.case_sensitive = false;
Self { git: which("git") } }
Ok(Self {
git,
repos: HashSet::new(),
bad_patterns: Vec::new(),
glob_match_options,
})
} }
pub fn get_repo_root<P: AsRef<Path>>(&self, path: P) -> Option<String> { /// Try to get the root of the repo specified in `path`.
pub fn get_repo_root<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
match path.as_ref().canonicalize() { match path.as_ref().canonicalize() {
Ok(mut path) => { Ok(mut path) => {
debug_assert!(path.exists()); debug_assert!(path.exists());
@@ -161,65 +196,210 @@ impl Git {
path_string path_string
}; };
if let Some(git) = &self.git { let output = Command::new(&self.git)
let output = Command::new(git) .stdin(Stdio::null())
.stdin(Stdio::null()) .current_dir(path)
.current_dir(path) .args(["rev-parse", "--show-toplevel"])
.args(["rev-parse", "--show-toplevel"]) .output_checked_utf8()
.check_output() .ok()
.ok() // trim the last newline char
.map(|output| output.trim().to_string()); .map(|output| PathBuf::from(output.stdout.trim()));
return output;
} return output;
} }
Err(e) => match e.kind() { Err(e) => match e.kind() {
io::ErrorKind::NotFound => debug!("{} does not exists", path.as_ref().display()), io::ErrorKind::NotFound => debug!("{} does not exist", path.as_ref().display()),
_ => error!("Error looking for {}: {}", path.as_ref().display(), e), _ => error!("Error looking for {}: {e}", path.as_ref().display(),),
}, },
} }
None None
} }
pub fn multi_pull_step(&self, repositories: &Repositories, ctx: &ExecutionContext) -> Result<()> {
if repositories.repositories.is_empty() {
return Err(SkipStep(String::from("No repositories to pull")).into());
}
print_separator("Git repositories"); /// Check if `path` is a git repo, if yes, add it to `self.repos`.
repositories ///
.bad_patterns /// Return the check result.
.iter() pub fn insert_if_repo<P: AsRef<Path>>(&mut self, path: P) -> bool {
.for_each(|pattern| print_warning(format!("Path {} did not contain any git repositories", pattern))); if let Some(repo) = self.get_repo_root(path) {
self.multi_pull(repositories, ctx) self.repos.insert(repo);
true
} else {
false
}
} }
pub fn multi_pull(&self, repositories: &Repositories, ctx: &ExecutionContext) -> Result<()> { /// Check if `repo` has a remote.
let git = self.git.as_ref().unwrap(); fn has_remotes<P: AsRef<Path>>(&self, repo: P) -> Option<bool> {
let mut cmd = Command::new(&self.git);
cmd.stdin(Stdio::null())
.current_dir(repo.as_ref())
.args(["remote", "show"]);
if let RunType::Dry = ctx.run_type() { let res = cmd.output_checked_utf8();
repositories
.repositories res.map(|output| output.stdout.lines().count() > 0)
.map_err(|e| {
error!("Error getting remotes for {}: {e}", repo.as_ref().display());
e
})
.ok()
}
/// Similar to `insert_if_repo`, with glob support.
pub fn glob_insert(&mut self, pattern: &str) {
if let Ok(glob) = glob_with(pattern, self.glob_match_options) {
let mut last_git_repo: Option<PathBuf> = None;
for entry in glob {
match entry {
Ok(path) => {
if let Some(last_git_repo) = &last_git_repo {
if path.is_descendant_of(last_git_repo) {
debug!(
"Skipping {} because it's a descendant of last known repo {}",
path.display(),
last_git_repo.display()
);
continue;
}
}
if self.insert_if_repo(&path) {
last_git_repo = Some(path);
}
}
Err(e) => {
error!("Error in path {e}");
}
}
}
if last_git_repo.is_none() {
self.bad_patterns.push(String::from(pattern));
}
} else {
error!("Bad glob pattern: {pattern}");
}
}
/// True if `self.repos` is empty.
pub fn is_repos_empty(&self) -> bool {
self.repos.is_empty()
}
/// Remove `path` from `self.repos`.
///
// `cfg(unix)` because it is only used in the oh-my-zsh step.
#[cfg(unix)]
pub fn remove<P: AsRef<Path>>(&mut self, path: P) {
let _removed = self.repos.remove(path.as_ref());
debug_assert!(_removed);
}
/// Try to pull a repo.
async fn pull_repo<P: AsRef<Path>>(&self, ctx: &ExecutionContext<'_>, repo: P) -> Result<()> {
let before_revision = get_head_revision(&self.git, &repo);
if ctx.config().verbose() {
println!("{} {}", style(t!("Pulling")).cyan().bold(), repo.as_ref().display());
}
let mut command = AsyncCommand::new(&self.git);
command
.stdin(Stdio::null())
.current_dir(&repo)
.args(["pull", "--ff-only"]);
if let Some(extra_arguments) = ctx.config().git_arguments() {
command.args(extra_arguments.split_whitespace());
}
let pull_output = command.output().await?;
let submodule_output = AsyncCommand::new(&self.git)
.args(["submodule", "update", "--recursive"])
.current_dir(&repo)
.stdin(Stdio::null())
.output()
.await?;
let result = output_checked_utf8(pull_output)
.and_then(|_| output_checked_utf8(submodule_output))
.wrap_err_with(|| format!("Failed to pull {}", repo.as_ref().display()));
if result.is_err() {
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(t!("Changed")).yellow().bold(), repo.as_ref().display());
Command::new(&self.git)
.stdin(Stdio::null())
.current_dir(&repo)
.args([
"--no-pager",
"log",
"--no-decorate",
"--oneline",
&format!("{before}..{after}"),
])
.status_checked()?;
println!();
}
_ => {
if ctx.config().verbose() {
println!("{} {}", style(t!("Up-to-date")).green().bold(), repo.as_ref().display());
}
}
}
}
result.map(|_| ())
}
/// Pull the repositories specified in `self.repos`.
///
/// # NOTE
/// This function will create an async runtime and do the real job so the
/// function itself is not async.
fn pull_repos(&self, ctx: &ExecutionContext) -> Result<()> {
if ctx.run_type().dry() {
self.repos
.iter() .iter()
.for_each(|repo| println!("Would pull {}", &repo)); .for_each(|repo| println!("{}", t!("Would pull {repo}", repo = repo.display())));
return Ok(()); return Ok(());
} }
let futures_iterator = repositories if !ctx.config().verbose() {
.repositories println!(
"\n{} {}\n",
style(t!("Only")).green().bold(),
t!("updated repositories will be shown...")
);
}
let futures_iterator = self
.repos
.iter() .iter()
.filter(|repo| match has_remotes(git, repo) { .filter(|repo| match self.has_remotes(repo) {
Some(false) => { Some(false) => {
println!( println!(
"{} {} because it has no remotes", "{} {} {}",
style("Skipping").yellow().bold(), style(t!("Skipping")).yellow().bold(),
repo repo.display(),
t!("because it has no remotes")
); );
false false
} }
_ => true, // repo has remotes or command to check for remotes has failed. proceed to pull anyway. _ => true, // repo has remotes or command to check for remotes has failed. proceed to pull anyway.
}) })
.map(|repo| pull_repository(repo.clone(), git, ctx)); .map(|repo| self.pull_repo(ctx, repo));
let stream_of_futures = if let Some(limit) = ctx.config().git_concurrency_limit() { let stream_of_futures = if let Some(limit) = ctx.config().git_concurrency_limit() {
iter(futures_iterator).buffer_unordered(limit).boxed() iter(futures_iterator).buffer_unordered(limit).boxed()
@@ -234,74 +414,3 @@ impl Git {
error.unwrap_or(Ok(())) error.unwrap_or(Ok(()))
} }
} }
impl<'a> Repositories<'a> {
pub fn new(git: &'a Git) -> Self {
let mut glob_match_options = MatchOptions::new();
if cfg!(windows) {
glob_match_options.case_sensitive = false;
}
Self {
git,
repositories: HashSet::new(),
bad_patterns: Vec::new(),
glob_match_options,
}
}
pub fn insert_if_repo<P: AsRef<Path>>(&mut self, path: P) -> bool {
if let Some(repo) = self.git.get_repo_root(path) {
self.repositories.insert(repo);
true
} else {
false
}
}
pub fn glob_insert(&mut self, pattern: &str) {
if let Ok(glob) = glob_with(pattern, self.glob_match_options) {
let mut last_git_repo: Option<PathBuf> = None;
for entry in glob {
match entry {
Ok(path) => {
if let Some(last_git_repo) = &last_git_repo {
if path.is_descendant_of(last_git_repo) {
debug!(
"Skipping {} because it's a decendant of last known repo {}",
path.display(),
last_git_repo.display()
);
continue;
}
}
if self.insert_if_repo(&path) {
last_git_repo = Some(path);
}
}
Err(e) => {
error!("Error in path {}", e);
}
}
}
if last_git_repo.is_none() {
self.bad_patterns.push(String::from(pattern));
}
} else {
error!("Bad glob pattern: {}", pattern);
}
}
#[cfg(unix)]
pub fn is_empty(&self) -> bool {
self.repositories.is_empty()
}
#[cfg(unix)]
pub fn remove(&mut self, path: &str) {
let _removed = self.repositories.remove(path);
debug_assert!(_removed);
}
}

45
src/steps/go.rs Normal file
View File

@@ -0,0 +1,45 @@
use std::path::PathBuf;
use std::process::Command;
use color_eyre::eyre::Result;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::terminal::print_separator;
use crate::utils;
use crate::utils::PathExt;
/// <https://github.com/Gelio/go-global-update>
pub fn run_go_global_update(ctx: &ExecutionContext) -> Result<()> {
let go_global_update = require_go_bin("go-global-update")?;
print_separator("go-global-update");
ctx.run_type().execute(go_global_update).status_checked()
}
/// <https://github.com/nao1215/gup>
pub fn run_go_gup(ctx: &ExecutionContext) -> Result<()> {
let gup = require_go_bin("gup")?;
print_separator("gup");
ctx.run_type().execute(gup).arg("update").status_checked()
}
/// Get the path of a Go binary.
fn require_go_bin(name: &str) -> Result<PathBuf> {
utils::require(name).or_else(|_| {
let go = utils::require("go")?;
// TODO: Does this work? `go help gopath` says that:
// > The GOPATH environment variable lists places to look for Go code.
// > On Unix, the value is a colon-separated string.
// > On Windows, the value is a semicolon-separated string.
// > On Plan 9, the value is a list.
// Should we also fallback to the env variable?
let gopath_output = Command::new(go).args(["env", "GOPATH"]).output_checked_utf8()?;
let gopath = gopath_output.stdout.trim();
PathBuf::from(gopath).join("bin").join(name).require()
})
}

View File

@@ -1,10 +1,9 @@
use crate::error::TopgradeError;
use crate::terminal::print_separator; use crate::terminal::print_separator;
use crate::utils::require; use crate::utils::require;
use anyhow::Result; use color_eyre::eyre::Result;
use rust_i18n::t;
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::executor::ExecutorOutput;
const UPGRADE_KAK: &str = include_str!("upgrade.kak"); const UPGRADE_KAK: &str = include_str!("upgrade.kak");
@@ -13,19 +12,13 @@ pub fn upgrade_kak_plug(ctx: &ExecutionContext) -> Result<()> {
print_separator("Kakoune"); print_separator("Kakoune");
let mut command = ctx.run_type().execute(kak); // TODO: Why supress output for this command?
command.args(["-ui", "dummy", "-e", UPGRADE_KAK]); ctx.run_type()
.execute(kak)
.args(["-ui", "dummy", "-e", UPGRADE_KAK])
.output()?;
let output = command.output()?; println!("{}", t!("Plugins upgraded"));
if let ExecutorOutput::Wet(output) = output {
let status = output.status;
if !status.success() {
return Err(TopgradeError::ProcessFailed(status).into());
} else {
println!("Plugins upgraded")
}
}
Ok(()) Ok(())
} }

View File

@@ -2,6 +2,7 @@ pub mod containers;
pub mod emacs; pub mod emacs;
pub mod generic; pub mod generic;
pub mod git; pub mod git;
pub mod go;
pub mod kakoune; pub mod kakoune;
pub mod node; pub mod node;
pub mod os; pub mod os;

View File

@@ -1,20 +1,20 @@
#![allow(unused_imports)]
use std::fmt::Display; use std::fmt::Display;
#[cfg(unix)] #[cfg(target_os = "linux")]
use std::os::unix::prelude::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use anyhow::Result; use crate::utils::{get_require_sudo_string, require_option};
use directories::BaseDirs; use crate::HOME_DIR;
use log::debug; use color_eyre::eyre::Result;
#[cfg(unix)] #[cfg(target_os = "linux")]
use nix::unistd::Uid; use nix::unistd::Uid;
use rust_i18n::t;
use semver::Version; use semver::Version;
use tracing::debug;
use crate::executor::{CommandExt, RunType}; use crate::command::CommandExt;
use crate::terminal::print_separator; use crate::terminal::{print_info, print_separator};
use crate::utils::{require, PathExt}; use crate::utils::{require, PathExt};
use crate::{error::SkipStep, execution_context::ExecutionContext}; use crate::{error::SkipStep, execution_context::ExecutionContext};
@@ -24,13 +24,6 @@ enum NPMVariant {
} }
impl NPMVariant { impl NPMVariant {
const fn long_name(&self) -> &str {
match self {
NPMVariant::Npm => "Node Package Manager",
NPMVariant::Pnpm => "PNPM",
}
}
const fn short_name(&self) -> &str { const fn short_name(&self) -> &str {
match self { match self {
NPMVariant::Npm => "npm", NPMVariant::Npm => "npm",
@@ -85,25 +78,29 @@ impl NPM {
let args = ["root", self.global_location_arg()]; let args = ["root", self.global_location_arg()];
Command::new(&self.command) Command::new(&self.command)
.args(args) .args(args)
.check_output() .output_checked_utf8()
.map(|s| PathBuf::from(s.trim())) .map(|s| PathBuf::from(s.stdout.trim()))
} }
fn version(&self) -> Result<Version> { fn version(&self) -> Result<Version> {
let version_str = Command::new(&self.command) let version_str = Command::new(&self.command)
.args(["--version"]) .args(["--version"])
.check_output() .output_checked_utf8()
.map(|s| s.trim().to_owned()); .map(|s| s.stdout.trim().to_owned());
Version::parse(&version_str?).map_err(|err| err.into()) Version::parse(&version_str?).map_err(|err| err.into())
} }
fn upgrade(&self, run_type: RunType, use_sudo: bool) -> Result<()> { fn upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> {
print_separator(self.variant.long_name());
let args = ["update", self.global_location_arg()]; let args = ["update", self.global_location_arg()];
if use_sudo { if use_sudo {
run_type.execute("sudo").args(args).check_run()?; let sudo = require_option(ctx.sudo().clone(), get_require_sudo_string())?;
ctx.run_type()
.execute(sudo)
.arg(&self.command)
.args(args)
.status_checked()?;
} else { } else {
run_type.execute(&self.command).args(args).check_run()?; ctx.run_type().execute(&self.command).args(args).status_checked()?;
} }
Ok(()) Ok(())
@@ -142,9 +139,9 @@ impl Yarn {
// //
// As “yarn dlx” don't need to “upgrade”, we // As “yarn dlx” don't need to “upgrade”, we
// ignore the whole task if Yarn is 2.x or above. // ignore the whole task if Yarn is 2.x or above.
let version = Command::new(&self.command).args(["--version"]).check_output(); let version = Command::new(&self.command).args(["--version"]).output_checked_utf8();
matches!(version, Ok(ver) if ver.starts_with('1') || ver.starts_with('0')) matches!(version, Ok(ver) if ver.stdout.starts_with('1') || ver.stdout.starts_with('0'))
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -152,22 +149,22 @@ impl Yarn {
let args = ["global", "dir"]; let args = ["global", "dir"];
Command::new(&self.command) Command::new(&self.command)
.args(args) .args(args)
.check_output() .output_checked_utf8()
.map(|s| PathBuf::from(s.trim())) .map(|s| PathBuf::from(s.stdout.trim()))
} }
fn upgrade(&self, run_type: RunType, use_sudo: bool) -> Result<()> { fn upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> {
print_separator("Yarn Package Manager");
let args = ["global", "upgrade"]; let args = ["global", "upgrade"];
if use_sudo { if use_sudo {
run_type let sudo = require_option(ctx.sudo().clone(), get_require_sudo_string())?;
.execute("sudo") ctx.run_type()
.execute(sudo)
.arg(self.yarn.as_ref().unwrap_or(&self.command)) .arg(self.yarn.as_ref().unwrap_or(&self.command))
.args(args) .args(args)
.check_run()?; .status_checked()?;
} else { } else {
run_type.execute(&self.command).args(args).check_run()?; ctx.run_type().execute(&self.command).args(args).status_checked()?;
} }
Ok(()) Ok(())
@@ -218,28 +215,32 @@ fn should_use_sudo_yarn(yarn: &Yarn, ctx: &ExecutionContext) -> Result<bool> {
pub fn run_npm_upgrade(ctx: &ExecutionContext) -> Result<()> { pub fn run_npm_upgrade(ctx: &ExecutionContext) -> Result<()> {
let npm = require("npm").map(|b| NPM::new(b, NPMVariant::Npm))?; let npm = require("npm").map(|b| NPM::new(b, NPMVariant::Npm))?;
print_separator(t!("Node Package Manager"));
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
npm.upgrade(ctx.run_type(), should_use_sudo(&npm, ctx)?) npm.upgrade(ctx, should_use_sudo(&npm, ctx)?)
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
{ {
npm.upgrade(ctx.run_type(), false) npm.upgrade(ctx, false)
} }
} }
pub fn run_pnpm_upgrade(ctx: &ExecutionContext) -> Result<()> { pub fn run_pnpm_upgrade(ctx: &ExecutionContext) -> Result<()> {
let pnpm = require("pnpm").map(|b| NPM::new(b, NPMVariant::Pnpm))?; let pnpm = require("pnpm").map(|b| NPM::new(b, NPMVariant::Pnpm))?;
print_separator(t!("Performant Node Package Manager"));
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
pnpm.upgrade(ctx.run_type(), should_use_sudo(&pnpm, ctx)?) pnpm.upgrade(ctx, should_use_sudo(&pnpm, ctx)?)
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
{ {
pnpm.upgrade(ctx.run_type(), false) pnpm.upgrade(ctx, false)
} }
} }
@@ -251,26 +252,73 @@ pub fn run_yarn_upgrade(ctx: &ExecutionContext) -> Result<()> {
return Ok(()); return Ok(());
} }
print_separator(t!("Yarn Package Manager"));
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
yarn.upgrade(ctx.run_type(), should_use_sudo_yarn(&yarn, ctx)?) yarn.upgrade(ctx, should_use_sudo_yarn(&yarn, ctx)?)
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
{ {
yarn.upgrade(ctx.run_type(), false) yarn.upgrade(ctx, false)
} }
} }
pub fn deno_upgrade(ctx: &ExecutionContext) -> Result<()> { pub fn deno_upgrade(ctx: &ExecutionContext) -> Result<()> {
let deno = require("deno")?; let deno = require("deno")?;
let deno_dir = ctx.base_dirs().home_dir().join(".deno"); let deno_dir = HOME_DIR.join(".deno");
if !deno.canonicalize()?.is_descendant_of(&deno_dir) { if !deno.canonicalize()?.is_descendant_of(&deno_dir) {
let skip_reason = SkipStep("Deno installed outside of .deno directory".to_string()); let skip_reason = SkipStep(t!("Deno installed outside of .deno directory").to_string());
return Err(skip_reason.into()); return Err(skip_reason.into());
} }
print_separator("Deno"); print_separator("Deno");
ctx.run_type().execute(&deno).arg("upgrade").check_run() ctx.run_type().execute(&deno).arg("upgrade").status_checked()
}
/// 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.iter() {
ctx.run_type()
.execute(&volta)
.args(["install", package])
.status_checked()?;
}
Ok(())
} }

View File

@@ -1,8 +1,10 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::terminal::print_separator; use crate::terminal::print_separator;
use crate::utils::require; use crate::utils::require;
use crate::utils::which;
use crate::Step; use crate::Step;
use anyhow::Result; use color_eyre::eyre::Result;
pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> { pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
//let pkg = require("pkg")?; //let pkg = require("pkg")?;
@@ -10,7 +12,7 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
print_separator("Termux Packages"); print_separator("Termux Packages");
let is_nala = pkg.end_with("nala"); let is_nala = pkg.ends_with("nala");
let mut command = ctx.run_type().execute(&pkg); let mut command = ctx.run_type().execute(&pkg);
command.arg("upgrade"); command.arg("upgrade");
@@ -18,20 +20,18 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
if ctx.config().yes(Step::System) { if ctx.config().yes(Step::System) {
command.arg("-y"); command.arg("-y");
} }
command.check_run()?; command.status_checked()?;
if !is_nala { if !is_nala && ctx.config().cleanup() {
if ctx.config().cleanup() { ctx.run_type().execute(&pkg).arg("clean").status_checked()?;
ctx.run_type().execute(&pkg).arg("clean").check_run()?;
let apt = require("apt")?; let apt = require("apt")?;
let mut command = ctx.run_type().execute(&apt); let mut command = ctx.run_type().execute(apt);
command.arg("autoremove"); command.arg("autoremove");
if ctx.config().yes(Step::System) { if ctx.config().yes(Step::System) {
command.arg("-y"); command.arg("-y");
}
command.check_run()?;
} }
command.status_checked()?;
} }
Ok(()) Ok(())

View File

@@ -1,13 +1,16 @@
use std::env::var_os; use std::env::var_os;
use std::ffi::OsString; use std::ffi::OsString;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::Result; use color_eyre::eyre;
use color_eyre::eyre::Result;
use rust_i18n::t;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::command::CommandExt;
use crate::error::TopgradeError; use crate::error::TopgradeError;
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::utils::require_option;
use crate::utils::which; use crate::utils::which;
use crate::{config, Step}; use crate::{config, Step};
@@ -29,11 +32,10 @@ pub struct YayParu {
impl ArchPackageManager for YayParu { impl ArchPackageManager for YayParu {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> { fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
if ctx.config().show_arch_news() { if ctx.config().show_arch_news() {
Command::new(&self.executable) ctx.run_type()
.execute(&self.executable)
.arg("-Pw") .arg("-Pw")
.spawn() .status_checked_with_codes(&[1, 0])?;
.and_then(|mut p| p.wait())
.ok();
} }
let mut command = ctx.run_type().execute(&self.executable); let mut command = ctx.run_type().execute(&self.executable);
@@ -48,7 +50,7 @@ impl ArchPackageManager for YayParu {
if ctx.config().yes(Step::System) { if ctx.config().yes(Step::System) {
command.arg("--noconfirm"); command.arg("--noconfirm");
} }
command.check_run()?; command.status_checked()?;
if ctx.config().cleanup() { if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable); let mut command = ctx.run_type().execute(&self.executable);
@@ -56,7 +58,7 @@ impl ArchPackageManager for YayParu {
if ctx.config().yes(Step::System) { if ctx.config().yes(Step::System) {
command.arg("--noconfirm"); command.arg("--noconfirm");
} }
command.check_run()?; command.status_checked()?;
} }
Ok(()) Ok(())
@@ -72,6 +74,37 @@ impl YayParu {
} }
} }
pub struct GarudaUpdate {
executable: PathBuf,
}
impl ArchPackageManager for GarudaUpdate {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let mut command = ctx.run_type().execute(&self.executable);
command
.env("PATH", get_execution_path())
.env("UPDATE_AUR", "1")
.env("SKIP_MIRRORLIST", "1");
if ctx.config().yes(Step::System) {
command.env("PACMAN_NOCONFIRM", "1");
}
command.args(ctx.config().garuda_update_arguments().split_whitespace());
command.status_checked()?;
Ok(())
}
}
impl GarudaUpdate {
fn get() -> Option<Self> {
Some(Self {
executable: which("garuda-update")?,
})
}
}
pub struct Trizen { pub struct Trizen {
executable: PathBuf, executable: PathBuf,
} }
@@ -88,7 +121,7 @@ impl ArchPackageManager for Trizen {
if ctx.config().yes(Step::System) { if ctx.config().yes(Step::System) {
command.arg("--noconfirm"); command.arg("--noconfirm");
} }
command.check_run()?; command.status_checked()?;
if ctx.config().cleanup() { if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable); let mut command = ctx.run_type().execute(&self.executable);
@@ -96,7 +129,7 @@ impl ArchPackageManager for Trizen {
if ctx.config().yes(Step::System) { if ctx.config().yes(Step::System) {
command.arg("--noconfirm"); command.arg("--noconfirm");
} }
command.check_run()?; command.status_checked()?;
} }
Ok(()) Ok(())
@@ -112,13 +145,13 @@ impl Trizen {
} }
pub struct Pacman { pub struct Pacman {
sudo: PathBuf,
executable: PathBuf, executable: PathBuf,
} }
impl ArchPackageManager for Pacman { impl ArchPackageManager for Pacman {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> { 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 command
.arg(&self.executable) .arg(&self.executable)
.arg("-Syu") .arg("-Syu")
@@ -126,15 +159,15 @@ impl ArchPackageManager for Pacman {
if ctx.config().yes(Step::System) { if ctx.config().yes(Step::System) {
command.arg("--noconfirm"); command.arg("--noconfirm");
} }
command.check_run()?; command.status_checked()?;
if ctx.config().cleanup() { 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"); command.arg(&self.executable).arg("-Scc");
if ctx.config().yes(Step::System) { if ctx.config().yes(Step::System) {
command.arg("--noconfirm"); command.arg("--noconfirm");
} }
command.check_run()?; command.status_checked()?;
} }
Ok(()) Ok(())
@@ -142,10 +175,9 @@ impl ArchPackageManager for Pacman {
} }
impl Pacman { impl Pacman {
pub fn get(ctx: &ExecutionContext) -> Option<Self> { pub fn get() -> Option<Self> {
Some(Self { Some(Self {
executable: which("powerpill").unwrap_or_else(|| PathBuf::from("pacman")), executable: which("powerpill").unwrap_or_else(|| PathBuf::from("pacman")),
sudo: ctx.sudo().to_owned()?,
}) })
} }
} }
@@ -175,7 +207,7 @@ impl ArchPackageManager for Pikaur {
command.arg("--noconfirm"); command.arg("--noconfirm");
} }
command.check_run()?; command.status_checked()?;
if ctx.config().cleanup() { if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable); let mut command = ctx.run_type().execute(&self.executable);
@@ -183,7 +215,7 @@ impl ArchPackageManager for Pikaur {
if ctx.config().yes(Step::System) { if ctx.config().yes(Step::System) {
command.arg("--noconfirm"); command.arg("--noconfirm");
} }
command.check_run()?; command.status_checked()?;
} }
Ok(()) Ok(())
@@ -214,7 +246,7 @@ impl ArchPackageManager for Pamac {
command.arg("--no-confirm"); command.arg("--no-confirm");
} }
command.check_run()?; command.status_checked()?;
if ctx.config().cleanup() { if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable); let mut command = ctx.run_type().execute(&self.executable);
@@ -222,7 +254,7 @@ impl ArchPackageManager for Pamac {
if ctx.config().yes(Step::System) { if ctx.config().yes(Step::System) {
command.arg("--no-confirm"); command.arg("--no-confirm");
} }
command.check_run()?; command.status_checked()?;
} }
Ok(()) Ok(())
@@ -231,47 +263,76 @@ impl ArchPackageManager for Pamac {
pub struct Aura { pub struct Aura {
executable: PathBuf, executable: PathBuf,
sudo: PathBuf,
} }
impl Aura { impl Aura {
fn get(ctx: &ExecutionContext) -> Option<Self> { fn get() -> Option<Self> {
Some(Self { Some(Self {
executable: which("aura")?, executable: which("aura")?,
sudo: ctx.sudo().to_owned()?,
}) })
} }
} }
impl ArchPackageManager for Aura { impl ArchPackageManager for Aura {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> { fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let sudo = which("sudo").unwrap_or_else(PathBuf::new); use semver::Version;
let mut aur_update = ctx.run_type().execute(&sudo);
if sudo.ends_with("sudo") { let version_cmd_output = ctx
aur_update .run_type()
.arg(&self.executable) .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") .arg("-Au")
.args(ctx.config().aura_aur_arguments().split_whitespace()); .args(ctx.config().aura_aur_arguments().split_whitespace());
if ctx.config().yes(Step::System) { if ctx.config().yes(Step::System) {
aur_update.arg("--noconfirm"); cmd.arg("--noconfirm");
} }
cmd.status_checked()?;
aur_update.check_run()?; let mut cmd = ctx.run_type().execute(sudo);
} else { cmd.arg(&self.executable)
println!("Aura requires sudo installed to work with AUR packages") .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.check_run()?;
Ok(()) Ok(())
} }
} }
@@ -284,27 +345,29 @@ pub fn get_arch_package_manager(ctx: &ExecutionContext) -> Option<Box<dyn ArchPa
let pacman = which("powerpill").unwrap_or_else(|| PathBuf::from("pacman")); let pacman = which("powerpill").unwrap_or_else(|| PathBuf::from("pacman"));
match ctx.config().arch_package_manager() { match ctx.config().arch_package_manager() {
config::ArchPackageManager::Autodetect => YayParu::get("paru", &pacman) config::ArchPackageManager::Autodetect => GarudaUpdate::get()
.map(box_package_manager) .map(box_package_manager)
.or_else(|| YayParu::get("paru", &pacman).map(box_package_manager))
.or_else(|| YayParu::get("yay", &pacman).map(box_package_manager)) .or_else(|| YayParu::get("yay", &pacman).map(box_package_manager))
.or_else(|| Trizen::get().map(box_package_manager)) .or_else(|| Trizen::get().map(box_package_manager))
.or_else(|| Pikaur::get().map(box_package_manager)) .or_else(|| Pikaur::get().map(box_package_manager))
.or_else(|| Pamac::get().map(box_package_manager)) .or_else(|| Pamac::get().map(box_package_manager))
.or_else(|| Pacman::get(ctx).map(box_package_manager)) .or_else(|| Pacman::get().map(box_package_manager))
.or_else(|| Aura::get(ctx).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::Trizen => Trizen::get().map(box_package_manager),
config::ArchPackageManager::Paru => YayParu::get("paru", &pacman).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::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::Pikaur => Pikaur::get().map(box_package_manager),
config::ArchPackageManager::Pamac => Pamac::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),
} }
} }
pub fn upgrade_arch_linux(ctx: &ExecutionContext) -> Result<()> { pub fn upgrade_arch_linux(ctx: &ExecutionContext) -> Result<()> {
let package_manager = let package_manager =
get_arch_package_manager(ctx).ok_or_else(|| anyhow::Error::from(TopgradeError::FailedGettingPackageManager))?; get_arch_package_manager(ctx).ok_or_else(|| eyre::Report::from(TopgradeError::FailedGettingPackageManager))?;
package_manager.upgrade(ctx) package_manager.upgrade(ctx)
} }
@@ -321,7 +384,7 @@ pub fn show_pacnew() {
.peekable(); .peekable();
if iter.peek().is_some() { if iter.peek().is_some() {
println!("\nPacman backup configuration files found:"); println!("\n{}", t!("Pacman backup configuration files found:"));
for entry in iter { for entry in iter {
println!("{}", entry.path().display()); println!("{}", entry.path().display());

View File

@@ -1,26 +1,36 @@
use crate::executor::RunType; use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::terminal::print_separator; use crate::terminal::print_separator;
use crate::utils::require_option; use crate::utils::{get_require_sudo_string, require_option};
use anyhow::Result; use crate::Step;
use std::path::PathBuf; use color_eyre::eyre::Result;
use std::process::Command; use std::process::Command;
pub fn upgrade_packages(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(sudo, String::from("No sudo detected"))?; let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator("DrgaonFly BSD Packages"); print_separator(t!("DragonFly BSD Packages"));
run_type let mut cmd = ctx.run_type().execute(sudo);
.execute(sudo) cmd.args(["/usr/local/sbin/pkg", "upgrade"]);
.args(&["/usr/local/sbin/pkg", "upgrade"]) if ctx.config().yes(Step::System) {
.check_run() cmd.arg("-y");
}
cmd.status_checked()
} }
pub fn audit_packages(sudo: &Option<PathBuf>) -> Result<()> { pub fn audit_packages(ctx: &ExecutionContext) -> Result<()> {
if let Some(sudo) = sudo { let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
println!();
Command::new(sudo) print_separator(t!("DragonFly BSD Audit"));
.args(&["/usr/local/sbin/pkg", "audit", "-Fr"])
.spawn()? #[allow(clippy::disallowed_methods)]
.wait()?; if !Command::new(sudo)
.args(["/usr/local/sbin/pkg", "audit", "-Fr"])
.status()?
.success()
{
println!(t!(
"The package audit was successful, but vulnerable packages still remain on the system"
));
} }
Ok(()) Ok(())
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +1,59 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, RunType};
use crate::terminal::{print_separator, prompt_yesno}; use crate::terminal::{print_separator, prompt_yesno};
use crate::{error::TopgradeError, utils::require, Step}; use crate::utils::{get_require_sudo_string, require_option};
use anyhow::Result; use crate::{utils::require, Step};
use log::debug; use color_eyre::eyre::Result;
use rust_i18n::t;
use std::collections::HashSet;
use std::fs; use std::fs;
use std::process::Command; use std::process::Command;
use tracing::debug;
pub fn run_macports(ctx: &ExecutionContext) -> Result<()> { pub fn run_macports(ctx: &ExecutionContext) -> Result<()> {
require("port")?; require("port")?;
let sudo = ctx.sudo().as_ref().unwrap(); let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator("MacPorts"); print_separator("MacPorts");
ctx.run_type().execute(sudo).args(["port", "selfupdate"]).check_run()?; ctx.run_type()
.execute(sudo)
.args(["port", "selfupdate"])
.status_checked()?;
ctx.run_type() ctx.run_type()
.execute(sudo) .execute(sudo)
.args(["port", "-u", "upgrade", "outdated"]) .args(["port", "-u", "upgrade", "outdated"])
.check_run()?; .status_checked()?;
if ctx.config().cleanup() { if ctx.config().cleanup() {
ctx.run_type() ctx.run_type()
.execute(sudo) .execute(sudo)
.args(["port", "-N", "reclaim"]) .args(["port", "-N", "reclaim"])
.check_run()?; .status_checked()?;
} }
Ok(()) Ok(())
} }
pub fn run_mas(run_type: RunType) -> Result<()> { pub fn run_mas(ctx: &ExecutionContext) -> Result<()> {
let mas = require("mas")?; let mas = require("mas")?;
print_separator("macOS App Store"); print_separator(t!("macOS App Store"));
run_type.execute(mas).arg("upgrade").check_run() ctx.run_type().execute(mas).arg("upgrade").status_checked()
} }
pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> { 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()); let should_ask = !(ctx.config().yes(Step::System) || ctx.config().dry_run());
if should_ask { if should_ask {
println!("Finding available software"); println!("{}", t!("Finding available software"));
if system_update_available()? { 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 { if !answer {
return Ok(()); return Ok(());
} }
println!(); println!();
} else { } else {
println!("No new software available."); println!("{}", t!("No new software available."));
return Ok(()); return Ok(());
} }
} }
@@ -58,20 +65,15 @@ pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> {
command.arg("--no-scan"); command.arg("--no-scan");
} }
command.check_run() command.status_checked()
} }
fn system_update_available() -> Result<bool> { fn system_update_available() -> Result<bool> {
let output = Command::new("softwareupdate").arg("--list").output()?; let output = Command::new("softwareupdate").arg("--list").output_checked_utf8()?;
debug!("{:?}", output); debug!("{:?}", output);
let status = output.status; Ok(!output.stderr.contains("No new software available"))
if !status.success() {
return Err(TopgradeError::ProcessFailed(status).into());
}
let string_output = String::from_utf8(output.stderr)?;
debug!("{:?}", string_output);
Ok(!string_output.contains("No new software available"))
} }
pub fn run_sparkle(ctx: &ExecutionContext) -> Result<()> { pub fn run_sparkle(ctx: &ExecutionContext) -> Result<()> {
@@ -83,13 +85,160 @@ pub fn run_sparkle(ctx: &ExecutionContext) -> Result<()> {
let probe = Command::new(&sparkle) let probe = Command::new(&sparkle)
.args(["--probe", "--application"]) .args(["--probe", "--application"])
.arg(application.path()) .arg(application.path())
.check_output(); .output_checked_utf8();
if probe.is_ok() { if probe.is_ok() {
let mut command = ctx.run_type().execute(&sparkle); let mut command = ctx.run_type().execute(&sparkle);
command.args(["bundle", "--check-immediately", "--application"]); command.args(["bundle", "--check-immediately", "--application"]);
command.arg(application.path()); command.arg(application.path());
command.spawn()?.wait()?; command.status_checked()?;
} }
} }
Ok(()) Ok(())
} }
pub fn update_xcodes(ctx: &ExecutionContext) -> Result<()> {
let xcodes = require("xcodes")?;
print_separator("Xcodes");
let should_ask = !(ctx.config().yes(Step::Xcodes) || ctx.config().dry_run());
let releases = ctx
.run_type()
.execute(&xcodes)
.args(["update"])
.output_checked_utf8()?
.stdout;
let releases_installed: Vec<String> = releases
.lines()
.filter(|r| r.contains("(Installed)"))
.map(String::from)
.collect();
if releases_installed.is_empty() {
println!("{}", t!("No Xcode releases installed."));
return Ok(());
}
let (installed_gm, installed_beta, installed_regular) =
releases_installed
.iter()
.fold((false, false, false), |(gm, beta, regular), release| {
(
gm || release.contains("GM") || release.contains("Release Candidate"),
beta || release.contains("Beta"),
regular
|| !(release.contains("GM")
|| release.contains("Release Candidate")
|| release.contains("Beta")),
)
});
let releases_gm = releases
.lines()
.filter(|&r| r.matches("GM").count() > 0 || r.matches("Release Candidate").count() > 0)
.map(String::from)
.collect();
let releases_beta = releases
.lines()
.filter(|&r| r.matches("Beta").count() > 0)
.map(String::from)
.collect();
let releases_regular = releases
.lines()
.filter(|&r| {
r.matches("GM").count() == 0
&& r.matches("Release Candidate").count() == 0
&& r.matches("Beta").count() == 0
})
.map(String::from)
.collect();
if installed_gm {
process_xcodes_releases(releases_gm, should_ask, ctx)?;
}
if installed_beta {
process_xcodes_releases(releases_beta, should_ask, ctx)?;
}
if installed_regular {
process_xcodes_releases(releases_regular, should_ask, ctx)?;
}
let releases_new = ctx
.run_type()
.execute(&xcodes)
.args(["list"])
.output_checked_utf8()?
.stdout;
let releases_gm_new_installed: HashSet<_> = releases_new
.lines()
.filter(|release| {
release.contains("(Installed)") && (release.contains("GM") || release.contains("Release Candidate"))
})
.collect();
let releases_beta_new_installed: HashSet<_> = releases_new
.lines()
.filter(|release| release.contains("(Installed)") && release.contains("Beta"))
.collect();
let releases_regular_new_installed: HashSet<_> = releases_new
.lines()
.filter(|release| {
release.contains("(Installed)")
&& !(release.contains("GM") || release.contains("Release Candidate") || release.contains("Beta"))
})
.collect();
for releases_new_installed in [
releases_gm_new_installed,
releases_beta_new_installed,
releases_regular_new_installed,
] {
if should_ask && releases_new_installed.len() == 2 {
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(),
])
.status_checked();
}
}
}
Ok(())
}
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()
{
println!(
"{} {}",
t!("New Xcode release detected:"),
releases_filtered.last().cloned().unwrap_or_default()
);
if should_ask {
let answer_install = prompt_yesno(t!("Would you like to install it?").as_ref())?;
if answer_install {
let _ = ctx
.run_type()
.execute(xcodes)
.args(["install", &releases_filtered.last().cloned().unwrap_or_default()])
.status_checked();
}
println!();
}
}
Ok(())
}

View File

@@ -1,17 +1,33 @@
use crate::executor::RunType; use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::terminal::print_separator; use crate::terminal::print_separator;
use crate::utils::require_option; use crate::utils::{get_require_sudo_string, require_option};
use anyhow::Result; use color_eyre::eyre::Result;
use std::path::PathBuf;
pub fn upgrade_openbsd(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { pub fn upgrade_openbsd(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(sudo, String::from("No sudo detected"))?; let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator("OpenBSD Update"); print_separator(t!("OpenBSD Update"));
run_type.execute(sudo).args(&["/usr/sbin/sysupgrade", "-n"]).check_run() ctx.run_type()
.execute(sudo)
.args(["/usr/sbin/sysupgrade", "-n"])
.status_checked()
} }
pub fn upgrade_packages(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(sudo, String::from("No sudo detected"))?; let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
print_separator("OpenBSD Packages"); print_separator(t!("OpenBSD Packages"));
run_type.execute(sudo).args(&["/usr/sbin/pkg_add", "-u"]).check_run()
if ctx.config().cleanup() {
ctx.run_type()
.execute(sudo)
.args(["/usr/sbin/pkg_delete", "-ac"])
.status_checked()?;
}
ctx.run_type()
.execute(sudo)
.args(["/usr/sbin/pkg_add", "-u"])
.status_checked()?;
Ok(())
} }

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
NAME="Fedora Linux"
VERSION="39.20240415.0 (IoT Edition)"
ID=fedora
VERSION_ID=39
VERSION_CODENAME=""
PLATFORM_ID="platform:f39"
PRETTY_NAME="Fedora Linux 39.20240415.0 (IoT Edition)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:39"
HOME_URL="https://fedoraproject.org/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f39/system-administrators-guide/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://bugzilla.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=39
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=39
SUPPORT_END=2024-11-12
VARIANT="IoT Edition"
VARIANT_ID=iot
OSTREE_VERSION='39.20240415.0'

View File

@@ -0,0 +1,23 @@
NAME="Fedora Linux"
VERSION="39.20240105.0 (Kinoite)"
ID=fedora
VERSION_ID=39
VERSION_CODENAME=""
PLATFORM_ID="platform:f39"
PRETTY_NAME="Fedora Linux 39.20240105.0 (Kinoite)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:39"
DEFAULT_HOSTNAME="fedora"
HOME_URL="https://kinoite.fedoraproject.org"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora-kinoite/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://pagure.io/fedora-kde/SIG/issues"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=39
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=39
SUPPORT_END=2024-11-12
VARIANT="Kinoite"
VARIANT_ID=kinoite
OSTREE_VERSION='39.20240105.0'

View File

@@ -0,0 +1,22 @@
NAME="Fedora Linux"
VERSION="39 (Onyx)"
ID=fedora
VERSION_ID=39
VERSION_CODENAME=""
PLATFORM_ID="platform:f39"
PRETTY_NAME="Fedora Linux 39 (Onyx)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:39"
DEFAULT_HOSTNAME="fedora"
HOME_URL="https://fedoraproject.org/onyx/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora-onyx/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://bugzilla.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=39
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=39
SUPPORT_END=2024-05-14
VARIANT="Onyx"
VARIANT_ID=onyx

View File

@@ -0,0 +1,22 @@
NAME="Fedora Linux"
VERSION="39 (Sericea)"
ID=fedora
VERSION_ID=39
VERSION_CODENAME=""
PLATFORM_ID="platform:f39"
PRETTY_NAME="Fedora Linux 39 (Sericea)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:39"
DEFAULT_HOSTNAME="fedora"
HOME_URL="https://fedoraproject.org/sericea/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora-sericea/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://gitlab.com/fedora/sigs/sway/SIG/-/issues"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=39
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=39
SUPPORT_END=2024-05-14
VARIANT="Sericea"
VARIANT_ID=sericea

View File

@@ -0,0 +1,22 @@
NAME="Fedora Linux"
VERSION="39 (Silverblue)"
ID=fedora
VERSION_ID=39
VERSION_CODENAME=""
PLATFORM_ID="platform:f39"
PRETTY_NAME="Fedora Linux 39 (Silverblue)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:39"
DEFAULT_HOSTNAME="fedora"
HOME_URL="https://silverblue.fedoraproject.org"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora-silverblue/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://github.com/fedora-silverblue/issue-tracker/issues"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=39
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=39
SUPPORT_END=2024-05-14
VARIANT="Silverblue"
VARIANT_ID=silverblue

View File

@@ -0,0 +1,23 @@
NAME="Fedora Linux"
VERSION="40.20240426.0 (Sway Atomic)"
ID=fedora
VERSION_ID=40
VERSION_CODENAME=""
PLATFORM_ID="platform:f40"
PRETTY_NAME="Fedora Linux 40.20240426.0 (Sway Atomic)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:40"
DEFAULT_HOSTNAME="fedora"
HOME_URL="https://fedoraproject.org/atomic-desktops/sway/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora-sericea/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://gitlab.com/fedora/sigs/sway/SIG/-/issues"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=40
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=40
SUPPORT_END=2025-05-13
VARIANT="Sway Atomic"
VARIANT_ID=sway-atomic
OSTREE_VERSION='40.20240426.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

@@ -0,0 +1,23 @@
NAME="Nobara Linux"
VERSION="39 (GNOME Edition)"
ID=nobara
ID_LIKE="rhel centos fedora"
VERSION_ID=39
VERSION_CODENAME=""
PLATFORM_ID="platform:f39"
PRETTY_NAME="Nobara Linux 39 (GNOME Edition)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=nobara-logo-icon
CPE_NAME="cpe:/o:nobaraproject:nobara:39"
DEFAULT_HOSTNAME="nobara"
HOME_URL="https://nobaraproject.org/"
DOCUMENTATION_URL="https://www.nobaraproject.org/"
SUPPORT_URL="https://www.nobaraproject.org/"
BUG_REPORT_URL="https://gitlab.com/gloriouseggroll/nobara-images"
REDHAT_BUGZILLA_PRODUCT="Nobara"
REDHAT_BUGZILLA_PRODUCT_VERSION=39
REDHAT_SUPPORT_PRODUCT="Nobara"
REDHAT_SUPPORT_PRODUCT_VERSION=39
SUPPORT_END=2024-05-14
VARIANT="GNOME Edition"
VARIANT_ID=gnome

View File

@@ -0,0 +1,9 @@
NAME="PCLinuxOS"
VERSION="2022"
ID=pclinuxos
VERSION_ID=2022
ID_LIKE="mandriva"
PRETTY_NAME="PCLinuxOS 2022"
ANSI_COLOR="1;37"
HOME_URL="http://www.pclinuxos.com/"
SUPPORT_URL="http://www.pclinuxos.com/"

View File

@@ -0,0 +1,11 @@
NAME="Solus"
VERSION="4.4"
ID="solus"
VERSION_CODENAME=harmony
VERSION_ID="4.4"
PRETTY_NAME="Solus 4.4 Harmony"
ANSI_COLOR="1;34"
HOME_URL="https://getsol.us"
SUPPORT_URL="https://help.getsol.us/docs/user/contributing/getting-involved"
BUG_REPORT_URL="https://dev.getsol.us/"
LOGO="distributor-logo-solus"

View File

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

View File

@@ -0,0 +1,5 @@
ID=wolfi
NAME="Wolfi"
PRETTY_NAME="Wolfi"
VERSION_ID="20230201"
HOME_URL="https://wolfi.dev"

View File

@@ -1,33 +1,51 @@
use crate::error::{SkipStep, TopgradeError}; use std::ffi::OsStr;
use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, Executor, ExecutorExitStatus, RunType};
use crate::terminal::print_separator;
#[cfg(not(target_os = "macos"))]
use crate::utils::require_option;
use crate::utils::{require, PathExt};
use crate::Step;
use anyhow::Result;
use directories::BaseDirs;
use home;
use ini::Ini;
use log::debug;
use std::fs; use std::fs;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::path::Component;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use std::{env, path::Path}; use std::{env::var, path::Path};
use crate::command::CommandExt;
use crate::{Step, HOME_DIR};
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use home;
use ini::Ini;
#[cfg(target_os = "linux")]
use nix::unistd::Uid;
use rust_i18n::t;
use semver::Version;
use tracing::debug;
#[cfg(target_os = "linux")]
use super::linux::Distribution;
use crate::error::SkipStep;
use crate::execution_context::ExecutionContext;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::executor::Executor;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::executor::RunType;
use crate::terminal::print_separator;
use crate::utils::{get_require_sudo_string, require, require_option, PathExt};
#[cfg(any(target_os = "linux", target_os = "macos"))]
const INTEL_BREW: &str = "/usr/local/bin/brew"; const INTEL_BREW: &str = "/usr/local/bin/brew";
#[cfg(any(target_os = "linux", target_os = "macos"))]
const ARM_BREW: &str = "/opt/homebrew/bin/brew"; const ARM_BREW: &str = "/opt/homebrew/bin/brew";
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
#[allow(dead_code)] #[allow(dead_code)]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub enum BrewVariant { pub enum BrewVariant {
Path, Path,
MacIntel, MacIntel,
MacArm, MacArm,
} }
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl BrewVariant { impl BrewVariant {
fn binary_name(self) -> &'static str { fn binary_name(self) -> &'static str {
match self { match self {
@@ -77,84 +95,123 @@ impl BrewVariant {
} }
} }
pub fn run_fisher(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { pub fn run_fisher(ctx: &ExecutionContext) -> Result<()> {
let fish = require("fish")?; let fish = require("fish")?;
if env::var("fisher_path").is_err() { Command::new(&fish)
base_dirs .args(["-c", "type -t fisher"])
.home_dir() .output_checked_utf8()
.join(".config/fish/functions/fisher.fish") .map(|_| ())
.require()?; .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(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(t!("`fish_update_completions` is not available").to_string()))?;
print_separator("Fisher"); print_separator("Fisher");
let version_str = run_type let version_str = ctx
.run_type()
.execute(&fish) .execute(&fish)
.args(["-c", "fisher --version"]) .args(["-c", "fisher --version"])
.check_output()?; .output_checked_utf8()?
.stdout;
debug!("Fisher version: {}", version_str); debug!("Fisher version: {}", version_str);
if version_str.starts_with("fisher version 3.") { if version_str.starts_with("fisher version 3.") {
// v3 - see https://github.com/topgrade-rs/topgrade/pull/37#issuecomment-1283844506 // v3 - see https://github.com/topgrade-rs/topgrade/pull/37#issuecomment-1283844506
run_type.execute(&fish).args(["-c", "fisher"]).check_run() ctx.run_type().execute(&fish).args(["-c", "fisher"]).status_checked()
} else { } else {
// v4 // v4
run_type.execute(&fish).args(["-c", "fisher update"]).check_run() ctx.run_type()
.execute(&fish)
.args(["-c", "fisher update"])
.status_checked()
} }
} }
pub fn run_bashit(ctx: &ExecutionContext) -> Result<()> { pub fn run_bashit(ctx: &ExecutionContext) -> Result<()> {
ctx.base_dirs().home_dir().join(".bash_it").require()?; HOME_DIR.join(".bash_it").require()?;
print_separator("Bash-it"); print_separator("Bash-it");
ctx.run_type() ctx.run_type()
.execute("bash") .execute("bash")
.args(["-lic", &format!("bash-it update {}", ctx.config().bashit_branch())]) .args(["-lic", &format!("bash-it update {}", ctx.config().bashit_branch())])
.check_run() .status_checked()
}
pub fn run_oh_my_bash(ctx: &ExecutionContext) -> Result<()> {
require("bash")?;
let oh_my_bash = var("OSH")
// default to `~/.oh-my-bash`
.unwrap_or(
HOME_DIR
.join(".oh-my-bash")
.to_str()
.expect("should be UTF-8 encoded")
.to_string(),
)
.require()?;
print_separator("oh-my-bash");
let mut update_script = oh_my_bash;
update_script.push_str("/tools/upgrade.sh");
ctx.run_type().execute("bash").arg(update_script).status_checked()
} }
pub fn run_oh_my_fish(ctx: &ExecutionContext) -> Result<()> { pub fn run_oh_my_fish(ctx: &ExecutionContext) -> Result<()> {
let fish = require("fish")?; let fish = require("fish")?;
ctx.base_dirs() HOME_DIR.join(".local/share/omf/pkg/omf/functions/omf.fish").require()?;
.home_dir()
.join(".local/share/omf/pkg/omf/functions/omf.fish")
.require()?;
print_separator("oh-my-fish"); print_separator("oh-my-fish");
ctx.run_type().execute(fish).args(["-c", "omf update"]).check_run() ctx.run_type().execute(fish).args(["-c", "omf update"]).status_checked()
} }
pub fn run_pkgin(ctx: &ExecutionContext) -> Result<()> { pub fn run_pkgin(ctx: &ExecutionContext) -> Result<()> {
let pkgin = require("pkgin")?; let pkgin = require("pkgin")?;
let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?;
let mut command = ctx.run_type().execute(ctx.sudo().as_ref().unwrap()); print_separator("Pkgin");
let mut command = ctx.run_type().execute(sudo);
command.arg(&pkgin).arg("update"); command.arg(&pkgin).arg("update");
if ctx.config().yes(Step::Pkgin) { if ctx.config().yes(Step::Pkgin) {
command.arg("-y"); command.arg("-y");
} }
command.check_run()?; command.status_checked()?;
let mut command = ctx.run_type().execute(ctx.sudo().as_ref().unwrap()); let mut command = ctx.run_type().execute(sudo);
command.arg(&pkgin).arg("upgrade"); command.arg(&pkgin).arg("upgrade");
if ctx.config().yes(Step::Pkgin) { if ctx.config().yes(Step::Pkgin) {
command.arg("-y"); command.arg("-y");
} }
command.check_run() command.status_checked()
} }
pub fn run_fish_plug(ctx: &ExecutionContext) -> Result<()> { pub fn run_fish_plug(ctx: &ExecutionContext) -> Result<()> {
let fish = require("fish")?; let fish = require("fish")?;
ctx.base_dirs() HOME_DIR
.home_dir()
.join(".local/share/fish/plug/kidonng/fish-plug/functions/plug.fish") .join(".local/share/fish/plug/kidonng/fish-plug/functions/plug.fish")
.require()?; .require()?;
print_separator("fish-plug"); print_separator("fish-plug");
ctx.run_type().execute(fish).args(["-c", "plug update"]).check_run() ctx.run_type()
.execute(fish)
.args(["-c", "plug update"])
.status_checked()
} }
/// Upgrades `fundle` and `fundle` plugins. /// Upgrades `fundle` and `fundle` plugins.
@@ -164,22 +221,22 @@ pub fn run_fish_plug(ctx: &ExecutionContext) -> Result<()> {
/// See: <https://github.com/danhper/fundle> /// See: <https://github.com/danhper/fundle>
pub fn run_fundle(ctx: &ExecutionContext) -> Result<()> { pub fn run_fundle(ctx: &ExecutionContext) -> Result<()> {
let fish = require("fish")?; let fish = require("fish")?;
ctx.base_dirs().home_dir().join(".config/fish/fundle").require()?; HOME_DIR.join(".config/fish/fundle").require()?;
print_separator("fundle"); print_separator("fundle");
ctx.run_type() ctx.run_type()
.execute(fish) .execute(fish)
.args(["-c", "fundle self-update && fundle update"]) .args(["-c", "fundle self-update && fundle update"])
.check_run() .status_checked()
} }
#[cfg(not(any(target_os = "android", target_os = "macos")))] #[cfg(not(any(target_os = "android", target_os = "macos")))]
pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> { pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
let gdbus = require("gdbus")?; let gdbus = require("gdbus")?;
require_option( require_option(
env::var("XDG_CURRENT_DESKTOP").ok().filter(|p| p.contains("GNOME")), 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") let output = Command::new("gdbus")
.args([ .args([
@@ -192,14 +249,14 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
"--method", "--method",
"org.freedesktop.DBus.ListActivatableNames", "org.freedesktop.DBus.ListActivatableNames",
]) ])
.check_output()?; .output_checked_utf8()?;
debug!("Checking for gnome extensions: {}", output); debug!("Checking for gnome extensions: {}", output);
if !output.contains("org.gnome.Shell.Extensions") { 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() ctx.run_type()
.execute(gdbus) .execute(gdbus)
@@ -213,9 +270,27 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
"--method", "--method",
"org.gnome.Shell.Extensions.CheckForUpdates", "org.gnome.Shell.Extensions.CheckForUpdates",
]) ])
.check_run() .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<()> { pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()> {
#[allow(unused_variables)] #[allow(unused_variables)]
let binary_name = require(variant.binary_name())?; let binary_name = require(variant.binary_name())?;
@@ -223,25 +298,57 @@ pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
if variant.is_path() && !BrewVariant::is_macos_custom(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());
} }
} }
#[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()); print_separator(variant.step_title());
let run_type = ctx.run_type(); let run_type = ctx.run_type();
variant.execute(run_type).arg("update").check_run()?; variant.execute(run_type).arg("update").status_checked()?;
variant
.execute(run_type) let mut command = variant.execute(run_type);
.args(["upgrade", "--ignore-pinned", "--formula"]) command.args(["upgrade", "--formula"]);
.check_run()?;
if ctx.config().brew_fetch_head() {
command.arg("--fetch-HEAD");
}
command.status_checked()?;
if ctx.config().cleanup() { if ctx.config().cleanup() {
variant.execute(run_type).arg("cleanup").check_run()?; variant.execute(run_type).arg("cleanup").status_checked()?;
} }
if ctx.config().brew_autoremove() { if ctx.config().brew_autoremove() {
variant.execute(run_type).arg("autoremove").check_run()?; variant.execute(run_type).arg("autoremove").status_checked()?;
} }
Ok(()) Ok(())
@@ -251,7 +358,7 @@ pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<
pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()> { pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()> {
let binary_name = require(variant.binary_name())?; let binary_name = require(variant.binary_name())?;
if variant.is_path() && !BrewVariant::is_macos_custom(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())); print_separator(format!("{} - Cask", variant.step_title()));
let run_type = ctx.run_type(); let run_type = ctx.run_type();
@@ -259,8 +366,8 @@ pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()>
let cask_upgrade_exists = variant let cask_upgrade_exists = variant
.execute(RunType::Wet) .execute(RunType::Wet)
.args(["--repository", "buo/cask-upgrade"]) .args(["--repository", "buo/cask-upgrade"])
.check_output() .output_checked_utf8()
.map(|p| Path::new(p.trim()).exists())?; .map(|p| Path::new(p.stdout.trim()).exists())?;
let mut brew_args = vec![]; let mut brew_args = vec![];
@@ -274,12 +381,18 @@ pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()>
if ctx.config().brew_cask_greedy() { if ctx.config().brew_cask_greedy() {
brew_args.push("--greedy"); brew_args.push("--greedy");
} }
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).check_run()?; variant.execute(run_type).args(&brew_args).status_checked()?;
if ctx.config().cleanup() { if ctx.config().cleanup() {
variant.execute(run_type).arg("cleanup").check_run()?; variant.execute(run_type).arg("cleanup").status_checked()?;
} }
Ok(()) Ok(())
@@ -290,7 +403,7 @@ pub fn run_guix(ctx: &ExecutionContext) -> Result<()> {
let run_type = ctx.run_type(); let run_type = ctx.run_type();
let output = Command::new(&guix).arg("pull").check_output(); let output = Command::new(&guix).arg("pull").output_checked_utf8();
debug!("guix pull output: {:?}", output); debug!("guix pull output: {:?}", output);
let should_upgrade = output.is_ok(); let should_upgrade = output.is_ok();
debug!("Can Upgrade Guix: {:?}", should_upgrade); debug!("Can Upgrade Guix: {:?}", should_upgrade);
@@ -298,15 +411,16 @@ pub fn run_guix(ctx: &ExecutionContext) -> Result<()> {
print_separator("Guix"); print_separator("Guix");
if should_upgrade { if should_upgrade {
return run_type.execute(&guix).args(["package", "-u"]).check_run(); 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<()> { pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
let nix = require("nix")?; let nix = require("nix")?;
let nix_channel = require("nix-channel")?; let nix_channel = require("nix-channel")?;
let nix_env = require("nix-env")?; let nix_env = require("nix-env")?;
// TODO: Is None possible here?
let profile_path = match home::home_dir() { let profile_path = match home::home_dir() {
Some(home) => Path::new(&home).join(".nix-profile"), Some(home) => Path::new(&home).join(".nix-profile"),
None => Path::new("/nix/var/nix/profiles/per-user/default").into(), None => Path::new("/nix/var/nix/profiles/per-user/default").into(),
@@ -314,56 +428,173 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
debug!("nix profile: {:?}", profile_path); debug!("nix profile: {:?}", profile_path);
let manifest_json_path = profile_path.join("manifest.json"); let manifest_json_path = profile_path.join("manifest.json");
let output = Command::new(&nix_env).args(["--query", "nix"]).check_output();
debug!("nix-env output: {:?}", output);
let should_self_upgrade = output.is_ok();
print_separator("Nix"); print_separator("Nix");
let multi_user = fs::metadata(&nix)?.uid() == 0;
debug!("Multi user nix: {}", multi_user);
#[cfg(target_os = "linux")]
{
use super::linux::Distribution;
if let Ok(Distribution::NixOS) = Distribution::detect() {
return Err(SkipStep(String::from("Nix on NixOS must be upgraded via nixos-rebuild switch")).into());
}
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
if let Ok(..) = require("darwin-rebuild") { if require("darwin-rebuild").is_ok() {
return Err(SkipStep(String::from( return Err(
"Nix-darwin on macOS must be upgraded via darwin-rebuild switch", SkipStep(t!("Nix-darwin on macOS must be upgraded via darwin-rebuild switch").to_string()).into(),
)) );
.into());
} }
} }
let run_type = ctx.run_type(); let run_type = ctx.run_type();
run_type.execute(nix_channel).arg("--update").status_checked()?;
if should_self_upgrade { let mut get_version_cmd = ctx.run_type().execute(&nix);
if multi_user { get_version_cmd.arg("--version");
ctx.execute_elevated(&nix, true)?.arg("upgrade-nix").check_run()?; let get_version_cmd_output = get_version_cmd.output_checked_utf8()?;
} else { let get_version_cmd_first_line_stdout = get_version_cmd_output
run_type.execute(&nix).arg("upgrade-nix").check_run()?; .stdout
.lines()
.next()
.expect("nix --version gives an empty output");
let splitted: Vec<&str> = get_version_cmd_first_line_stdout.split_whitespace().collect();
let version = if splitted.len() >= 3 {
Version::parse(splitted[2]).expect("invalid version")
} else {
panic!("nix --version output format changed, file an issue to Topgrade!")
};
debug!("Nix version: {:?}", version);
// Nix since 2.21.0 uses `--all --impure` rather than `.*` to upgrade all packages
let packages = if version >= Version::new(2, 21, 0) {
vec!["--all", "--impure"]
} else {
vec![".*"]
};
if Path::new(&manifest_json_path).exists() {
run_type
.execute(nix)
.args(nix_args())
.arg("profile")
.arg("upgrade")
.args(&packages)
.arg("--verbose")
.status_checked()
} else {
let mut command = run_type.execute(nix_env);
command.arg("--upgrade");
if let Some(args) = ctx.config().nix_env_arguments() {
command.args(args.split_whitespace());
};
command.status_checked()
}
}
pub fn run_nix_self_upgrade(ctx: &ExecutionContext) -> Result<()> {
let nix = require("nix")?;
// Should we attempt to upgrade Nix with `nix upgrade-nix`?
#[allow(unused_mut)]
let mut should_self_upgrade = cfg!(target_os = "macos");
#[cfg(target_os = "linux")]
{
// We can't use `nix upgrade-nix` on NixOS.
if let Ok(Distribution::NixOS) = Distribution::detect() {
should_self_upgrade = false;
} }
} }
run_type.execute(nix_channel).arg("--update").check_run()?; if !should_self_upgrade {
return Err(SkipStep(t!("`nix upgrade-nix` can only be used on macOS or non-NixOS Linux").to_string()).into());
if std::path::Path::new(&manifest_json_path).exists() {
run_type
.execute(&nix)
.arg("profile")
.arg("upgrade")
.arg(".*")
.check_run()
} else {
run_type.execute(&nix_env).arg("--upgrade").check_run()
} }
if nix_profile_dir(&nix)?.is_none() {
return Err(
SkipStep(t!("`nix upgrade-nix` cannot be run when Nix is installed in a profile").to_string()).into(),
);
}
print_separator(t!("Nix (self-upgrade)"));
let multi_user = fs::metadata(&nix)?.uid() == 0;
debug!("Multi user nix: {}", multi_user);
let nix_args = nix_args();
if multi_user {
ctx.execute_elevated(&nix, true)?
.args(nix_args)
.arg("upgrade-nix")
.status_checked()
} else {
ctx.run_type()
.execute(&nix)
.args(nix_args)
.arg("upgrade-nix")
.status_checked()
}
}
/// If we try to `nix upgrade-nix` but Nix is installed with `nix profile`, we'll get a `does not
/// appear to be part of a Nix profile` error.
///
/// We duplicate some of the `nix` logic here to avoid this.
/// See: <https://github.com/NixOS/nix/blob/f0180487a0e4c0091b46cb1469c44144f5400240/src/nix/upgrade-nix.cc#L102-L139>
///
/// See: <https://github.com/NixOS/nix/issues/5473>
fn nix_profile_dir(nix: &Path) -> Result<Option<PathBuf>> {
// NOTE: `nix` uses the location of the `nix-env` binary for this but we're using the `nix`
// binary; should be the same.
let nix_bin_dir = nix.parent();
if nix_bin_dir.and_then(|p| p.file_name()) != Some(OsStr::new("bin")) {
debug!("Nix is not installed in a `bin` directory: {nix_bin_dir:?}");
return Ok(None);
}
let nix_dir = nix_bin_dir
.and_then(|bin_dir| bin_dir.parent())
.ok_or_else(|| eyre!("Unable to find Nix install directory from Nix binary {nix:?}"))?;
debug!("Found Nix in {nix_dir:?}");
let mut profile_dir = nix_dir.to_path_buf();
while profile_dir.is_symlink() {
profile_dir = profile_dir
.parent()
.ok_or_else(|| eyre!("Path has no parent: {profile_dir:?}"))?
.join(
profile_dir
.read_link()
.wrap_err_with(|| format!("Failed to read symlink {profile_dir:?}"))?,
);
// NOTE: `nix` uses a hand-rolled canonicalize function, Rust just uses `realpath`.
if profile_dir
.canonicalize()
.wrap_err_with(|| format!("Failed to canonicalize {profile_dir:?}"))?
.components()
.any(|component| component == Component::Normal(OsStr::new("profiles")))
{
break;
}
}
debug!("Found Nix profile {profile_dir:?}");
let user_env = profile_dir
.canonicalize()
.wrap_err_with(|| format!("Failed to canonicalize {profile_dir:?}"))?;
Ok(
if user_env
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.ends_with("user-environment"))
.unwrap_or(false)
{
Some(profile_dir)
} else {
None
},
)
}
fn nix_args() -> [&'static str; 2] {
["--extra-experimental-features", "nix-command"]
} }
pub fn run_yadm(ctx: &ExecutionContext) -> Result<()> { pub fn run_yadm(ctx: &ExecutionContext) -> Result<()> {
@@ -371,50 +602,95 @@ pub fn run_yadm(ctx: &ExecutionContext) -> Result<()> {
print_separator("yadm"); print_separator("yadm");
ctx.run_type().execute(yadm).arg("pull").check_run() ctx.run_type().execute(yadm).arg("pull").status_checked()
} }
pub fn run_asdf(run_type: RunType) -> Result<()> { pub fn run_asdf(ctx: &ExecutionContext) -> Result<()> {
let asdf = require("asdf")?; let asdf = require("asdf")?;
print_separator("asdf"); print_separator("asdf");
let exit_status = run_type.execute(&asdf).arg("update").spawn()?.wait()?; ctx.run_type()
.execute(&asdf)
.arg("update")
.status_checked_with_codes(&[42])?;
if let ExecutorExitStatus::Wet(e) = exit_status { ctx.run_type()
if !(e.success() || e.code().map(|c| c == 42).unwrap_or(false)) { .execute(&asdf)
return Err(TopgradeError::ProcessFailed(e).into()); .args(["plugin", "update", "--all"])
} .status_checked()
}
run_type.execute(&asdf).args(["plugin", "update", "--all"]).check_run()
} }
pub fn run_home_manager(run_type: RunType) -> Result<()> { pub fn run_mise(ctx: &ExecutionContext) -> Result<()> {
let mise = require("mise")?;
print_separator("mise");
ctx.run_type().execute(&mise).arg("upgrade").status_checked()?;
ctx.run_type()
.execute(&mise)
.args(["plugins", "update"])
.status_checked()
}
pub fn run_home_manager(ctx: &ExecutionContext) -> Result<()> {
let home_manager = require("home-manager")?; let home_manager = require("home-manager")?;
print_separator("home-manager"); print_separator("home-manager");
run_type.execute(home_manager).arg("switch").check_run()
let mut cmd = ctx.run_type().execute(home_manager);
cmd.arg("switch");
if let Some(extra_args) = ctx.config().home_manager() {
cmd.args(extra_args);
}
cmd.status_checked()
} }
pub fn run_tldr(run_type: RunType) -> Result<()> { pub fn run_tldr(ctx: &ExecutionContext) -> Result<()> {
let tldr = require("tldr")?; let tldr = require("tldr")?;
print_separator("TLDR"); print_separator("TLDR");
run_type.execute(tldr).arg("--update").check_run() ctx.run_type().execute(tldr).arg("--update").status_checked()
} }
pub fn run_pearl(run_type: RunType) -> Result<()> { pub fn run_pearl(ctx: &ExecutionContext) -> Result<()> {
let pearl = require("pearl")?; let pearl = require("pearl")?;
print_separator("pearl"); print_separator("pearl");
run_type.execute(pearl).arg("update").check_run() ctx.run_type().execute(pearl).arg("update").status_checked()
} }
pub fn run_sdkman(base_dirs: &BaseDirs, cleanup: bool, run_type: RunType) -> Result<()> { 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"));
if !pyenv_dir.exists() {
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(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()
}
pub fn run_sdkman(ctx: &ExecutionContext) -> Result<()> {
let bash = require("bash")?; let bash = require("bash")?;
let sdkman_init_path = env::var("SDKMAN_DIR") let sdkman_init_path = var("SDKMAN_DIR")
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|_| base_dirs.home_dir().join(".sdkman")) .unwrap_or_else(|_| HOME_DIR.join(".sdkman"))
.join("bin") .join("bin")
.join("sdkman-init.sh") .join("sdkman-init.sh")
.require() .require()
@@ -422,9 +698,9 @@ pub fn run_sdkman(base_dirs: &BaseDirs, cleanup: bool, run_type: RunType) -> Res
print_separator("SDKMAN!"); print_separator("SDKMAN!");
let sdkman_config_path = env::var("SDKMAN_DIR") let sdkman_config_path = var("SDKMAN_DIR")
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|_| base_dirs.home_dir().join(".sdkman")) .unwrap_or_else(|_| HOME_DIR.join(".sdkman"))
.join("etc") .join("etc")
.join("config") .join("config")
.require()?; .require()?;
@@ -437,41 +713,57 @@ pub fn run_sdkman(base_dirs: &BaseDirs, cleanup: bool, run_type: RunType) -> Res
if selfupdate_enabled == "true" { if selfupdate_enabled == "true" {
let cmd_selfupdate = format!("source {} && sdk selfupdate", &sdkman_init_path); let cmd_selfupdate = format!("source {} && sdk selfupdate", &sdkman_init_path);
run_type ctx.run_type()
.execute(&bash) .execute(&bash)
.args(["-c", cmd_selfupdate.as_str()]) .args(["-c", cmd_selfupdate.as_str()])
.check_run()?; .status_checked()?;
} }
let cmd_update = format!("source {} && sdk update", &sdkman_init_path); let cmd_update = format!("source {} && sdk update", &sdkman_init_path);
run_type.execute(&bash).args(["-c", cmd_update.as_str()]).check_run()?; ctx.run_type()
.execute(&bash)
.args(["-c", cmd_update.as_str()])
.status_checked()?;
let cmd_upgrade = format!("source {} && sdk upgrade", &sdkman_init_path); let cmd_upgrade = format!("source {} && sdk upgrade", &sdkman_init_path);
run_type.execute(&bash).args(["-c", cmd_upgrade.as_str()]).check_run()?; ctx.run_type()
.execute(&bash)
.args(["-c", cmd_upgrade.as_str()])
.status_checked()?;
if cleanup { if ctx.config().cleanup() {
let cmd_flush_archives = format!("source {} && sdk flush archives", &sdkman_init_path); let cmd_flush_archives = format!("source {} && sdk flush archives", &sdkman_init_path);
run_type ctx.run_type()
.execute(&bash) .execute(&bash)
.args(["-c", cmd_flush_archives.as_str()]) .args(["-c", cmd_flush_archives.as_str()])
.check_run()?; .status_checked()?;
let cmd_flush_temp = format!("source {} && sdk flush temp", &sdkman_init_path); let cmd_flush_temp = format!("source {} && sdk flush temp", &sdkman_init_path);
run_type ctx.run_type()
.execute(&bash) .execute(&bash)
.args(["-c", cmd_flush_temp.as_str()]) .args(["-c", cmd_flush_temp.as_str()])
.check_run()?; .status_checked()?;
} }
Ok(()) Ok(())
} }
pub fn run_bun(ctx: &ExecutionContext) -> Result<()> { pub fn run_bun_packages(ctx: &ExecutionContext) -> Result<()> {
let bun = require("bun")?; let bun = require("bun")?;
print_separator("Bun"); print_separator(t!("Bun Packages"));
ctx.run_type().execute(bun).arg("upgrade").check_run() let mut package_json: PathBuf = var("BUN_INSTALL")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".bun"));
package_json.push("install/global/package.json");
if !package_json.exists() {
println!("{}", t!("No global packages installed"));
return Ok(());
}
ctx.run_type().execute(bun).args(["-g", "update"]).status_checked()
} }
/// Update dotfiles with `rcm(7)`. /// Update dotfiles with `rcm(7)`.
@@ -481,10 +773,18 @@ pub fn run_rcm(ctx: &ExecutionContext) -> Result<()> {
let rcup = require("rcup")?; let rcup = require("rcup")?;
print_separator("rcm"); print_separator("rcm");
ctx.run_type().execute(rcup).arg("-v").check_run() ctx.run_type().execute(rcup).arg("-v").status_checked()
} }
pub fn reboot() { pub fn run_maza(ctx: &ExecutionContext) -> Result<()> {
print!("Rebooting..."); let maza = require("maza")?;
Command::new("sudo").arg("reboot").spawn().unwrap().wait().unwrap();
print_separator("maza");
ctx.run_type().execute(maza).arg("update").status_checked()
}
pub fn reboot() -> Result<()> {
print!("{}", t!("Rebooting..."));
Command::new("sudo").arg("reboot").status_checked()
} }

View File

@@ -1,16 +1,17 @@
use std::convert::TryFrom;
use std::path::Path; use std::path::Path;
use std::{ffi::OsStr, process::Command}; use std::{ffi::OsStr, process::Command};
use anyhow::Result; use color_eyre::eyre::Result;
use log::debug; use etcetera::base_strategy::BaseStrategy;
use tracing::debug;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, RunType};
use crate::terminal::{print_separator, print_warning}; use crate::terminal::{print_separator, print_warning};
use crate::utils::require; use crate::utils::{require, which};
use crate::{error::SkipStep, steps::git::Repositories}; use crate::{error::SkipStep, steps::git::RepoStep};
use crate::{powershell, Step}; use crate::{powershell, Step};
use rust_i18n::t;
pub fn run_chocolatey(ctx: &ExecutionContext) -> Result<()> { pub fn run_chocolatey(ctx: &ExecutionContext) -> Result<()> {
let choco = require("choco")?; let choco = require("choco")?;
@@ -18,23 +19,22 @@ pub fn run_chocolatey(ctx: &ExecutionContext) -> Result<()> {
print_separator("Chocolatey"); print_separator("Chocolatey");
let mut cmd = &choco; let mut command = match ctx.sudo() {
let mut args = vec!["upgrade", "all"]; Some(sudo) => {
let mut command = ctx.run_type().execute(sudo);
command.arg(choco);
command
}
None => ctx.run_type().execute(choco),
};
if let Some(sudo) = ctx.sudo() { command.args(["upgrade", "all"]);
cmd = sudo;
args.insert(0, "choco");
}
let mut command = ctx.run_type().execute(cmd);
command.args(&args);
if yes { if yes {
command.arg("--yes"); command.arg("--yes");
} }
command.check_run() command.status_checked()
} }
pub fn run_winget(ctx: &ExecutionContext) -> Result<()> { pub fn run_winget(ctx: &ExecutionContext) -> Result<()> {
@@ -42,31 +42,80 @@ pub fn run_winget(ctx: &ExecutionContext) -> Result<()> {
print_separator("winget"); print_separator("winget");
if !ctx.config().enable_winget() { ctx.run_type()
print_warning("Winget is disabled by default. Enable it by setting enable_winget=true in the [windows] section in the configuration."); .execute(winget)
return Err(SkipStep(String::from("Winget is disabled by default")).into()); .args(["upgrade", "--all"])
} .status_checked()
ctx.run_type().execute(&winget).args(["upgrade", "--all"]).check_run()
} }
pub fn run_scoop(cleanup: bool, run_type: RunType) -> Result<()> { pub fn run_scoop(ctx: &ExecutionContext) -> Result<()> {
let scoop = require("scoop")?; let scoop = require("scoop")?;
print_separator("Scoop"); print_separator("Scoop");
run_type.execute(&scoop).args(["update"]).check_run()?; ctx.run_type().execute(&scoop).args(["update"]).status_checked()?;
run_type.execute(&scoop).args(["update", "*"]).check_run()?; ctx.run_type().execute(&scoop).args(["update", "*"]).status_checked()?;
if cleanup { if ctx.config().cleanup() {
run_type.execute(&scoop).args(["cleanup", "*"]).check_run()?; ctx.run_type().execute(&scoop).args(["cleanup", "*"]).status_checked()?;
ctx.run_type()
.execute(&scoop)
.args(["cache", "rm", "-a"])
.status_checked()?
} }
Ok(()) Ok(())
} }
pub fn update_wsl(ctx: &ExecutionContext) -> Result<()> {
if !is_wsl_installed()? {
return Err(SkipStep(t!("WSL not installed").to_string()).into());
}
let wsl = require("wsl")?;
print_separator(t!("Update WSL"));
let mut wsl_command = ctx.run_type().execute(wsl);
wsl_command.args(["--update"]);
if ctx.config().wsl_update_pre_release() {
wsl_command.args(["--pre-release"]);
}
if ctx.config().wsl_update_use_web_download() {
wsl_command.args(["--web-download"]);
}
wsl_command.status_checked()?;
Ok(())
}
/// Detect if WSL is installed or not.
///
/// For WSL, we cannot simply check if command `wsl` is installed as on newer
/// versions of Windows (since windows 10 version 2004), this commmand is
/// installed by default.
///
/// If the command is installed and the user hasn't installed any Linux distros
/// on it, command `wsl -l` would print a help message and exit with failure, we
/// use this to check whether WSL is install or not.
fn is_wsl_installed() -> Result<bool> {
if let Some(wsl) = which("wsl") {
// Don't use `output_checked` as an execution failure log is not wanted
#[allow(clippy::disallowed_methods)]
let output = Command::new(wsl).arg("-l").output()?;
let status = output.status;
if status.success() {
return Ok(true);
}
}
Ok(false)
}
fn get_wsl_distributions(wsl: &Path) -> Result<Vec<String>> { fn get_wsl_distributions(wsl: &Path) -> Result<Vec<String>> {
let output = Command::new(wsl).args(["--list", "-q"]).check_output()?; let output = Command::new(wsl).args(["--list", "-q"]).output_checked_utf8()?.stdout;
Ok(output Ok(output
.lines() .lines()
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
@@ -77,24 +126,61 @@ fn get_wsl_distributions(wsl: &Path) -> Result<Vec<String>> {
fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> Result<()> { fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> Result<()> {
let topgrade = Command::new(wsl) let topgrade = Command::new(wsl)
.args(["-d", dist, "bash", "-lc", "which topgrade"]) .args(["-d", dist, "bash", "-lc", "which topgrade"])
.check_output() .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();
let mut command = ctx.run_type().execute(wsl);
// The `arg` method automatically quotes its arguments.
// This means we can't append additional arguments to `topgrade` in WSL
// by calling `arg` successively.
//
// For example:
//
// ```rust
// command
// .args(["-d", dist, "bash", "-c"])
// .arg(format!("TOPGRADE_PREFIX={dist} exec {topgrade}"));
// ```
//
// creates a command string like:
// > `C:\WINDOWS\system32\wsl.EXE -d Ubuntu bash -c 'TOPGRADE_PREFIX=Ubuntu exec /bin/topgrade'`
//
// Adding the following:
//
// ```rust
// command.arg("-v");
// ```
//
// appends the next argument like so:
// > `C:\WINDOWS\system32\wsl.EXE -d Ubuntu bash -c 'TOPGRADE_PREFIX=Ubuntu exec /bin/topgrade' -v`
// which means `-v` isn't passed to `topgrade`.
let mut args = String::new();
if ctx.config().verbose() {
args.push_str("-v");
}
let mut command = ctx.run_type().execute(&wsl);
command command
.args(["-d", dist, "bash", "-c"]) .args(["-d", dist, "bash", "-c"])
.arg(format!("TOPGRADE_PREFIX={} exec {}", dist, topgrade)); .arg(format!("TOPGRADE_PREFIX={dist} exec {topgrade} {args}"));
if ctx.config().yes(Step::Wsl) { if ctx.config().yes(Step::Wsl) {
command.arg("-y"); command.arg("-y");
} }
command.check_run() command.status_checked()
} }
pub fn run_wsl_topgrade(ctx: &ExecutionContext) -> Result<()> { pub fn run_wsl_topgrade(ctx: &ExecutionContext) -> Result<()> {
if !is_wsl_installed()? {
return Err(SkipStep(t!("WSL not installed").to_string()).into());
}
let wsl = require("wsl")?; let wsl = require("wsl")?;
let wsl_distributions = get_wsl_distributions(wsl)?; let wsl_distributions = get_wsl_distributions(&wsl)?;
let mut ran = false; let mut ran = false;
debug!("WSL distributions: {:?}", wsl_distributions); debug!("WSL distributions: {:?}", wsl_distributions);
@@ -113,33 +199,36 @@ pub fn run_wsl_topgrade(ctx: &ExecutionContext) -> Result<()> {
if ran { if ran {
Ok(()) Ok(())
} else { } 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<()> { pub fn windows_update(ctx: &ExecutionContext) -> Result<()> {
let powershell = powershell::Powershell::windows_powershell(); let powershell = powershell::Powershell::windows_powershell();
print_separator(t!("Windows Update"));
if powershell.supports_windows_update() { if powershell.supports_windows_update() {
print_separator("Windows Update"); println!("The installer will request to run as administrator, expect a prompt.");
return powershell.windows_update(ctx);
powershell.windows_update(ctx)
} else {
print_warning(t!(
"Consider installing PSWindowsUpdate as the use of Windows Update via USOClient is not supported."
));
Err(SkipStep(t!("USOClient not supported.").to_string()).into())
} }
let usoclient = require("UsoClient")?;
print_separator("Windows Update");
println!("Running Windows Update. Check the control panel for progress.");
ctx.run_type().execute(&usoclient).arg("ScanInstallWait").check_run()?;
ctx.run_type().execute(&usoclient).arg("StartInstall").check_run()
} }
pub fn reboot() { pub fn reboot() -> Result<()> {
Command::new("shutdown").args(["/R", "/T", "0"]).spawn().ok(); // If this works, it won't return, but if it doesn't work, it may return a useful error
// message.
Command::new("shutdown").args(["/R", "/T", "0"]).status_checked()
} }
pub fn insert_startup_scripts(ctx: &ExecutionContext, git_repos: &mut Repositories) -> Result<()> { pub fn insert_startup_scripts(git_repos: &mut RepoStep) -> Result<()> {
let startup_dir = ctx let startup_dir = crate::WINDOWS_DIRS
.base_dirs()
.data_dir() .data_dir()
.join("Microsoft\\Windows\\Start Menu\\Programs\\Startup"); .join("Microsoft\\Windows\\Start Menu\\Programs\\Startup");
for entry in std::fs::read_dir(&startup_dir)?.flatten() { for entry in std::fs::read_dir(&startup_dir)?.flatten() {
@@ -148,7 +237,7 @@ pub fn insert_startup_scripts(ctx: &ExecutionContext, git_repos: &mut Repositori
if let Ok(lnk) = parselnk::Lnk::try_from(Path::new(&path)) { if let Ok(lnk) = parselnk::Lnk::try_from(Path::new(&path)) {
debug!("Startup link: {:?}", lnk); debug!("Startup link: {:?}", lnk);
if let Some(path) = lnk.relative_path() { 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

@@ -3,10 +3,11 @@ use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use anyhow::Result; use color_eyre::eyre::Result;
use rust_i18n::t;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::executor::CommandExt;
use crate::terminal::{is_dumb, print_separator}; use crate::terminal::{is_dumb, print_separator};
use crate::utils::{require_option, which, PathExt}; use crate::utils::{require_option, which, PathExt};
use crate::Step; use crate::Step;
@@ -27,8 +28,8 @@ impl Powershell {
let profile = path.as_ref().and_then(|path| { let profile = path.as_ref().and_then(|path| {
Command::new(path) Command::new(path)
.args(["-NoProfile", "-Command", "Split-Path $profile"]) .args(["-NoProfile", "-Command", "Split-Path $profile"])
.check_output() .output_checked_utf8()
.map(|output| PathBuf::from(output.trim())) .map(|output| PathBuf::from(output.stdout.trim()))
.and_then(|p| p.require()) .and_then(|p| p.require())
.ok() .ok()
}); });
@@ -50,10 +51,10 @@ impl Powershell {
.args([ .args([
"-NoProfile", "-NoProfile",
"-Command", "-Command",
&format!("Get-Module -ListAvailable {}", command), &format!("Get-Module -ListAvailable {command}"),
]) ])
.check_output() .output_checked_utf8()
.map(|result| !result.is_empty()) .map(|result| !result.stdout.is_empty())
.unwrap_or(false) .unwrap_or(false)
} }
@@ -62,9 +63,9 @@ impl Powershell {
} }
pub fn update_modules(&self, ctx: &ExecutionContext) -> Result<()> { 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"]; let mut cmd = vec!["Update-Module"];
@@ -76,12 +77,12 @@ impl Powershell {
cmd.push("-Force") cmd.push("-Force")
} }
println!("Updating modules..."); println!("{}", t!("Updating modules..."));
ctx.run_type() ctx.run_type()
.execute(powershell) .execute(powershell)
// This probably doesn't need `shell_words::join`. // This probably doesn't need `shell_words::join`.
.args(["-NoProfile", "-Command", &cmd.join(" ")]) .args(["-NoProfile", "-Command", &cmd.join(" ")])
.check_run() .status_checked()
} }
#[cfg(windows)] #[cfg(windows)]
@@ -94,10 +95,18 @@ impl Powershell {
#[cfg(windows)] #[cfg(windows)]
pub fn windows_update(&self, ctx: &ExecutionContext) -> Result<()> { 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()); 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 = if let Some(sudo) = ctx.sudo() {
let mut command = ctx.run_type().execute(sudo); let mut command = ctx.run_type().execute(sudo);
command.arg(powershell); command.arg(powershell);
@@ -107,18 +116,7 @@ impl Powershell {
}; };
command command
.args([ .args(["-NoProfile", &install_windowsupdate_verbose, accept_all])
"-NoProfile", .status_checked()
"-Command",
&format!(
"Import-Module PSWindowsUpdate; Install-WindowsUpdate -MicrosoftUpdate {} -Verbose",
if ctx.config().accept_all_windows_updates() {
"-AcceptAll"
} else {
""
}
),
])
.check_run()
} }
} }

View File

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

View File

@@ -2,13 +2,14 @@ use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::{fmt::Display, rc::Rc, str::FromStr}; use std::{fmt::Display, rc::Rc, str::FromStr};
use anyhow::Result; use color_eyre::eyre::Result;
use log::{debug, error};
use regex::Regex; use regex::Regex;
use rust_i18n::t;
use strum::EnumString; use strum::EnumString;
use tracing::{debug, error};
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::executor::CommandExt;
use crate::terminal::print_separator; use crate::terminal::print_separator;
use crate::{error::SkipStep, utils, Step}; use crate::{error::SkipStep, utils, Step};
@@ -61,10 +62,11 @@ impl Vagrant {
let output = Command::new(&self.path) let output = Command::new(&self.path)
.arg("status") .arg("status")
.current_dir(directory) .current_dir(directory)
.check_output()?; .output_checked_utf8()?;
debug!("Vagrant output in {}: {}", directory, output); debug!("Vagrant output in {}: {}", directory, output);
let boxes = output let boxes = output
.stdout
.split('\n') .split('\n')
.skip(2) .skip(2)
.take_while(|line| !(line.is_empty() || line.starts_with('\r'))) .take_while(|line| !(line.is_empty() || line.starts_with('\r')))
@@ -115,7 +117,7 @@ impl<'a> TemporaryPowerOn<'a> {
.execute(vagrant) .execute(vagrant)
.args([subcommand, &vagrant_box.name]) .args([subcommand, &vagrant_box.name])
.current_dir(vagrant_box.path.clone()) .current_dir(vagrant_box.path.clone())
.check_run()?; .status_checked()?;
Ok(TemporaryPowerOn { Ok(TemporaryPowerOn {
vagrant, vagrant,
vagrant_box, vagrant_box,
@@ -142,7 +144,7 @@ impl<'a> Drop for TemporaryPowerOn<'a> {
.execute(self.vagrant) .execute(self.vagrant)
.args([subcommand, &self.vagrant_box.name]) .args([subcommand, &self.vagrant_box.name])
.current_dir(self.vagrant_box.path.clone()) .current_dir(self.vagrant_box.path.clone())
.check_run() .status_checked()
.ok(); .ok();
} }
} }
@@ -150,14 +152,14 @@ impl<'a> Drop for TemporaryPowerOn<'a> {
pub fn collect_boxes(ctx: &ExecutionContext) -> Result<Vec<VagrantBox>> { pub fn collect_boxes(ctx: &ExecutionContext) -> Result<Vec<VagrantBox>> {
let directories = utils::require_option( let directories = utils::require_option(
ctx.config().vagrant_directories(), 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 { let vagrant = Vagrant {
path: utils::require("vagrant")?, path: utils::require("vagrant")?,
}; };
print_separator("Vagrant"); print_separator("Vagrant");
println!("Collecting Vagrant boxes"); println!("{}", t!("Collecting Vagrant boxes"));
let mut result = Vec::new(); let mut result = Vec::new();
@@ -182,7 +184,11 @@ pub fn topgrade_vagrant_box(ctx: &ExecutionContext, vagrant_box: &VagrantBox) ->
let mut _poweron = None; let mut _poweron = None;
if !vagrant_box.initial_status.powered_on() { if !vagrant_box.initial_status.powered_on() {
if !(ctx.config().vagrant_power_on().unwrap_or(true)) { 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 { } else {
print_separator(seperator); print_separator(seperator);
_poweron = Some(vagrant.temporary_power_on(vagrant_box, ctx)?); _poweron = Some(vagrant.temporary_power_on(vagrant_box, ctx)?);
@@ -199,21 +205,21 @@ pub fn topgrade_vagrant_box(ctx: &ExecutionContext, vagrant_box: &VagrantBox) ->
.execute(&vagrant.path) .execute(&vagrant.path)
.current_dir(&vagrant_box.path) .current_dir(&vagrant_box.path)
.args(["ssh", "-c", &command]) .args(["ssh", "-c", &command])
.check_run() .status_checked()
} }
pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> { pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> {
let vagrant = utils::require("vagrant")?; let vagrant = utils::require("vagrant")?;
print_separator("Vagrant boxes"); print_separator(t!("Vagrant boxes"));
let outdated = Command::new(&vagrant) let outdated = Command::new(&vagrant)
.args(["box", "outdated", "--global"]) .args(["box", "outdated", "--global"])
.check_output()?; .output_checked_utf8()?;
let re = Regex::new(r"\* '(.*?)' for '(.*?)' is outdated").unwrap(); let re = Regex::new(r"\* '(.*?)' for '(.*?)' is outdated").unwrap();
let mut found = false; let mut found = false;
for ele in re.captures_iter(&outdated) { for ele in re.captures_iter(&outdated.stdout) {
found = true; found = true;
let _ = ctx let _ = ctx
.run_type() .run_type()
@@ -222,13 +228,16 @@ pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> {
.arg(ele.get(1).unwrap().as_str()) .arg(ele.get(1).unwrap().as_str())
.arg("--provider") .arg("--provider")
.arg(ele.get(2).unwrap().as_str()) .arg(ele.get(2).unwrap().as_str())
.check_run(); .status_checked();
} }
if !found { if !found {
println!("No outdated boxes") println!("{}", t!("No outdated boxes"))
} else { } else {
ctx.run_type().execute(&vagrant).args(["box", "prune"]).check_run()?; ctx.run_type()
.execute(&vagrant)
.args(["box", "prune"])
.status_checked()?;
} }
Ok(()) Ok(())

View File

@@ -1,26 +1,38 @@
use crate::executor::RunType; use std::env;
use std::path::PathBuf;
use std::process::Command;
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use crate::command::CommandExt;
use crate::config::TmuxConfig;
use crate::config::TmuxSessionMode;
use crate::terminal::print_separator; use crate::terminal::print_separator;
use crate::HOME_DIR;
use crate::{ use crate::{
execution_context::ExecutionContext, execution_context::ExecutionContext,
utils::{which, Check, PathExt}, utils::{which, PathExt},
}; };
use anyhow::Result;
use directories::BaseDirs;
use std::env;
use std::io;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{exit, Command};
pub fn run_tpm(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { use rust_i18n::t;
let tpm = base_dirs #[cfg(unix)]
.home_dir() use std::os::unix::process::CommandExt as _;
.join(".tmux/plugins/tpm/bin/update_plugins")
.require()?; pub fn run_tpm(ctx: &ExecutionContext) -> Result<()> {
let tpm = match env::var("TMUX_PLUGIN_MANAGER_PATH") {
// If `TMUX_PLUGIN_MANAGER_PATH` is set, search for
// `$TMUX_PLUGIN_MANAGER_PATH/bin/install_plugins/tpm/bin/update_plugins`
Ok(var) => PathBuf::from(var).join("bin/install_plugins/tpm/bin/update_plugins"),
// Otherwise, use the default location `~/.tmux/plugins/tpm/bin/update_plugins`
Err(_) => HOME_DIR.join(".tmux/plugins/tpm/bin/update_plugins"),
}
.require()?;
print_separator("tmux plugins"); print_separator("tmux plugins");
run_type.execute(tpm).arg("all").check_run() ctx.run_type().execute(tpm).arg("all").status_checked()
} }
struct Tmux { struct Tmux {
@@ -44,73 +56,144 @@ impl Tmux {
command command
} }
fn has_session(&self, session_name: &str) -> Result<bool, io::Error> { fn has_session(&self, session_name: &str) -> Result<bool> {
Ok(self Ok(self
.build() .build()
.args(["has-session", "-t", session_name]) .args(["has-session", "-t", session_name])
.output()? .output_checked_with(|_| Ok(()))?
.status .status
.success()) .success())
} }
fn new_session(&self, session_name: &str) -> Result<bool, io::Error> { /// Create a new tmux session with the given name, running the given command.
Ok(self /// The command is passed to `sh` (see "shell-command arguments are sh(1) commands" in the
/// `tmux(1)` man page).
fn new_session(&self, session_name: &str, window_name: &str, command: &str) -> Result<()> {
let _ = self
.build() .build()
.args(["new-session", "-d", "-s", session_name, "-n", "dummy"]) // `-d`: initial size comes from the global `default-size` option (instead
.spawn()? // of passing `-x` and `-y` arguments.
.wait()? // (What do those even do?)
.success()) // `-s`: session name
// `-n`: window name (always `topgrade`)
.args(["new-session", "-d", "-s", session_name, "-n", window_name, command])
.output_checked()?;
Ok(())
} }
fn run_in_session(&self, command: &str) -> Result<()> { /// Like [`new_session`] but it appends a digit to the session name (if necessary) to
self.build() /// avoid duplicate session names.
.args(["new-window", "-t", "topgrade", command]) ///
.spawn()? /// The session name is returned.
.wait()? fn new_unique_session(&self, session_name: &str, window_name: &str, command: &str) -> Result<String> {
.check()?; let mut session = session_name.to_owned();
for i in 1.. {
if !self
.has_session(&session)
.context("Error determining if a tmux session exists")?
{
self.new_session(&session, window_name, command)
.context("Error running Topgrade in tmux")?;
return Ok(session);
}
session = format!("{session_name}-{i}");
}
unreachable!()
}
Ok(()) /// Create a new window in the given tmux session, running the given command.
fn new_window(&self, session_name: &str, window_name: &str, command: &str) -> Result<()> {
self.build()
// `-d`: initial size comes from the global `default-size` option (instead
// of passing `-x` and `-y` arguments.
// (What do those even do?)
// `-s`: session name
// `-n`: window name
.args([
"new-window",
"-a",
"-t",
&format!("{session_name}:{window_name}"),
"-n",
window_name,
command,
])
.env_remove("TMUX")
.status_checked()
}
fn window_indices(&self, session_name: &str) -> Result<Vec<usize>> {
self.build()
.args(["list-windows", "-F", "#{window_index}", "-t", session_name])
.output_checked_utf8()?
.stdout
.lines()
.map(|l| l.parse())
.collect::<Result<Vec<usize>, _>>()
.context("Failed to compute tmux windows")
} }
} }
pub fn run_in_tmux(args: Vec<String>) -> ! { pub fn run_in_tmux(config: TmuxConfig) -> Result<()> {
let command = { let command = {
let mut command = vec![ let mut command = vec![
String::from("env"), String::from("env"),
String::from("TOPGRADE_KEEP_END=1"), String::from("TOPGRADE_KEEP_END=1"),
String::from("TOPGRADE_INSIDE_TMUX=1"), String::from("TOPGRADE_INSIDE_TMUX=1"),
]; ];
// TODO: Should we use `topgrade` instead of the first argument here, which may be
// a local path?
command.extend(env::args()); command.extend(env::args());
shell_words::join(command) shell_words::join(command)
}; };
let tmux = Tmux::new(args); let tmux = Tmux::new(config.args);
if !tmux.has_session("topgrade").expect("Error detecting a tmux session") { // Find an unused session and run `topgrade` in it with the current command's arguments.
tmux.new_session("topgrade").expect("Error creating a tmux session"); let session_name = "topgrade";
} let window_name = "topgrade";
let session = tmux.new_unique_session(session_name, window_name, &command)?;
tmux.run_in_session(&command).expect("Error running Topgrade in tmux"); let is_inside_tmux = env::var("TMUX").is_ok();
tmux.build() let err = match config.session_mode {
.args(["kill-window", "-t", "topgrade:dummy"]) TmuxSessionMode::AttachIfNotInSession => {
.output() if is_inside_tmux {
.expect("Error killing the dummy tmux window"); // 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()
}
}
if env::var("TMUX").is_err() { TmuxSessionMode::AttachAlways => {
let err = tmux.build().args(["attach", "-t", "topgrade"]).exec(); if is_inside_tmux {
panic!("{:?}", err); tmux.build().args(["switch-client", "-t", &session]).exec()
} else { } else {
println!("Topgrade launched in a new tmux session"); tmux.build().args(["attach-session", "-t", &session]).exec()
exit(0); }
} }
};
Err(eyre!("{err}")).context("Failed to `execvp(3)` tmux")
} }
pub fn run_command(ctx: &ExecutionContext, command: &str) -> Result<()> { pub fn run_command(ctx: &ExecutionContext, window_name: &str, command: &str) -> Result<()> {
Tmux::new(ctx.config().tmux_arguments()?) let tmux = Tmux::new(ctx.config().tmux_config()?.args);
.build()
.args(["new-window", "-a", "-t", "topgrade:1", command]) match ctx.get_tmux_session() {
.env_remove("TMUX") Some(session_name) => {
.spawn()? let indices = tmux.window_indices(&session_name)?;
.wait()? let last_window = indices
.check() .iter()
.last()
.ok_or_else(|| eyre!("tmux session {session_name} has no windows"))?;
tmux.new_window(&session_name, &format!("{last_window}"), command)?;
}
None => {
let name = tmux.new_unique_session("topgrade", window_name, command)?;
ctx.set_tmux_session(name);
}
}
Ok(())
} }

View File

@@ -1,17 +1,20 @@
use anyhow::Result; use color_eyre::eyre::Result;
use crate::command::CommandExt;
use crate::config::Step; use crate::config::Step;
use crate::terminal::print_separator; use crate::terminal::print_separator;
use crate::{execution_context::ExecutionContext, utils::require}; use crate::{execution_context::ExecutionContext, utils::require};
use log::debug;
use std::path::Path; use std::path::Path;
use std::{path::PathBuf, process::Command}; use std::{path::PathBuf, process::Command};
use tracing::debug;
fn list_toolboxes(toolbx: &Path) -> Result<Vec<String>> { fn list_toolboxes(toolbx: &Path) -> Result<Vec<String>> {
let output = Command::new(toolbx).args(["list", "--containers"]).output()?; let output = Command::new(toolbx)
let output_str = String::from_utf8(output.stdout)?; .args(["list", "--containers"])
.output_checked_utf8()?;
let proc: Vec<String> = output_str let proc: Vec<String> = output
.stdout
.lines() .lines()
// Skip the first line since that contains only status information // Skip the first line since that contains only status information
.skip(1) .skip(1)
@@ -39,7 +42,7 @@ pub fn run_toolbx(ctx: &ExecutionContext) -> Result<()> {
let topgrade_path = topgrade_path.to_str().unwrap(); let topgrade_path = topgrade_path.to_str().unwrap();
for tb in toolboxes.iter() { for tb in toolboxes.iter() {
let topgrade_prefix = format!("TOPGRADE_PREFIX='Toolbx {}'", tb); let topgrade_prefix = format!("TOPGRADE_PREFIX='Toolbx {tb}'");
let mut args = vec![ let mut args = vec![
"run", "run",
"-c", "-c",
@@ -49,12 +52,14 @@ pub fn run_toolbx(ctx: &ExecutionContext) -> Result<()> {
topgrade_path, topgrade_path,
"--only", "--only",
"system", "system",
"--no-self-update",
"--skip-notify",
]; ];
if ctx.config().yes(Step::Toolbx) { if ctx.config().yes(Step::Toolbx) {
args.push("--yes"); args.push("--yes");
} }
let _output = ctx.run_type().execute(&toolbx).args(&args).check_run(); ctx.run_type().execute(&toolbx).args(&args).status_checked()?;
} }
Ok(()) Ok(())

View File

@@ -1,3 +1,19 @@
" AstroUpdate calls a plugin manager - Lazy as of this writing. So we check for it before
" others. Add to init.lua:
" updater = {
" skip_prompts = true,
" },
if exists(":AstroUpdate")
echo "AstroUpdate"
AstroUpdate
quitall
endif
if exists(":MasonUpdate")
echo "MasonUpdate"
MasonUpdate
endif
if exists(":NeoBundleUpdate") if exists(":NeoBundleUpdate")
echo "NeoBundle" echo "NeoBundle"
NeoBundleUpdate NeoBundleUpdate
@@ -33,15 +49,15 @@ if exists(":PaqUpdate")
PaqUpdate PaqUpdate
endif endif
if exists(":CocUpdateSync") if exists(":Lazy")
echo "CocUpdateSync" echo "Lazy Update"
CocUpdateSync Lazy! sync | qa
endif endif
if exists(':PackerSync') if exists(':PackerSync')
echo "Packer" echo "Packer"
autocmd User PackerComplete quitall autocmd User PackerComplete quitall
PackerSync PackerSync
else else
quitall quitall
endif endif

View File

@@ -1,38 +1,38 @@
use crate::command::CommandExt;
use crate::error::{SkipStep, TopgradeError}; use crate::error::{SkipStep, TopgradeError};
use anyhow::Result; use crate::HOME_DIR;
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
use crate::executor::{CommandExt, Executor, ExecutorOutput, RunType}; use crate::executor::{Executor, ExecutorOutput};
use crate::terminal::print_separator; use crate::terminal::print_separator;
use crate::{ use crate::{
execution_context::ExecutionContext, execution_context::ExecutionContext,
utils::{require, PathExt}, utils::{require, PathExt},
}; };
use directories::BaseDirs; use rust_i18n::t;
use log::debug;
use std::path::PathBuf; use std::path::PathBuf;
use std::{ use std::{
io::{self, Write}, io::{self, Write},
process::Command, process::Command,
}; };
use tracing::debug;
const UPGRADE_VIM: &str = include_str!("upgrade.vim"); const UPGRADE_VIM: &str = include_str!("upgrade.vim");
pub fn vimrc(base_dirs: &BaseDirs) -> Result<PathBuf> { pub fn vimrc() -> Result<PathBuf> {
base_dirs HOME_DIR
.home_dir()
.join(".vimrc") .join(".vimrc")
.require() .require()
.or_else(|_| base_dirs.home_dir().join(".vim/vimrc").require()) .or_else(|_| HOME_DIR.join(".vim/vimrc").require())
} }
fn nvimrc(base_dirs: &BaseDirs) -> Result<PathBuf> { fn nvimrc() -> Result<PathBuf> {
#[cfg(unix)] #[cfg(unix)]
let base_dir = let base_dir = crate::XDG_DIRS.config_dir();
// Bypass directories crate as nvim doesn't use the macOS-specific directories.
std::env::var_os("XDG_CONFIG_HOME").map_or_else(|| base_dirs.home_dir().join(".config"), PathBuf::from);
#[cfg(windows)] #[cfg(windows)]
let base_dir = base_dirs.cache_dir(); let base_dir = crate::WINDOWS_DIRS.cache_dir();
base_dir base_dir
.join("nvim/init.vim") .join("nvim/init.vim")
@@ -58,14 +58,14 @@ fn upgrade(command: &mut Executor, ctx: &ExecutionContext) -> Result<()> {
let status = output.status; let status = output.status;
if !status.success() || ctx.config().verbose() { if !status.success() || ctx.config().verbose() {
io::stdout().write(&output.stdout).ok(); io::stdout().write_all(&output.stdout).ok();
io::stderr().write(&output.stderr).ok(); io::stderr().write_all(&output.stderr).ok();
} }
if !status.success() { if !status.success() {
return Err(TopgradeError::ProcessFailed(status).into()); return Err(TopgradeError::ProcessFailed(command.get_program(), status).into());
} else { } else {
println!("Plugins upgraded") println!("{}", t!("Plugins upgraded"))
} }
} }
@@ -73,46 +73,46 @@ fn upgrade(command: &mut Executor, ctx: &ExecutionContext) -> Result<()> {
} }
pub fn upgrade_ultimate_vimrc(ctx: &ExecutionContext) -> Result<()> { pub fn upgrade_ultimate_vimrc(ctx: &ExecutionContext) -> Result<()> {
let config_dir = ctx.base_dirs().home_dir().join(".vim_runtime").require()?; let config_dir = HOME_DIR.join(".vim_runtime").require()?;
let git = require("git")?; let git = require("git")?;
let python = require("python3")?; let python = require("python3")?;
let update_plugins = config_dir.join("update_plugins.py").require()?; let update_plugins = config_dir.join("update_plugins.py").require()?;
print_separator("The Ultimate vimrc"); print_separator(t!("The Ultimate vimrc"));
ctx.run_type() ctx.run_type()
.execute(&git) .execute(&git)
.current_dir(&config_dir) .current_dir(&config_dir)
.args(["reset", "--hard"]) .args(["reset", "--hard"])
.check_run()?; .status_checked()?;
ctx.run_type() ctx.run_type()
.execute(&git) .execute(&git)
.current_dir(&config_dir) .current_dir(&config_dir)
.args(["clean", "-d", "--force"]) .args(["clean", "-d", "--force"])
.check_run()?; .status_checked()?;
ctx.run_type() ctx.run_type()
.execute(&git) .execute(&git)
.current_dir(&config_dir) .current_dir(&config_dir)
.args(["pull", "--rebase"]) .args(["pull", "--rebase"])
.check_run()?; .status_checked()?;
ctx.run_type() ctx.run_type()
.execute(python) .execute(python)
.current_dir(config_dir) .current_dir(config_dir)
.arg(update_plugins) .arg(update_plugins)
.check_run()?; .status_checked()?;
Ok(()) Ok(())
} }
pub fn upgrade_vim(base_dirs: &BaseDirs, ctx: &ExecutionContext) -> Result<()> { pub fn upgrade_vim(ctx: &ExecutionContext) -> Result<()> {
let vim = require("vim")?; let vim = require("vim")?;
let output = Command::new(&vim).arg("--version").check_output()?; let output = Command::new(&vim).arg("--version").output_checked_utf8()?;
if !output.starts_with("VIM") { 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(base_dirs)?; let vimrc = vimrc()?;
print_separator("Vim"); print_separator("Vim");
upgrade( upgrade(
@@ -126,9 +126,9 @@ pub fn upgrade_vim(base_dirs: &BaseDirs, ctx: &ExecutionContext) -> Result<()> {
) )
} }
pub fn upgrade_neovim(base_dirs: &BaseDirs, ctx: &ExecutionContext) -> Result<()> { pub fn upgrade_neovim(ctx: &ExecutionContext) -> Result<()> {
let nvim = require("nvim")?; let nvim = require("nvim")?;
let nvimrc = nvimrc(base_dirs)?; let nvimrc = nvimrc()?;
print_separator("Neovim"); print_separator("Neovim");
upgrade( upgrade(
@@ -142,10 +142,10 @@ pub fn upgrade_neovim(base_dirs: &BaseDirs, ctx: &ExecutionContext) -> Result<()
) )
} }
pub fn run_voom(_base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { pub fn run_voom(ctx: &ExecutionContext) -> Result<()> {
let voom = require("voom")?; let voom = require("voom")?;
print_separator("voom"); print_separator("voom");
run_type.execute(voom).arg("update").check_run() ctx.run_type().execute(voom).arg("update").status_checked()
} }

View File

@@ -1,149 +1,216 @@
use crate::execution_context::ExecutionContext;
use crate::executor::{CommandExt, RunType};
use crate::git::Repositories;
use crate::terminal::print_separator;
use crate::utils::{require, PathExt};
use anyhow::Result;
use directories::BaseDirs;
use log::debug;
use std::env; use std::env;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use color_eyre::eyre::Result;
use tracing::debug;
use walkdir::WalkDir; use walkdir::WalkDir;
pub fn run_zr(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::git::RepoStep;
use crate::terminal::print_separator;
use crate::utils::{require, PathExt};
use crate::HOME_DIR;
use crate::XDG_DIRS;
use etcetera::base_strategy::BaseStrategy;
pub fn run_zr(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?; let zsh = require("zsh")?;
require("zr")?; require("zr")?;
print_separator("zr"); print_separator("zr");
let cmd = format!("source {} && zr --update", zshrc(base_dirs).display()); let cmd = format!("source {} && zr --update", zshrc().display());
run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).check_run() ctx.run_type()
.execute(zsh)
.args(["-l", "-c", cmd.as_str()])
.status_checked()
} }
pub fn zshrc(base_dirs: &BaseDirs) -> PathBuf { fn zdotdir() -> PathBuf {
env::var("ZDOTDIR") env::var("ZDOTDIR")
.map(|p| Path::new(&p).join(".zshrc")) .map(PathBuf::from)
.unwrap_or_else(|_| base_dirs.home_dir().join(".zshrc")) .unwrap_or_else(|_| HOME_DIR.clone())
} }
pub fn run_antibody(run_type: RunType) -> Result<()> { pub fn zshrc() -> PathBuf {
zdotdir().join(".zshrc")
}
pub fn run_antidote(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?;
let mut antidote = zdotdir().join(".antidote").require()?;
antidote.push("antidote.zsh");
print_separator("antidote");
ctx.run_type()
.execute(zsh)
.arg("-c")
.arg(format!("source {} && antidote update", antidote.display()))
.status_checked()
}
pub fn run_antibody(ctx: &ExecutionContext) -> Result<()> {
require("zsh")?; require("zsh")?;
let antibody = require("antibody")?; let antibody = require("antibody")?;
print_separator("antibody"); print_separator("antibody");
run_type.execute(antibody).arg("update").check_run() ctx.run_type().execute(antibody).arg("update").status_checked()
} }
pub fn run_antigen(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { pub fn run_antigen(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?; let zsh = require("zsh")?;
let zshrc = zshrc(base_dirs).require()?; let zshrc = zshrc().require()?;
env::var("ADOTDIR") env::var("ADOTDIR")
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|_| base_dirs.home_dir().join("antigen.zsh")) .unwrap_or_else(|_| HOME_DIR.join("antigen.zsh"))
.require()?; .require()?;
print_separator("antigen"); print_separator("antigen");
let cmd = format!("source {} && (antigen selfupdate ; antigen update)", zshrc.display()); let cmd = format!("source {} && (antigen selfupdate ; antigen update)", zshrc.display());
run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).check_run() ctx.run_type()
.execute(zsh)
.args(["-l", "-c", cmd.as_str()])
.status_checked()
} }
pub fn run_zgenom(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { pub fn run_zgenom(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?; let zsh = require("zsh")?;
let zshrc = zshrc(base_dirs).require()?; let zshrc = zshrc().require()?;
env::var("ZGEN_SOURCE") env::var("ZGEN_SOURCE")
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|_| base_dirs.home_dir().join(".zgenom")) .unwrap_or_else(|_| HOME_DIR.join(".zgenom"))
.require()?; .require()?;
print_separator("zgenom"); print_separator("zgenom");
let cmd = format!("source {} && zgenom selfupdate && zgenom update", zshrc.display()); let cmd = format!("source {} && zgenom selfupdate && zgenom update", zshrc.display());
run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).check_run() ctx.run_type()
.execute(zsh)
.args(["-l", "-c", cmd.as_str()])
.status_checked()
} }
pub fn run_zplug(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { pub fn run_zplug(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?; let zsh = require("zsh")?;
zshrc(base_dirs).require()?; zshrc().require()?;
env::var("ZPLUG_HOME") env::var("ZPLUG_HOME")
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|_| base_dirs.home_dir().join(".zplug")) .unwrap_or_else(|_| HOME_DIR.join(".zplug"))
.require()?; .require()?;
print_separator("zplug"); print_separator("zplug");
run_type.execute(zsh).args(["-i", "-c", "zplug update"]).check_run() ctx.run_type()
.execute(zsh)
.args(["-i", "-c", "zplug update"])
.status_checked()
} }
pub fn run_zinit(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { pub fn run_zinit(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?; let zsh = require("zsh")?;
let zshrc = zshrc(base_dirs).require()?; let zshrc = zshrc().require()?;
env::var("ZINIT_HOME") env::var("ZINIT_HOME")
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|_| base_dirs.home_dir().join(".zinit")) .unwrap_or_else(|_| XDG_DIRS.data_dir().join("zinit"))
.require()?; .require()?;
print_separator("zinit"); print_separator("zinit");
let cmd = format!("source {} && zinit self-update && zinit update --all", zshrc.display(),); let cmd = format!("source {} && zinit self-update && zinit update --all", zshrc.display());
run_type.execute(zsh).args(["-i", "-c", cmd.as_str()]).check_run() ctx.run_type()
.execute(zsh)
.args(["-i", "-c", cmd.as_str()])
.status_checked()
} }
pub fn run_zi(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { pub fn run_zi(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?; let zsh = require("zsh")?;
let zshrc = zshrc(base_dirs).require()?; let zshrc = zshrc().require()?;
base_dirs.home_dir().join(".zi").require()?; HOME_DIR.join(".zi").require()?;
print_separator("zi"); print_separator("zi");
let cmd = format!("source {} && zi self-update && zi update --all", zshrc.display(),); let cmd = format!("source {} && zi self-update && zi update --all", zshrc.display());
run_type.execute(zsh).args(["-i", "-c", &cmd]).check_run() ctx.run_type().execute(zsh).args(["-i", "-c", &cmd]).status_checked()
} }
pub fn run_zim(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { pub fn run_zim(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?; let zsh = require("zsh")?;
env::var("ZIM_HOME") env::var("ZIM_HOME")
.or_else(|_| { .or_else(|_| {
Command::new("zsh") Command::new("zsh")
// TODO: Should these be quoted?
.args(["-c", "[[ -n ${ZIM_HOME} ]] && print -n ${ZIM_HOME}"]) .args(["-c", "[[ -n ${ZIM_HOME} ]] && print -n ${ZIM_HOME}"])
.check_output() .output_checked_utf8()
.map(|o| o.stdout)
}) })
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|_| base_dirs.home_dir().join(".zim")) .unwrap_or_else(|_| HOME_DIR.join(".zim"))
.require()?; .require()?;
print_separator("zim"); print_separator("zim");
run_type ctx.run_type()
.execute(zsh) .execute(zsh)
.args(["-i", "-c", "zimfw upgrade && zimfw update"]) .args(["-i", "-c", "zimfw upgrade && zimfw update"])
.check_run() .status_checked()
} }
pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> { pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
require("zsh")?; require("zsh")?;
let oh_my_zsh = ctx.base_dirs().home_dir().join(".oh-my-zsh").require()?;
// When updating `oh-my-zsh` on a remote machine through topgrade, the
// following processes will be created:
//
// SSH -> ZSH -> ZSH ($SHELL) -> topgrade -> ZSH
//
// The first ZSH process, won't source zshrc (as it is a login shell),
// and thus it won't have the ZSH environment variable, as a result, the
// children processes won't get it either, so we source the zshrc and set
// the ZSH variable for topgrade here.
if ctx.under_ssh() {
let res_env_zsh = Command::new("zsh")
.args(["-ic", "print -rn -- ${ZSH:?}"])
.output_checked_utf8();
// this command will fail if `ZSH` is not set
if let Ok(output) = res_env_zsh {
let env_zsh = output.stdout;
debug!("Oh-my-zsh: under SSH, setting ZSH={}", env_zsh);
env::set_var("ZSH", env_zsh);
}
}
let oh_my_zsh = env::var("ZSH")
.map(PathBuf::from)
// default to `~/.oh-my-zsh`
.unwrap_or(HOME_DIR.join(".oh-my-zsh"))
.require()?;
print_separator("oh-my-zsh"); print_separator("oh-my-zsh");
let custom_dir = env::var::<_>("ZSH_CUSTOM") let custom_dir = env::var::<_>("ZSH_CUSTOM")
.or_else(|_| { .or_else(|_| {
Command::new("zsh") Command::new("zsh")
// TODO: Should these be quoted?
.args(["-c", "test $ZSH_CUSTOM && echo -n $ZSH_CUSTOM"]) .args(["-c", "test $ZSH_CUSTOM && echo -n $ZSH_CUSTOM"])
.check_output() .output_checked_utf8()
.map(|o| o.stdout)
}) })
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let default_path = oh_my_zsh.join("custom"); let default_path = oh_my_zsh.join("custom");
debug!( debug!(
"Running zsh returned {}. Using default path: {}", "Running zsh returned {e}. Using default path: {}",
e,
default_path.display() default_path.display()
); );
default_path default_path
@@ -151,22 +218,20 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
debug!("oh-my-zsh custom dir: {}", custom_dir.display()); debug!("oh-my-zsh custom dir: {}", custom_dir.display());
let mut custom_repos = Repositories::new(ctx.git()); let mut custom_repos = RepoStep::try_new()?;
for entry in WalkDir::new(custom_dir).max_depth(2) { for entry in WalkDir::new(custom_dir).max_depth(2) {
let entry = entry?; let entry = entry?;
custom_repos.insert_if_repo(entry.path()); custom_repos.insert_if_repo(entry.path());
} }
custom_repos.remove(&oh_my_zsh.to_string_lossy()); custom_repos.remove(&oh_my_zsh);
if !custom_repos.is_empty() {
println!("Pulling custom plugins and themes");
ctx.git().multi_pull(&custom_repos, ctx)?;
}
ctx.run_type() ctx.run_type()
.execute("zsh") .execute("zsh")
.env("ZSH", &oh_my_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
.check_run_with_codes(&[80]) // in this update.
// See this comment: https://github.com/r-darwish/topgrade/issues/569#issuecomment-736756731
// for more information.
.status_checked_with_codes(&[80])
} }

125
src/sudo.rs Normal file
View File

@@ -0,0 +1,125 @@
use std::ffi::OsStr;
use std::path::Path;
use std::path::PathBuf;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use serde::Deserialize;
use strum::AsRefStr;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::executor::Executor;
use crate::terminal::print_separator;
use crate::utils::which;
#[derive(Clone, Debug)]
pub struct Sudo {
/// The path to the `sudo` binary.
path: PathBuf,
/// The type of program being used as `sudo`.
kind: SudoKind,
}
impl Sudo {
/// Get the `sudo` binary for this platform.
pub fn detect() -> Option<Self> {
which("doas")
.map(|p| (p, SudoKind::Doas))
.or_else(|| which("sudo").map(|p| (p, SudoKind::Sudo)))
.or_else(|| which("gsudo").map(|p| (p, SudoKind::Gsudo)))
.or_else(|| which("pkexec").map(|p| (p, SudoKind::Pkexec)))
.or_else(|| which("please").map(|p| (p, SudoKind::Please)))
.map(|(path, kind)| Self { path, kind })
}
/// Create Sudo from SudoKind, if found in the system
pub fn new(kind: SudoKind) -> Option<Self> {
which(kind.as_ref()).map(|path| Self { path, kind })
}
/// Elevate permissions with `sudo`.
///
/// This helps prevent blocking `sudo` prompts from stopping the run in the middle of a
/// step.
///
/// See: https://github.com/topgrade-rs/topgrade/issues/205
pub fn elevate(&self, ctx: &ExecutionContext) -> Result<()> {
print_separator("Sudo");
let mut cmd = ctx.run_type().execute(self);
match self.kind {
SudoKind::Doas => {
// `doas` doesn't have anything like `sudo -v` to cache credentials,
// so we just execute a dummy `echo` command so we have something
// unobtrusive to run.
// See: https://man.openbsd.org/doas
cmd.arg("echo");
}
SudoKind::Sudo => {
// From `man sudo` on macOS:
// -v, --validate
// Update the user's cached credentials, authenticating the user
// if necessary. For the sudoers plugin, this extends the sudo
// timeout for another 5 minutes by default, but does not run a
// command. Not all security policies support cached credentials.
cmd.arg("-v");
}
SudoKind::Gsudo => {
// Shows current user, cache and console status.
// See: https://gerardog.github.io/gsudo/docs/usage
cmd.arg("status");
}
SudoKind::Pkexec => {
// I don't think this does anything; `pkexec` usually asks for
// authentication every time, although it can be configured
// differently.
//
// See the note for `doas` above.
//
// See: https://linux.die.net/man/1/pkexec
cmd.arg("echo");
}
SudoKind::Please => {
// From `man please`
// -w, --warm
// Warm the access token and exit.
cmd.arg("-w");
}
}
cmd.status_checked().wrap_err("Failed to elevate permissions")
}
/// Execute a command with `sudo`.
pub fn execute_elevated(&self, ctx: &ExecutionContext, command: &Path, interactive: bool) -> Executor {
let mut cmd = ctx.run_type().execute(self);
if let SudoKind::Sudo = self.kind {
cmd.arg("--preserve-env=DIFFPROG");
}
if interactive {
cmd.arg("-i");
}
cmd.arg(command);
cmd
}
}
#[derive(Clone, Copy, Debug, Deserialize, AsRefStr)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum SudoKind {
Doas,
Sudo,
Gsudo,
Pkexec,
Please,
}
impl AsRef<OsStr> for Sudo {
fn as_ref(&self) -> &OsStr {
self.path.as_ref()
}
}

View File

@@ -1,24 +1,23 @@
use std::cmp::{max, min}; use std::cmp::{max, min};
use std::env; use std::env;
use std::io::{self, Write}; use std::io::{self, Write};
#[cfg(target_os = "linux")]
use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use std::sync::Mutex; use std::sync::Mutex;
use std::time::Duration; use std::time::Duration;
use chrono::{Local, Timelike}; use chrono::{Local, Timelike};
use color_eyre::eyre;
use color_eyre::eyre::Context;
use console::{style, Key, Term}; use console::{style, Key, Term};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::{debug, error};
#[cfg(target_os = "macos")]
use notify_rust::{Notification, Timeout}; use notify_rust::{Notification, Timeout};
use rust_i18n::t;
use tracing::{debug, error};
#[cfg(windows)] #[cfg(windows)]
use which_crate::which; use which_crate::which;
use crate::command::CommandExt;
use crate::report::StepResult; use crate::report::StepResult;
#[cfg(target_os = "linux")]
use crate::utils::which;
lazy_static! { lazy_static! {
static ref TERMINAL: Mutex<Terminal> = Mutex::new(Terminal::new()); static ref TERMINAL: Mutex<Terminal> = Mutex::new(Terminal::new());
@@ -34,13 +33,8 @@ pub fn shell() -> &'static str {
which("pwsh").map(|_| "pwsh").unwrap_or("powershell") which("pwsh").map(|_| "pwsh").unwrap_or("powershell")
} }
pub fn run_shell() { pub fn run_shell() -> eyre::Result<()> {
Command::new(shell()) Command::new(shell()).env("IN_TOPGRADE", "1").status_checked()
.env("IN_TOPGRADE", "1")
.spawn()
.unwrap()
.wait()
.unwrap();
} }
struct Terminal { struct Terminal {
@@ -50,8 +44,6 @@ struct Terminal {
set_title: bool, set_title: bool,
display_time: bool, display_time: bool,
desktop_notification: bool, desktop_notification: bool,
#[cfg(target_os = "linux")]
notify_send: Option<PathBuf>,
} }
impl Terminal { impl Terminal {
@@ -61,13 +53,11 @@ impl Terminal {
width: term.size_checked().map(|(_, w)| w), width: term.size_checked().map(|(_, w)| w),
term, term,
prefix: env::var("TOPGRADE_PREFIX") prefix: env::var("TOPGRADE_PREFIX")
.map(|prefix| format!("({}) ", prefix)) .map(|prefix| format!("({prefix}) "))
.unwrap_or_else(|_| String::new()), .unwrap_or_else(|_| String::new()),
set_title: true, set_title: true,
display_time: true, display_time: true,
desktop_notification: false, desktop_notification: false,
#[cfg(target_os = "linux")]
notify_send: which("notify-send"),
} }
} }
@@ -83,33 +73,18 @@ impl Terminal {
self.display_time = display_time self.display_time = display_time
} }
#[allow(unused_variables)]
fn notify_desktop<P: AsRef<str>>(&self, message: P, timeout: Option<Duration>) { fn notify_desktop<P: AsRef<str>>(&self, message: P, timeout: Option<Duration>) {
debug!("Desktop notification: {}", message.as_ref()); debug!("Desktop notification: {}", message.as_ref());
cfg_if::cfg_if! { let mut notification = Notification::new();
if #[cfg(target_os = "macos")] { notification
let mut notification = Notification::new(); .summary("Topgrade")
notification.summary("Topgrade") .body(message.as_ref())
.body(message.as_ref()) .appname("topgrade");
.appname("topgrade");
if let Some(timeout) = timeout { if let Some(timeout) = timeout {
notification.timeout(Timeout::Milliseconds(timeout.as_millis() as u32)); notification.timeout(Timeout::Milliseconds(timeout.as_millis() as u32));
}
notification.show().ok();
} else if #[cfg(target_os = "linux")] {
if let Some(ns) = self.notify_send.as_ref() {
let mut command = Command::new(ns);
if let Some(timeout) = timeout {
command.arg("-t");
command.arg(format!("{}", timeout.as_millis()));
}
command.args(["-a", "Topgrade", "Topgrade"]);
command.arg(message.as_ref());
command.output().ok();
}
}
} }
notification.show().ok();
} }
fn print_separator<P: AsRef<str>>(&mut self, message: P) { fn print_separator<P: AsRef<str>>(&mut self, message: P) {
@@ -142,7 +117,7 @@ impl Terminal {
.write_fmt(format_args!( .write_fmt(format_args!(
"{}\n", "{}\n",
style(format_args!( style(format_args!(
"\n―― {} {:^border$}", "\n── {} {:^border$}",
message, message,
"", "",
border = max( border = max(
@@ -158,11 +133,24 @@ impl Terminal {
.ok(); .ok();
} }
None => { None => {
self.term.write_fmt(format_args!("―― {} ――\n", message)).ok(); self.term.write_fmt(format_args!("―― {message} ――\n")).ok();
} }
} }
} }
#[allow(dead_code)]
fn print_error<P: AsRef<str>, Q: AsRef<str>>(&mut self, key: Q, message: P) {
let key = key.as_ref();
let message = message.as_ref();
self.term
.write_fmt(format_args!(
"{} {}",
style(format!("{}", t!("{key} failed:", key = key))).red().bold(),
message
))
.ok();
}
#[allow(dead_code)] #[allow(dead_code)]
fn print_warning<P: AsRef<str>>(&mut self, message: P) { fn print_warning<P: AsRef<str>>(&mut self, message: P) {
let message = message.as_ref(); let message = message.as_ref();
@@ -187,10 +175,10 @@ impl Terminal {
"{}: {}\n", "{}: {}\n",
key, key,
match result { match result {
StepResult::Success => format!("{}", style("OK").bold().green()), StepResult::Success => format!("{}", style(t!("OK")).bold().green()),
StepResult::Failure => format!("{}", style("FAILED").bold().red()), StepResult::Failure => format!("{}", style(t!("FAILED")).bold().red()),
StepResult::Ignored => format!("{}", style("IGNORED").bold().yellow()), StepResult::Ignored => format!("{}", style(t!("IGNORED")).bold().yellow()),
StepResult::Skipped(reason) => format!("{}: {}", style("SKIPPED").bold().blue(), reason), StepResult::Skipped(reason) => format!("{}: {}", style(t!("SKIPPED")).bold().blue(), reason),
} }
)) ))
.ok(); .ok();
@@ -201,7 +189,7 @@ impl Terminal {
self.term self.term
.write_fmt(format_args!( .write_fmt(format_args!(
"{}", "{}",
style(format!("{} (y)es/(N)o", question,)).yellow().bold() style(format!("{question} {}", t!("(Y)es/(N)o"))).yellow().bold()
)) ))
.ok(); .ok();
@@ -214,40 +202,47 @@ impl Terminal {
} }
} }
#[allow(unused_variables)] #[allow(unused_variables)]
fn should_retry(&mut self, interrupted: bool, step_name: &str) -> Result<bool, io::Error> { fn should_retry(&mut self, interrupted: bool, step_name: &str) -> eyre::Result<bool> {
if self.width.is_none() { if self.width.is_none() {
return Ok(false); return Ok(false);
} }
if self.set_title { if self.set_title {
self.term.set_title("Topgrade - Awaiting user"); self.term.set_title(format!("Topgrade - {}", t!("Awaiting user")));
} }
self.notify_desktop(format!("{} failed", step_name), None); if self.desktop_notification {
self.notify_desktop(format!("{}", t!("{step_name} failed", step_name = step_name)), None);
}
self.term let prompt_inner = style(format!("{}{}", self.prefix, t!("Retry? (y)es/(N)o/(s)hell/(q)uit")))
.write_fmt(format_args!( .yellow()
"\n{}", .bold();
style(format!("{}Retry? (y)es/(N)o/(s)hell/(q)uit", self.prefix))
.yellow() self.term.write_fmt(format_args!("\n{prompt_inner}")).ok();
.bold()
))
.ok();
let answer = loop { let answer = loop {
match self.term.read_key() { match self.term.read_key() {
Ok(Key::Char('y')) | Ok(Key::Char('Y')) => break Ok(true), Ok(Key::Char('y')) | Ok(Key::Char('Y')) => break Ok(true),
Ok(Key::Char('s')) | Ok(Key::Char('S')) => { Ok(Key::Char('s')) | Ok(Key::Char('S')) => {
println!("\n\nDropping you to shell. Fix what you need and then exit the shell.\n"); println!(
run_shell(); "\n\n{}\n",
break Ok(true); 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')) | Ok(Key::Char('N')) | Ok(Key::Enter) => break Ok(false),
Err(e) => { Err(e) => {
error!("Error reading from terminal: {}", e); error!("Error reading from terminal: {}", e);
break Ok(false); break Ok(false);
} }
Ok(Key::Char('q')) | Ok(Key::Char('Q')) => return Err(io::Error::from(io::ErrorKind::Interrupted)), Ok(Key::Char('q')) | Ok(Key::Char('Q')) => {
return Err(io::Error::from(io::ErrorKind::Interrupted)).context("Quit from user input")
}
_ => (), _ => (),
} }
}; };
@@ -268,7 +263,7 @@ impl Default for Terminal {
} }
} }
pub fn should_retry(interrupted: bool, step_name: &str) -> Result<bool, io::Error> { pub fn should_retry(interrupted: bool, step_name: &str) -> eyre::Result<bool> {
TERMINAL.lock().unwrap().should_retry(interrupted, step_name) TERMINAL.lock().unwrap().should_retry(interrupted, step_name)
} }
@@ -276,6 +271,11 @@ 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)
}
#[allow(dead_code)] #[allow(dead_code)]
pub fn print_warning<P: AsRef<str>>(message: P) { pub fn print_warning<P: AsRef<str>>(message: P) {
TERMINAL.lock().unwrap().print_warning(message) TERMINAL.lock().unwrap().print_warning(message)

View File

@@ -1,46 +1,22 @@
use crate::error::{SkipStep, TopgradeError};
use anyhow::Result;
use log::{debug, error};
use std::env; use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fmt::Debug; use std::fmt::Debug;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{ExitStatus, Output}; use std::process::Command;
pub trait Check { use color_eyre::eyre::Result;
fn check(self) -> Result<()>; use rust_i18n::t;
}
impl Check for Output { use tracing::{debug, error};
fn check(self) -> Result<()> { use tracing_subscriber::layer::SubscriberExt;
self.status.check() use tracing_subscriber::reload::{Handle, Layer};
} use tracing_subscriber::util::SubscriberInitExt;
} use tracing_subscriber::{fmt, Registry};
use tracing_subscriber::{registry, EnvFilter};
pub trait CheckWithCodes { use crate::command::CommandExt;
fn check_with_codes(self, codes: &[i32]) -> Result<()>; use crate::config::DEFAULT_LOG_LEVEL;
} use crate::error::SkipStep;
// Anything that implements CheckWithCodes also implements check
// if check_with_codes is given an empty array of codes to check
impl<T: CheckWithCodes> Check for T {
fn check(self) -> Result<()> {
self.check_with_codes(&[])
}
}
impl CheckWithCodes for ExitStatus {
fn check_with_codes(self, codes: &[i32]) -> Result<()> {
// Set the default to be -1 because the option represents a signal termination
let code = self.code().unwrap_or(-1);
if self.success() || codes.contains(&code) {
Ok(())
} else {
Err(TopgradeError::ProcessFailed(self).into())
}
}
}
pub trait PathExt pub trait PathExt
where where
@@ -76,7 +52,11 @@ where
debug!("Path {:?} exists", self.as_ref()); debug!("Path {:?} exists", self.as_ref());
Ok(self) Ok(self)
} else { } 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())
} }
} }
} }
@@ -102,13 +82,6 @@ pub fn which<T: AsRef<OsStr> + Debug>(binary_name: T) -> Option<PathBuf> {
} }
} }
pub fn sudo() -> Option<PathBuf> {
which("doas")
.or_else(|| which("sudo"))
.or_else(|| which("gsudo"))
.or_else(|| which("pkexec"))
}
pub fn editor() -> Vec<String> { pub fn editor() -> Vec<String> {
env::var("EDITOR") env::var("EDITOR")
.unwrap_or_else(|_| String::from(if cfg!(windows) { "notepad" } else { "vi" })) .unwrap_or_else(|_| String::from(if cfg!(windows) { "notepad" } else { "vi" }))
@@ -124,9 +97,14 @@ pub fn require<T: AsRef<OsStr> + Debug>(binary_name: T) -> Result<PathBuf> {
Ok(path) Ok(path)
} }
Err(e) => match e { Err(e) => match e {
which_crate::Error::CannotFindBinaryPath => { which_crate::Error::CannotFindBinaryPath => Err(SkipStep(format!(
Err(SkipStep(format!("Cannot find {:?} in PATH", &binary_name)).into()) "{}",
} t!(
"Cannot find {binary_name} in PATH",
binary_name = format!("{:?}", &binary_name)
)
))
.into()),
_ => { _ => {
panic!("Detecting {:?} failed: {}", &binary_name, e); panic!("Detecting {:?} failed: {}", &binary_name, e);
} }
@@ -142,3 +120,165 @@ pub fn require_option<T>(option: Option<T>, cause: String) -> Result<T> {
Err(SkipStep(cause).into()) Err(SkipStep(cause).into())
} }
} }
pub fn string_prepend_str(string: &mut String, s: &str) {
let mut new_string = String::with_capacity(string.len() + s.len());
new_string.push_str(s);
new_string.push_str(string);
*string = new_string;
}
#[cfg(target_family = "unix")]
pub fn hostname() -> Result<String> {
match nix::unistd::gethostname() {
Ok(os_str) => Ok(os_str
.into_string()
.map_err(|_| SkipStep(t!("Failed to get a UTF-8 encoded hostname").into()))?),
Err(e) => Err(e.into()),
}
}
#[cfg(target_family = "windows")]
pub fn hostname() -> Result<String> {
Command::new("hostname")
.output_checked_utf8()
.map_err(|err| SkipStep(t!("Failed to get hostname: {err}", err = err).to_string()).into())
.map(|output| output.stdout.trim().to_owned())
}
pub mod merge_strategies {
use merge::Merge;
use crate::config::Commands;
/// Prepends right to left (both Option<Vec<T>>)
pub fn vec_prepend_opt<T>(left: &mut Option<Vec<T>>, right: Option<Vec<T>>) {
if let Some(left_vec) = left {
if let Some(mut right_vec) = right {
right_vec.append(left_vec);
let _ = std::mem::replace(left, Some(right_vec));
}
} else {
*left = right;
}
}
/// Appends an Option<String> to another Option<String>
pub fn string_append_opt(left: &mut Option<String>, right: Option<String>) {
if let Some(left_str) = left {
if let Some(right_str) = right {
left_str.push(' ');
left_str.push_str(&right_str);
}
} else {
*left = right;
}
}
pub fn inner_merge_opt<T>(left: &mut Option<T>, right: Option<T>)
where
T: Merge,
{
if let Some(ref mut left_inner) = left {
if let Some(right_inner) = right {
left_inner.merge(right_inner);
}
} else {
*left = right;
}
}
pub fn commands_merge_opt(left: &mut Option<Commands>, right: Option<Commands>) {
if let Some(ref mut left_inner) = left {
if let Some(right_inner) = right {
left_inner.extend(right_inner);
}
} else {
*left = right;
}
}
}
// Skip causes
// TODO: Put them in a better place when we have more of them
pub 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.
///
/// # Shim
/// On Windows, if you install `python` through `winget`, an actual `python`
/// is installed as well as a `python3` shim. Shim is invokable, but when you
/// execute it, the Microsoft App Store will be launched instead of a Python
/// shell.
///
/// We do this check through `python -V`, a shim will just give `Python` with
/// no version number.
pub fn check_is_python_2_or_shim(python: PathBuf) -> Result<PathBuf> {
let output = Command::new(&python).arg("-V").output_checked_utf8()?;
// "Python x.x.x\n"
let stdout = output.stdout;
// ["Python"] or ["Python", "x.x.x"], the newline char is trimmed.
let mut split = stdout.split_whitespace();
if let Some(version) = split.nth(1) {
let major_version = version
.split('.')
.next()
.expect("Should have a major version number")
.parse::<u32>()
.expect("Major version should be a valid number");
if major_version == 2 {
return Err(SkipStep(t!("{python} is a Python 2, skip.", python = python.display()).to_string()).into());
}
} else {
// No version number, is a shim
return Err(SkipStep(t!("{python} is a Python shim, skip.", python = python.display()).to_string()).into());
}
Ok(python)
}
/// Set up the tracing logger
///
/// # Return value
/// A reload handle will be returned so that we can change the log level at
/// runtime.
pub fn install_tracing(filter_directives: &str) -> Result<Handle<EnvFilter, Registry>> {
let env_filter = EnvFilter::try_new(filter_directives)
.or_else(|_| EnvFilter::try_from_default_env())
.or_else(|_| EnvFilter::try_new(DEFAULT_LOG_LEVEL))?;
let fmt_layer = fmt::layer().with_target(false).without_time();
let (filter, reload_handle) = Layer::new(env_filter);
registry().with(filter).with(fmt_layer).init();
Ok(reload_handle)
}
/// Update the tracing logger with new `filter_directives`.
pub fn update_tracing(reload_handle: &Handle<EnvFilter, Registry>, filter_directives: &str) -> Result<()> {
let new = EnvFilter::try_new(filter_directives)
.or_else(|_| EnvFilter::try_from_default_env())
.or_else(|_| EnvFilter::try_new(DEFAULT_LOG_LEVEL))?;
reload_handle.modify(|old| *old = new)?;
Ok(())
}
/// Set up the error handler crate
pub fn install_color_eyre() -> Result<()> {
color_eyre::config::HookBuilder::new()
// Don't display the backtrace reminder by default:
// Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
// Run with RUST_BACKTRACE=full to include source snippets.
.display_env_section(false)
// Display location information by default:
// Location:
// src/steps.rs:92
.display_location_section(true)
.install()
}

View File

@@ -1,80 +0,0 @@
.hy
.TH "topgrade" "8"
.SH NAME
.PP
Topgrade \- Upgrade everything
.SH SYNOPSIS
.PP
topgrade [\fIoptions\f[]]
.SH DESCRIPTION
.PP
Keeping your system up to date usually involves invoking multiple package managers.
This results in big, non-portable shell one-liners saved in your shell.
To remedy this, \fBTopgrade\fR detects which tools you use and runs the appropriate commands to update them.
.SH OPTIONS
.TP
.B \-\-only <only>
Run only specific steps
.RS
.RE
.TP
.B \-\-disable <disable>
Disable specific steps
.RS
.RE
.TP
.B \-c, \-\-cleanup
Cleanup temporary or old files
.RS
.RE
.TP
.B \-n, \-\-dry\-run
List the commands that would be run
.RS
.RE
.TP
.B \-\-edit\-config
Edit the configuration file
.RS
.RE
.TP
.B \-h, \-\-help
Print help information
.RS
.RE
.TP
.B \-k, \-\-keep
Prompt for a key before exiting
.RS
.RE
.TP
.B \-\-no\-retry
Do not ask to retry failed steps
.RS
.RE
.TP
.B \-t, \-\-tmux
Run inside tmux
.RS
.RE
.TP
.B \-V, \-\-version
Print version information
.RS
.RE
.TP
.B \-v, \-\-verbose
Output logs
.RS
.RE
.B \-y, \-\-yes
Skip package manager's prompts (experimental)
.SH ARGUMENT FORMAT
Options can be given in any order.
A list of steps must be provided as a list of separate arguments, i.e. 'topgrade --only system shell'.
.SH BUGS
For a list of bugs see <\fIhttps://github.com/r-darwish/topgrade/issues\fR>.
.SH AUTHOR
\fBTopgrade\fR is maintained by Roey Dror (\[aq]r\-darwish\[aq]) and many other contributors.
You can view the full list at
<\fIhttps://github.com/r-darwish/topgrade/graphs/contributors\fR>