Compare commits

...

371 Commits
v15.0.0 ... v17

Author SHA1 Message Date
GideonBear
5bd5f10f0c chore(deps): update clap-cargo to v0.18.x 2025-11-23 13:33:53 +01:00
GideonBear
bfaca61deb chore(deps): update base64ct, globset, ignore 2025-11-23 13:26:47 +01:00
GideonBear
b00c4c1503 chore: upgrade to edition 2024
Co-authored-by: Ehren Bendler <a5ehren@gmail.com>
2025-11-23 13:26:47 +01:00
GideonBear
54a722be21 chore(deb): update copyright
Co-authored-by: Ehren Bendler <a5ehren@gmail.com>
2025-11-23 13:26:47 +01:00
GideonBear
bf4555ed29 chore(deps): unpin toml
Co-authored-by: Ehren Bendler <a5ehren@gmail.com>
2025-11-23 13:26:47 +01:00
GideonBear
46eb74109e style: apply new clippy fixes 2025-11-23 13:26:47 +01:00
GideonBear
0bde798b4a chore(deps): bump etcetera to 0.11.0 2025-11-23 13:26:47 +01:00
GideonBear
ebb4448950 chore(deps): remove temporary transitive dependency pins 2025-11-23 13:26:46 +01:00
GideonBear
b2b51bc8d2 refactor: replace home dependency with std::env::home_dir 2025-11-23 13:26:06 +01:00
GideonBear
8840a273b4 chore!: bump MSRV to 1.87.0 2025-11-23 13:26:06 +01:00
Filip Czaplicki
50e55dea77 feat: add colors to --help/-h (#1553) 2025-11-23 13:15:50 +01:00
Rubin Bhandari
f7c9e42066 feat(mise): add support for parallel job configuration in mise (#1548)
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
2025-11-21 09:08:40 +01:00
Daniil Kulchenko
ef3ee7bea7 feat(brew): add Homebrew cask support for Linux (#1539) 2025-11-20 20:10:47 +01:00
renovate[bot]
8eb300c4fb chore(deps): update rust crate indexmap to v2.12.1 (#1550)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 19:33:19 +01:00
renovate[bot]
b7b99a725c chore(deps): update actions/checkout action to v6 (#1551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 19:31:17 +01:00
Gideon
df090a89c4 docs: Add metadata to Python package for PyPI (#1549) 2025-11-20 19:25:56 +01:00
LILAY
856fa0ed5b docs(installation): update copr repo info in readme (#1545) 2025-11-20 17:28:39 +01:00
Rubin Bhandari
9bb5a680ac feat(mise): add mise configuration options for bump and interactive modes (#1546) 2025-11-20 17:28:13 +01:00
github-actions[bot]
2d40f7bdb3 chore: release v16.4.2 (#1544)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-20 14:23:57 +01:00
renovate[bot]
06b210d1c9 chore(deps): update dawidd6/action-homebrew-bump-formula action to v6 (#1543)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 14:22:19 +01:00
github-actions[bot]
e0e714e7b5 chore: release v16.4.1 (#1542)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-20 10:09:17 +01:00
Gideon
dfb8342d8b refactor: refactor run_containers error handling (#1541) 2025-11-20 10:08:15 +01:00
github-actions[bot]
9d662e36a1 chore: release v16.4.0 (#1528)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-20 09:23:51 +01:00
Gideon
45159c29fe chore(deps): update clap, clap_builder, clap_complete (#1540) 2025-11-20 09:09:07 +01:00
renovate[bot]
a27c68a1dd chore(deps): update github/codeql-action action to v4.31.4 (#1531)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 08:58:33 +01:00
Wang Bing-hua
cdee1c14d9 test(config): add custom commands order test (#1536) 2025-11-19 18:37:55 +01:00
Wang Bing-hua
8de6a36d86 fix(deps): restore custom commands order (#1535) 2025-11-19 16:38:54 +01:00
Gideon
b03a8d53bb refactor: make Config methods more consistent by utilizing #[derive(Default)] (#1534) 2025-11-19 15:00:18 +01:00
Gideon
b0510cdade docs(issue templates): use issue types (#1533) 2025-11-19 11:11:30 +01:00
John Holt
7945311b4b feat(os): add Origami Linux support (#1530)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-18 09:24:10 +01:00
renovate[bot]
75de4dfd3b chore(deps): lock file maintenance (#1505)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 20:45:02 +01:00
renovate[bot]
6a838bbcb7 chore(deps): update actions/checkout digest to 93cb6ef (#1526)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 20:44:03 +01:00
renovate[bot]
ab2bab8c9b chore(deps): update actions/checkout action to v5.0.1 (#1527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 20:43:49 +01:00
Jason Stelzer
9a55278d32 feat(containers): add option to run system prune (#1523)
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
2025-11-17 20:39:54 +01:00
github-actions[bot]
f9735f3b31 chore: release v16.3.0 (#1465)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-17 16:39:39 +01:00
tranzystorekk
50be214b56 chore(license): switch license variant to GPL-3.0-or-later (#1518) 2025-11-16 19:08:49 +01:00
Ehren Bendler
9ec8e83f41 chore(deps): update some dependencies (#1512) 2025-11-15 17:05:48 +01:00
renovate[bot]
c70984d458 chore(deps): update github/codeql-action action to v4.31.3 (#1483)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 13:02:36 +01:00
Gideon
a3503c0c70 refactor: remove unnecessary cfg-if dependency (#1509) 2025-11-15 12:47:54 +01:00
Gideon
ca2d16edfd ci(lint_pr): run on synchronize, and add zizmor ignore (#1508) 2025-11-15 10:56:42 +01:00
pre-commit-ci[bot]
722b1ad09e chore(pre-commit): autoupdate (#1464)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
2025-11-15 10:55:29 +01:00
Gideon
743845a66b fix(elan): skip running elan update on elan >=4.0.0 (#1507) 2025-11-15 10:46:18 +01:00
Gideon
2594f4c0fb docs: improve issue templates (#1235)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-15 10:07:55 +01:00
Gideon
639d055f9a feat: print summary and run post commands when (q)uit is used (#1254) 2025-11-15 10:03:46 +01:00
Gideon
ea2ccdd69f chore(deps): bump mac-notification-sys, use main branch temporarily (#1506) 2025-11-15 08:58:37 +01:00
Gideon
84a50afa83 chore: replace Dependabot with Renovate (#1486) 2025-11-15 08:22:27 +01:00
renovate[bot]
b13c1bd2d7 chore(deps): lock file maintenance (#1481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: GideonBear <87426140+GideonBear@users.noreply.github.com>
2025-11-14 19:52:50 +01:00
Gideon
7749f41d56 fix(deps): Fix non-locked install on older version of Rust (#1485) 2025-11-14 19:33:56 +01:00
Gideon
593a2a33d9 fix(deps): Fix non-locked install on older version of Rust (#1482) 2025-11-14 19:20:12 +01:00
renovate[bot]
4f693aeaf3 chore(deps): pin dependencies (#1478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-14 18:35:35 +01:00
renovate[bot]
c3d34184d0 chore(deps): update actions/dependency-review-action action to v4.8.2 (#1479)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-14 18:34:49 +01:00
Gideon
4aa224de87 chore: Add Renovate (#1477) 2025-11-14 18:23:17 +01:00
Andre Toerien
320b13c06b fix(bun): skip self-update if not installed via official script (#1476)
Co-authored-by: Steve Lau <stevelauc@outlook.com>
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
2025-11-14 08:51:31 +01:00
Izzy Meyer
907d778c55 fix(openbsd): fix compilation on OpenBSD (#1473)
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
2025-11-12 18:38:41 +01:00
Stuart Reilly
f3fccb86c0 refactor: Replace main's self update with a proper step call (#1470)
Co-authored-by: Stuart Reilly <sreilly@scottlogic.com>
2025-11-11 17:31:04 +01:00
Andre Toerien
bb4afb71e9 feat: run pre_sudo before pre_commands (#1469) 2025-11-11 16:17:00 +01:00
Gideon
ec8d30f634 ci(release): Fix homebrew releases (#1468) 2025-11-11 16:12:50 +01:00
Rubin Bhandari
50d318641a feat(chezmoi): add exclude_encrypted config (#1453)
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
2025-11-10 21:12:40 +01:00
github-actions[bot]
c5267f6087 chore: release v16.2.1 (#1462)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-10 20:07:08 +01:00
Gideon
d80e8f64d1 fix(release): Use bash in Windows to fix powershell issues (#1461) 2025-11-10 19:18:51 +01:00
Gideon
c6f2e0cc44 fix(release): Fix .deb distribution (#1460) 2025-11-10 19:18:29 +01:00
Gideon
99c3e8af26 fix(release): Fix .deb distribution (#1458) 2025-11-10 18:47:55 +01:00
github-actions[bot]
30d3537c0e chore: release v16.2.0 (#1407)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-10 18:31:28 +01:00
Gideon
90cb16e3d0 fix(release): fix homebrew releases by migrating to dawidd6/action-homebrew-bump-formula (#1457) 2025-11-10 18:27:32 +01:00
Gideon
5192a0f1dc fix(mise): fix mise self-update failing when installed via a package manager (#1456) 2025-11-10 18:20:40 +01:00
Gideon
bec7edf1fc fix(release): Add man page to .deb distribution (#1455) 2025-11-10 18:12:49 +01:00
Bodebojo
051784ac0d fix(self-update): fix windows self-update reporting failure on successful self-update (#1452) 2025-11-10 17:04:03 +01:00
PandaMimo
17d715479a feat(mise): run mise self-update (#1450)
Co-authored-by: Milo <milo@example.com>
2025-11-10 14:16:49 +01:00
Bodebojo
39a90f5ebe fix(pkgfile): make pkgfile opt-in (#1449)
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
2025-11-10 14:11:03 +01:00
PandaMimo
80c4bd5065 ref: comment run_config_update (#1448)
Co-authored-by: Milo <milo@example.com>
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
2025-11-10 13:44:17 +01:00
Bodebojo
222d800a32 fix(vcpkg): fix permission denied when updating vcpkg if it's installed as root (#1447) 2025-11-10 12:22:44 +01:00
Gideon
b29699fc55 docs: Expand LLM guidelines in CONTRIBUTING.md (#1445) 2025-11-09 09:13:27 +01:00
Oliver Tzeng
fc0e5461eb fix(zh_TW): fixed zh_TW strings (#1446) 2025-11-09 08:58:45 +01:00
Gideon
75da4a709c docs: Add AI guidelines to CONTRIBUTING.md (#1444) 2025-11-08 19:42:04 +01:00
Gideon
02e388122b ref: add comments to Config::allowed_steps (#1291) 2025-11-08 11:12:25 +01:00
Gideon
02fe1087de feat(falconf): add falconf step (#1219)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-08 11:10:45 +01:00
Gideon
34b7943fd1 fix(git): fix shellexpand::tilde in git_repos in topgrade.d/* (#1223) 2025-11-08 11:10:24 +01:00
Gideon
b61886f0f9 ref(nix): Deduplicate run_nix and run_nix_self_upgrade nix --version checking (#1376) 2025-11-08 11:07:01 +01:00
Gideon
d9a8ecfd33 ref: remove commented-out library code and unnecessary bin declaration (#1373) 2025-11-08 11:05:13 +01:00
Gideon
6c68bfaf64 ref: Simplify target cfgs (#1346) 2025-11-08 11:04:52 +01:00
Gideon
5866a0570f feat(hyprpm): add hyprpm step (#1213) 2025-11-08 11:04:19 +01:00
Gideon
28f5754efd feat(doom): add doom.aot option (#1214) 2025-11-08 11:03:08 +01:00
Gideon
ea1b286c98 fix(auto-cpufreq): skip when install script is not used (#1215) 2025-11-08 11:00:03 +01:00
Gideon
22ab77de6d feat: add show_distribution_summary config option (#1259) 2025-11-08 10:58:36 +01:00
Gideon
410bd61c75 ref: tidy up binary-conflict code (#1329) 2025-11-08 10:57:31 +01:00
Gideon
07b422915c docs: Improve installation section (#1442) 2025-11-07 12:12:52 +01:00
SteveCoding125
5b9d387ef3 fix(vim): change nvimrc base_dir for windows (#1433) 2025-11-06 11:07:19 +01:00
Gideon
5b5dd27834 fix(guix): fix overcomplicated Guix step (#1290) 2025-11-06 11:05:38 +01:00
Gideon
79f65981a5 fix(gem): fix incorrectly placed debug message in gem step (#1212) 2025-11-06 11:04:20 +01:00
Gideon
a52c775247 feat(rustup): add rustup.channels config (#1206) 2025-11-06 11:04:00 +01:00
Gideon
549111db3a fix(conda): replace deprecated auto_activate_base (#1158) 2025-11-06 11:03:13 +01:00
Gideon
1572974ec4 fix(containers): fix panic in containers step (#1150) 2025-11-06 10:25:52 +01:00
Gideon
8387468607 fix(jetbrains-toolbox): fix step not dry running (#1253) 2025-11-06 10:22:28 +01:00
Gideon
94979d6b7a chore(deps): Update jetbrains-toolbox-updater (#1438) 2025-11-05 17:32:11 +01:00
Daniel Hast
6652a2aa90 ci: remove template expansion in code contexts (#1434) 2025-11-04 20:54:03 +01:00
Mag Mell
f943b220d9 feat(os): add AOSC OS support (#1424) 2025-11-04 09:10:15 +01:00
dependabot[bot]
0fc7016d68 chore(deps): bump github/codeql-action from 4.31.0 to 4.31.2 (#1427)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 16:41:28 +01:00
Daniel Hast
9e9e6c9d55 ci: don't persist credentials in actions/checkout (#1422) 2025-11-03 12:29:07 +01:00
Gideon
bafa15c8f1 docs: Improve CONTRIBUTING.md (#1420) 2025-11-02 16:40:11 +01:00
Gideon
f669de8272 docs: Update SECURITY.md (#1421) 2025-11-02 15:59:45 +01:00
Gideon
ff26835406 chore(pre-commit): Run pre-commit (#1419) 2025-11-02 15:14:18 +01:00
Stuart Reilly
99892359c7 feat: add damp run type (#1217)
Co-authored-by: Stuart Reilly <sreilly@scottlogic.com>
2025-11-02 15:06:35 +01:00
Gideon
8fc25d7fd4 ci: Enforce conventional commits in PR titles (#1418) 2025-11-02 15:01:22 +01:00
GideonBear
942bfaa708 docs: Improve contributing section 2025-11-02 12:48:28 +01:00
GideonBear
a1fd324e82 docs: Remove roadmap 2025-11-02 12:48:28 +01:00
GideonBear
62bf2c6e90 docs: Reformat README.md 2025-11-02 12:48:28 +01:00
GideonBear
9f1e0c8eef docs: Update installation methods 2025-11-02 12:48:28 +01:00
Gideon
027de7c865 chore(release): Fix dispatch error in create_release_assets.yml (#1406) 2025-11-01 19:38:10 +01:00
github-actions[bot]
266adabd13 chore: release v16.1.2 (#1400)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-01 19:18:37 +01:00
Gideon
7d2e43f83b fix(release): Fix cross-compilation for arm requiring glibc>=2.39 (#1405) 2025-11-01 19:15:54 +01:00
Gideon
294a90a7c3 fix(release): Fix FreeBSD build (#1404) 2025-11-01 16:43:22 +01:00
Gideon
af5def1551 chore(release): Fix cross trying to fmt (#1403) 2025-11-01 16:30:56 +01:00
Gideon
8c63ee6a18 fix(release): Fix FreeBSD build (#1402) 2025-11-01 16:22:00 +01:00
Gideon
ee8ae8623d fix(release): Fix manual workflow trigger (#1401) 2025-11-01 16:19:31 +01:00
Gideon
222e6b55c0 fix(release): Fix FreeBSD build and add manual workflow trigger (#1399) 2025-11-01 16:03:15 +01:00
github-actions[bot]
2050a80665 chore: release v16.1.1 (#1392)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-01 15:12:40 +01:00
Gideon
e01be14041 fix(typst): Skip typst when self-update is disabled (#1397) 2025-11-01 14:53:26 +01:00
Gideon
dd6bc580fa fix(release): Fix winget release workflow (#1395) 2025-11-01 12:18:38 +01:00
Gideon
22ef36a185 chore: Update from deprecated macos-13 to macos-15-intel (#1394) 2025-11-01 12:04:53 +01:00
Gideon
b57ceccbe1 fix(release): Fix FreeBSD release (#1393) 2025-11-01 11:25:02 +01:00
Gideon
e9d430a4e4 fix(release): Fix FreeBSD release (#1391) 2025-11-01 11:22:24 +01:00
github-actions[bot]
8a247fba95 chore: release v16.1.0 (#1389)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-01 10:11:43 +01:00
Gideon
5b6c31bd89 chore(release): switch to release-plz (#1333) 2025-10-31 19:31:37 +01:00
Gideon
c316e2af69 chore(pre-commit): Make pre-commit.ci use conventional commits (#1388) 2025-10-31 17:00:39 +01:00
Gideon
7270aa96f0 feat(deb-get): Skip non-deb-get packages by passing --dg-only (#1386) 2025-10-29 18:43:03 +01:00
pre-commit-ci[bot]
dd823eb489 chore(pre-commit): pre-commit autoupdate (#1383)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: GideonBear <87426140+GideonBear@users.noreply.github.com>
2025-10-28 09:39:19 +01:00
dependabot[bot]
cb29305385 chore(deps): bump actions/upload-artifact from 4.6.2 to 5.0.0 (#1382)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 5.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.2...v5.0.0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 5.0.0
  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>
2025-10-27 17:41:59 +01:00
dependabot[bot]
2dfa37dd0c chore(deps): bump github/codeql-action from 4.30.9 to 4.31.0 (#1379)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 17:16:36 +01:00
dependabot[bot]
b3d3284f18 chore(deps): bump actions/download-artifact from 5.0.0 to 6.0.0 (#1380)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 17:16:29 +01:00
dependabot[bot]
73e3e133c6 chore(deps): bump taiki-e/install-action from 2.62.33 to 2.62.38 (#1381)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 17:16:20 +01:00
Gideon
9828af9f03 chore(pre-commit): Fix pre-commit-config.yaml (#1378) 2025-10-27 16:54:55 +01:00
Gideon
48aa6b5ac5 fix(freshclam): run with sudo when running without sudo fails (#1118) 2025-10-27 16:44:53 +01:00
Stef
e5b3ed1461 feat(typst): add typst step (#1374) 2025-10-26 12:57:59 +01:00
Mitchell Berendhuysen
5fad9f0ec6 chore(release): Add .deb auto completion script (#1353)
Co-authored-by: GideonBear <87426140+GideonBear@users.noreply.github.com>
2025-10-25 18:02:04 +02:00
SteveCoding125
f4a5507716 fix(tldr): move tldr to be a generic step (#1370) 2025-10-21 18:51:34 +02:00
dependabot[bot]
2ea9d1d6fd chore(deps): bump github/codeql-action from 4.30.8 to 4.30.9 (#1369)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 17:45:25 +02:00
dependabot[bot]
e393b1f90b chore(deps): bump taiki-e/install-action from 2.62.28 to 2.62.33 (#1368)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 17:43:48 +02:00
Stef
2259e81bb0 feat(step): Add atuin step (#1367) 2025-10-19 14:09:40 +02:00
Jochen Schalanda
65a30292a3 feat(nix): support upgrading Determinate Nix (#1366)
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
2025-10-15 09:39:09 +02:00
dependabot[bot]
494eef3472 chore(deps): bump actions/dependency-review-action from 4.8.0 to 4.8.1 (#1362)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 18:44:00 +02:00
dependabot[bot]
bee1e865b2 chore(deps): bump softprops/action-gh-release from 2.3.4 to 2.4.1 (#1364)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 18:43:53 +02:00
dependabot[bot]
c92b049bbc chore(deps): bump taiki-e/install-action from 2.62.21 to 2.62.28 (#1363)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 18:43:48 +02:00
dependabot[bot]
c9985480fe chore(deps): bump github/codeql-action from 3.30.6 to 4.30.8 (#1365)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 18:43:45 +02:00
Chinmay Dalal
e1b5b76d8e fix(nix): fix nix upgrade command selection for profiles in XDG_STATE_HOME (#1354) 2025-10-08 18:45:57 +02:00
Peter Storch
762a74f5f9 fix(containers): Docker update fails on M Macs due to platform / (#1360)
Suppresses the `--platform` parameter to docker pull if platform is only '/'.

fixes #1154
2025-10-06 20:10:09 +02:00
dependabot[bot]
a7a2d8493e chore(deps): bump github/codeql-action from 3.30.5 to 3.30.6 (#1355)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 17:29:12 +02:00
dependabot[bot]
bc6538d209 chore(deps): bump softprops/action-gh-release from 2.3.3 to 2.3.4 (#1356)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 17:28:53 +02:00
dependabot[bot]
93d841310e chore(deps): bump taiki-e/install-action from 2.62.13 to 2.62.21 (#1357)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 17:28:49 +02:00
dependabot[bot]
2ac679f17e chore(deps): bump ossf/scorecard-action from 2.4.2 to 2.4.3 (#1358)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 17:28:39 +02:00
dependabot[bot]
9a3ef463f9 chore(deps): bump actions/dependency-review-action from 4.7.3 to 4.8.0 (#1350)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 09:15:18 +02:00
dependabot[bot]
affc5fcd75 chore(deps): bump github/codeql-action from 3.30.3 to 3.30.5 (#1349)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 09:15:06 +02:00
dependabot[bot]
96bff5e974 chore(deps): bump taiki-e/install-action from 2.62.1 to 2.62.13 (#1351)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 09:14:57 +02:00
dependabot[bot]
e828e14fa5 chore(deps): bump actions/cache from 4.2.4 to 4.3.0 (#1352)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 09:14:43 +02:00
Nils
4c6dc8ff82 Fix WSL distribution name cleanup (#1348) 2025-09-28 19:35:18 +02:00
Nils
75bd7c90d3 chore(pyproject): mark version as dynamic (#1347) 2025-09-28 19:27:30 +02:00
Andre Toerien
2aa3d94a98 feat(sudo): print warning if Windows Sudo is misconfigured 2025-09-28 19:14:45 +02:00
Andre Toerien
f4ac809dff refactor(deps): replace winapi with windows 2025-09-28 19:14:45 +02:00
Andre Toerien
0dee534f84 fix(sudo): reorder require_sudo() after print_separator() 2025-09-28 15:38:30 +02:00
Andre Toerien
ad9f2c2ccb feat(sudo): print warning if steps were skipped due to missing sudo 2025-09-28 15:38:30 +02:00
AThePeanut4
a886d20a7b refactor(sudo): rename interactive to login_shell 2025-09-28 14:57:40 +02:00
Andre Toerien
791993795a fix(sudo): use require_sudo for windows commands 2025-09-28 14:57:40 +02:00
Andre Toerien
a2afdb821f feat(sudo): add SudoKind::Null 2025-09-28 14:57:40 +02:00
Andre Toerien
47b51a8be0 feat: detect and warn if running as root 2025-09-28 14:57:40 +02:00
Andre Toerien
7c7e7c3ce4 Fix "WSL already reported" panic (#1344) 2025-09-28 08:25:10 +02:00
Andre Toerien
fec08a5ad1 Move step logic out of Powershell struct (#1345) 2025-09-27 19:55:56 +02:00
dependabot[bot]
3961ef61c8 chore(deps): bump taiki-e/install-action from 2.61.5 to 2.62.1 (#1335)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-22 17:27:49 +02:00
dependabot[bot]
84692da9a2 chore(deps): bump Swatinem/rust-cache from 2.8.0 to 2.8.1 (#1336)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-22 17:27:44 +02:00
Andre Toerien
fb7ba52e39 Fixes for #1188; custom_commands broken (#1332)
Co-authored-by: GideonBear <87426140+GideonBear@users.noreply.github.com>
2025-09-20 13:59:19 +02:00
SteveCoding125
898823abb2 use login shell when executing topgrade (#1327) 2025-09-18 17:06:59 +02:00
Rafael Scalet
ccefd0a43a feat: add --no-tmux flag (#1328) 2025-09-18 14:01:19 +02:00
dependabot[bot]
98f0be61ed chore(deps): bump taiki-e/install-action from 2.60.0 to 2.61.5 (#1325)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 18:39:56 +02:00
dependabot[bot]
6bb1d54cb0 chore(deps): bump github/codeql-action from 3.30.1 to 3.30.3 (#1324)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 18:39:49 +02:00
Nils
69a76e32b7 chore(pre-commit): add typos with conservative excludes; no content changes (#1317)
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
Co-authored-by: niStee <52573120+niStee@users.noreply.github.comclear>
2025-09-14 08:20:51 +02:00
Rafael Scalet
99d989d486 feat: add step for mandb - user and system (update man entries) (#1319) 2025-09-14 08:08:52 +02:00
Nils
456d62224e chore: fix simple typos in code and comments (split var, whether, Extensions) (#1318)
Co-authored-by: niStee <52573120+niStee@users.noreply.github.comclear>
2025-09-13 17:12:39 +02:00
Rafael Scalet
b662fae11e feat: support for pkgfile (#1306) 2025-09-10 13:31:27 +02:00
dependabot[bot]
0926bd2f6c chore(deps): bump github/codeql-action from 3.29.11 to 3.30.1 (#1301)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 17:30:51 +02:00
dependabot[bot]
3fb473ae95 chore(deps): bump softprops/action-gh-release from 2.3.2 to 2.3.3 (#1302)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 17:30:45 +02:00
dependabot[bot]
603ed18a4c chore(deps): bump taiki-e/install-action from 2.58.21 to 2.60.0 (#1303)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 17:30:40 +02:00
dependabot[bot]
0307fdd296 chore(deps): bump actions/dependency-review-action from 4.7.2 to 4.7.3 (#1304)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 17:30:34 +02:00
dependabot[bot]
c10dcdbfdb chore(deps): bump actions/attest-build-provenance from 2.4.0 to 3.0.0 (#1305)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 17:30:30 +02:00
Nils
31d8e579c6 Security: update tracing-subscriber to ~0.3.20 (ANSI escape injection fix, GHSA-xwfj-jgwm-7wp5) (#1288)
* Security: pin tracing-subscriber to 0.3.20 (fix ANSI escape injection, GHSA-xwfj-jgwm-7wp5)\n\n- Ensure tracing-subscriber is 0.3.20\n- Document reason near tracing init\n- Windows: enable winapi features (consoleapi, wincon) to fix build\n- Verified build passes locally\n\nRefs:\n- GHSA-xwfj-jgwm-7wp5

* Update src/utils.rs

Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>

* Update Cargo.toml

Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>

---------

Co-authored-by: niStee <52573120+niStee@users.noreply.github.comclear>
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
2025-08-31 20:51:08 +02:00
Rafael Scalet
7b3fec0349 feat: add "show_skipped" option in config file #1280 (#1286) 2025-08-29 11:01:40 +02:00
dependabot[bot]
e32a58f6ff chore(deps): bump github/codeql-action from 3.29.8 to 3.29.11 (#1281)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 17:38:57 +02:00
dependabot[bot]
36cd726676 chore(deps): bump actions/dependency-review-action from 4.7.1 to 4.7.2 (#1282)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 17:38:36 +02:00
dependabot[bot]
8dc08de628 chore(deps): bump taiki-e/install-action from 2.58.9 to 2.58.21 (#1283)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 17:38:04 +02:00
dependabot[bot]
0361954919 chore(deps): bump PyO3/maturin-action from 1.49.3 to 1.49.4 (#1285)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 17:37:39 +02:00
dependabot[bot]
29a62575f4 chore(deps): bump actions/cache from 4.2.3 to 4.2.4 (#1284)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 17:37:10 +02:00
uwuclxdy
547a6df2ae Support "Insiders" versions of VSCode and VSCodium (#1279) 2025-08-25 16:27:02 +02:00
Stuart Reilly
53d08cdf28 Sudo preserve env list argument is --preserve-env (#1276)
Sudo preserve-env

Sudo's preserve env list is `--preserve-env=...` not `--preserve_env=...`.

https://www.man7.org/linux/man-pages/man8/sudo.8.html
2025-08-20 17:33:35 +02:00
Ehren Bendler
a033152c60 Clippy fixes from rust 1.91 nightly (#1267) 2025-08-13 17:01:25 +02:00
dependabot[bot]
9472aaca7a chore(deps): bump actions/checkout from 4.2.2 to 5.0.0 (#1264)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 11:47:45 +02:00
dependabot[bot]
6254b99e02 chore(deps): bump actions/download-artifact from 4.3.0 to 5.0.0 (#1263)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 11:46:33 +02:00
dependabot[bot]
3d17bdb747 chore(deps): bump taiki-e/install-action from 2.58.0 to 2.58.9 (#1261)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 11:45:14 +02:00
dependabot[bot]
d2eeeb9129 chore(deps): bump ossf/scorecard-action from 2.4.0 to 2.4.2 (#1262)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 11:44:50 +02:00
dependabot[bot]
1d626e0add chore(deps): bump github/codeql-action from 3.29.5 to 3.29.8 (#1265)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 11:44:27 +02:00
Kian-Meng Ang
9f4cb8c1b6 feat: fix typos (#1221)
Co-authored-by: GideonBear <87426140+GideonBear@users.noreply.github.com>
2025-08-11 18:15:21 +02:00
Nils
91fc5e3902 chore(ci): Dependabot, workflow security (#1257)
Co-authored-by: StepSecurity Bot <bot@stepsecurity.io>
Co-authored-by: niStee <52573120+niStee@users.noreply.github.comclear>
Co-authored-by: GideonBear <87426140+GideonBear@users.noreply.github.com>
2025-08-11 10:24:18 +02:00
Falk Woldmann Lu
9048cd8f47 refactor: replace once_cell crate with std equivalent (#1260) 2025-08-11 09:57:32 +02:00
Nils
4f5e8a8836 chore(deps): bump tokio from 1.38 to 1.47 (#1256)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-09 13:55:28 +02:00
Gudsfile
29b05fa50f i18n(app.yml): fix fr language #1248 2025-08-06 20:11:56 +02:00
Andre Toerien
dbe1a5c988 refactor(sudo): add SudoKind::WinSudo 2025-08-06 15:21:51 +02:00
Andre Toerien
b6c1290934 refactor(sudo): add SudoExecuteOpts builder functions and preserve_env enum 2025-08-06 15:21:51 +02:00
Andre Toerien
306ff3c7c5 fix(sudo): prevent sudo_command = "sudo" finding gsudo 2025-08-06 15:21:51 +02:00
Andre Toerien
0e43e0d7fc refactor(yarn): remove unnecessary Yarn::yarn field 2025-08-06 15:21:51 +02:00
Andre Toerien
4da696321a refactor(apt): extract detect_apt() function 2025-08-06 15:21:51 +02:00
Andre Toerien
a95dd1e037 refactor: route sudo usage through Sudo::execute* 2025-08-06 15:21:51 +02:00
Andre Toerien
012a6bbde3 fix(sudo): set sudo flags depending on kind 2025-08-06 15:21:51 +02:00
Andre Toerien
32197f79f3 refactor: move RunType::execute to ExecutionContext 2025-08-06 15:21:51 +02:00
Alwin
257d202646 fix: skip gcloud update step if component manager is disabled (#1237) 2025-07-21 15:21:29 +02:00
Andre Toerien
c00365c19d fix(i18n): use double-quotes for translations with newlines 2025-07-18 09:21:31 +02:00
Andre Toerien
3f9fe845e5 refactor(powershell): store powershell path directly 2025-07-18 09:21:31 +02:00
Andre Toerien
b166aae835 fix(powershell): run microsoft_store command directly 2025-07-18 09:21:31 +02:00
Andre Toerien
31f0097862 fix(powershell): remove mentions of USOClient 2025-07-18 09:21:31 +02:00
Andre Toerien
f78514dbd8 fix(powershell): execution policy check breaks when run in pwsh
When topgrade is run from within pwsh, the execution policy check breaks
for the Windows Update and Windows Store steps, because they use normal
powershell and the inherited PSModulePath environment variable breaks
the Microsoft.PowerShell.Security module import. So we unset that
variable to fix the issue, but also allow for those steps to use pwsh as
neither step actually requires PowerShell 5.

Co-authored-by: nistee <52573120+niStee@users.noreply.github.com>
2025-07-18 09:21:31 +02:00
Andre Toerien
9fc5fe9798 fix(powershell): don't use sudo with Update-Module for pwsh
Co-authored-by: Ao <197966644+ao-xing@users.noreply.github.com>
Co-authored-by: nistee <52573120+niStee@users.noreply.github.com>
2025-07-18 09:21:31 +02:00
Andre Toerien
6d14ac1693 refactor(powershell): cleanup and simplify code 2025-07-18 09:21:31 +02:00
Steve Brown
b8ab573c00 fix(powershell): add -Command to module update cmdline 2025-07-18 09:21:31 +02:00
Stuart Reilly
75ac6808a1 Move step running into enum for dynamic ordering (#1188)
Co-authored-by: Stuart Reilly <sreilly@scottlogic.com>
2025-07-16 11:16:27 +02:00
Max Kapur
6719ff93d8 feat(conda): allow configuring additional envs to update (#1048) 2025-07-15 13:15:42 +02:00
Tom van Dijk
6b8327faad feat(step): nix-helper (#1045) 2025-07-15 11:05:46 +02:00
Andreas Hoornstra
85c8bd2277 feat(winget): winget uses sudo when [windows] winget_use_sudo = true (#1061)
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
2025-07-15 09:12:55 +02:00
Axel H.
23fff2a09f fix(tmux): support all default tpm locations (xdg and both hardcoded locations) (#1146) 2025-07-14 10:08:00 +02:00
Ville Skyttä
689db93c99 Generate artifact attestations for release assets (#1216) 2025-07-14 10:05:03 +02:00
Sam Hug
1114556661 windows update, use explicit reboot policy (#1143) 2025-07-14 09:57:00 +02:00
Cthulhux
f8c910a3c2 fixed the German translation for "y/n/s/q" (#1220) 2025-07-14 09:56:26 +02:00
Sam Vente
f18ae089ff feat: suppress pixi release notes by default (#1225)
Co-authored-by: Gideon <87426140+GideonBear@users.noreply.github.com>
2025-07-14 09:51:02 +02:00
SteveLauC
4a64992054 docs: add Discord invite link to README (#1203) 2025-06-26 08:53:25 +08:00
Gideon
9fefb47242 Catch secondary uv self-update error (#1201) 2025-06-25 19:24:07 +08:00
Matt Thomson
fc5cc3c43b Handle another format change in asdf version (#1194)
This now includes a revision part which must be trimmed, e.g.

0.18.0 (revision unknown)
2025-06-19 17:29:11 +08:00
Gideon
27464b795e Preserve custom commmand order from config instead of sorting alphabetically (#1182) 2025-06-17 11:58:23 +08:00
Stuart Reilly
845558c1da Add support for multiple binary names and idea having multiple binaries (#1167)
Add support for tools to have multiple possible binaries because IntellIj IDEA on the AUR can have multiple depending on the edition.

[AUR Ultimate Edition](https://aur.archlinux.org/packages/intellij-idea-ultimate-edition) and [AUR Community Edition](https://aur.archlinux.org/packages/intellij-idea-community-edition-bin) renames the binary to `intellij-idea-ultimate-edition` and `intellij-idea-community-edition` respectively.
2025-06-17 11:52:58 +08:00
SteveLauC
31fe5aa452 ci: fix the invalid action version (#1185) 2025-06-17 11:36:17 +08:00
SteveLauC
b354e07ef3 ci: allow us to re-run AUR CI (#1184)
* ci: allow us to re-run AUR CI

* ci: no need to set shell as it is Linux
2025-06-17 11:32:16 +08:00
slowsage
50a74dac4b Update Yazi upgrade step to use ya pkg. (#1163) 2025-06-17 11:09:14 +08:00
SteveLauC
7558bbfe9b ci: use the new tag name and specify shell to bash (#1183) 2025-06-17 10:44:49 +08:00
SteveLauC
7518676ac9 ci: allow specifying tag when manually run 'create_release_assets.yml' (#1180)
Adds a parameter existing_tag to the workflow_dispatch event of "create_release_assets.yml", which would allow us to re-run a failed release.
2025-06-16 14:39:23 +08:00
SteveLauC
b7b665ff48 ci: fix homebrew ci, remove duplicate trigger event (#1179) 2025-06-16 11:45:48 +08:00
SteveLauC
1be941e815 ci: fix PyPI pipeline duplicate wheel name (#1178) 2025-06-16 11:43:11 +08:00
SteveLauC
d1b7eba44e ci: add event workflow_dispatch to release pipelines (#1177) 2025-06-16 11:36:14 +08:00
SteveLauC
38e2d5663a ci: fix pipeline relase to PyPI (#1176) 2025-06-16 10:52:35 +08:00
Gideon
3db95a3e67 Install rustfmt and clippy where necessary (#1171) 2025-06-16 10:04:06 +08:00
SteveLauC
ef0a0d69bb chore: release v16.0.4 (#1169) 2025-06-13 19:03:17 +08:00
Gideon
4b3a3e74f8 Fix "Only one instance of <IDE> can be run at a a time." error (#1123) 2025-06-13 18:54:07 +08:00
Gideon
2c4751c7b2 Add CachyOS support (#1153)
* Add CachyOS support

* Fix os-release file

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

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

* Remove uneccessary unit parameter

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

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

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

* Deduplicate

* Format

* Fix x.x.0 version crashing

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

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

* Improve comment consistency

* Add comments for missing Windows-only IDEs

* Fix typo

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

* Fix missing "plugins" in Android Studio step name

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

* Add breaking change to BREAKINGCHANGES_dev.md

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

* Sort `Step` enum

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-08 10:25:58 +08:00
Gideon
109a9c76e3 Downgrade create_release_assets.yml workflow (#1098)
Downgrade `create_release_assets.yml` workflow Ubuntu version to `22.04` from `24.04`
2025-04-07 08:39:56 +08:00
SteveLauC
4488f3d5d3 chore: bump version to 16.0.3 (#1094) 2025-04-03 17:56:01 +08:00
Gideon
5a7958d20e Fix aqua CLI and JetBrains Aqua conflict (#1092) 2025-04-03 17:40:52 +08:00
Gideon
481a942b76 Fix pixi self-update running when pixi is not installed with the … (#1087)
* Fix `pixi self-update` running when `pixi` is not installed with the `self-update` feature

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

* corrected formatting issues.

* Update src/steps/os/windows.rs

Remove code duplication.

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

---------

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

Lithuanian language.

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

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

* 🌐 i18n(locales): add missing translations

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

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

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

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

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

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

* Update locales/app.yml

* fix: correct German translation for "Breaking changes"

---------

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

* Update jetbrains-toolbox-updater

* Update jetbrains-toolbox-updater

* Update jetbrains-toolbox-updater

* Localize prints

* Update jetbrains-toolbox-updater

* Format

* Add localization

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

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

* returned inadvertently removed comment.

---------

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

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

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

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

* Format

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

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

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

* feat: add aurora,bluefin,coreos support

The `os_release`s came from the following images:

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

* fix: bazzite should be detected as FedoraImmutable

* squash me: cargo fmt

---------

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

* [feat] Update example config file

* [fix] Remove unneeded imports

* [feat] PR comments

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

* feat(zigup): add various configuration options

* feat(zigup): add cleanup option

* feat(zigup): multiple version support and cleanup

* refactor(zigup): remove set_default and simplify execution

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

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

* Bump MSRV

* Try MSRV without the patch version

* fix: pin toolchain to MSRV

* trying again

* fix dead code warning

---------

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

* Format & Config

* Revert "Format & Config"

This reverts commit 9eedecce8b312f8ad60563488c98cccfd50c0173.

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

This reverts commit 8c80c7a7d63ecd0936e0bd5cb07c2cbb1452c1fd.

* Allow self-update to fail when disabled

* Formatting

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

* Formatting

* Use the code suggested :)

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

* feat(poetry): add poetry_force_self_update config option

* docs: give this config a more detailed explanation

---------

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

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

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

---------

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

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

* fix: deny unknown fields on JuliaConfig deserialisation

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

* doc(julia): clarify startup_file option purpose

---------

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

* fix: missing quotes for string in toml file

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

* fix: deno upgrade for different executable versions

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

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

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

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

---------

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

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

* uv: fixing return behavior

---------

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

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

Closes #912

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

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

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

* Make OpenBSD step less talky and improve verbiage.

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

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

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

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

* "es" localization added

* Grammar fixes

* Fix YAML syntax errors

* Fix YAML syntax errors

* Fix duplicated

* Fix duplicate

* Grammar fix

* Grammar fix

* Fix duplicate

* Improve grammar

* Update locales/app.yml

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

* Improve Grammar

* Improve Grammar

* Improve Grammar

* Improve Grammar

* Improve Grammar

---------

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

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

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

translated topgrade to zh_TW

* Update app.yml

* Update app.yml

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

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

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

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

* style: fmt

* feat: i18n support for new steps

* fix: build on Linux

* fix: build on Linux

* refactor: rm unused translation keys

---------

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

* make linter happy

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

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

* feat: example config update

* feat: move the comment down to be relevant

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

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

* feat: make tmux_session_attach_mode private

* feat: remove tmux mode cli option

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

* feat: renames for tmux session management options

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

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

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

* Address feedback

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

* feat: adds `uv` python manager step

* moved new uv step from unix to generic

* containers step: added container runtime option to config

* documented breaking change

---------

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

* feat: adds `uv` python manager step

* moved new uv step from unix to generic

---------

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

* Move aqua cli to generic.rs

* Add a dry-run support to aqua

* style: format code

---------

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

Added Chocolatey

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

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

Fixes #611.

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

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

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

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

but as the verbose shows it works

* trying

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

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

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

* style(linux.rs): fix clippy reccomendations

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

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

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

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

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

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

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

View File

@@ -2,66 +2,79 @@
name: Bug report
about: Topgrade is misbehaving
title: ''
labels: 'C-bug'
type: Bug
assignees: ''
---
<!--
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)
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!
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.
-->
<!--
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.
-->
## Checklist
- [ ] I have searched the issue tracker for relevant or duplicate issues.
## Erroneous Behavior
<!--
<!--
What actually happened?
-->
## Expected Behavior
<!--
Describe the expected behavior
<!--
Describe the expected behavior.
-->
## Steps to reproduce
<!--
A minimal example to reproduce the issue
<!--
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
<!--
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
## Ran through `Remote Execution`
<!--
Did you run topgrade through `Remote Execution`?
-->
- [ ] Yes
- [ ] No
If yes, does the issue still occur when you run topgrade directly in your
remote host?
- [ ] Yes
- [ ] No
## Configuration file (Optional)
<!--
Paste your configuration file inside the code block if you think this issue is
<!--
Paste your configuration file inside the code block if you think this issue is
related to configuration.
-->
@@ -70,19 +83,21 @@ related to configuration.
```
## Additional Details
- Operation System/Version
<!-- For example, Fedora Linux 38 -->
- Installation
<!--
How did you install topgrade: build from repo / crates.io (cargo install topgrade)
<!--
How did you install topgrade: build from repo / crates.io (cargo install topgrade)
/ package manager (which one) / other (describe)
-->
- Topgrade version (`topgrade -V`)
## Verbose Output (`topgrade -v`)
<!--
<!--
Paste the verbose output into the pre-tags
-->

View File

@@ -1,25 +1,23 @@
---
name: Feature request
about: Can you please support...?
name: General feature request
about: Suggest a general feature, or feature within an already existing step
title: ''
labels: 'C-feature request'
type: Feature
assignees: ''
---
## I want to suggest a new step
## Checklist
* Which tool is this about? Where is its repository?
* Which operating systems are supported by this tool?
* 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 have searched the issue tracker for relevant or duplicate issues.
## I want to suggest some general feature
Topgrade should...
## More information
<!-- Assuming that someone else implements the feature,
please state if you know how to test it from a side branch of Topgrade. -->
- [ ] I am able and willing to implement this feature myself

30
.github/ISSUE_TEMPLATE/step_request.md vendored Normal file
View File

@@ -0,0 +1,30 @@
---
name: New step request
about: Suggest a new step/package manager to update
title: ''
type: Feature
labels: request step
assignees: ''
---
## Checklist
- [ ] I have searched the issue tracker for relevant or duplicate issues.
## I want to suggest a new step
* Which tool is this about? Where is its repository?
* Which operating systems are supported by this tool?
* 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?
## More information
<!-- Assuming that someone else implements the step,
please state if you know how to test it from a side branch of Topgrade. -->
- [ ] I am able and willing to implement this step myself

View File

@@ -1,17 +1,17 @@
## What does this PR do
## Standards checklist
- [ ] The PR title is descriptive.
- [ ] The PR title is descriptive
- [ ] I have read `CONTRIBUTING.md`
- [ ] *Optional:* I have tested the code myself
- [ ] If this PR introduces new user-facing messages they are translated
## For new steps
- [ ] *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
- [ ] *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

View File

@@ -1,10 +0,0 @@
# 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

@@ -7,15 +7,21 @@ env:
CARGO_TERM_COLOR: always
permissions:
contents: read
jobs:
TestConfig:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- run: |
CONFIG_PATH=~/.config/topgrade.toml;
if [ -f "$CONFIG_PATH" ]; then rm $CONFIG_PATH; fi
cargo build;
cargo build;
TOPGRADE_SKIP_BRKC_NOTIFY=true ./target/debug/topgrade --dry-run --only system;
stat $CONFIG_PATH;

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

@@ -0,0 +1,27 @@
on:
pull_request:
push:
branches:
- main
name: Check i18n
permissions:
contents: read
jobs:
check_locale:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- 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

@@ -11,6 +11,9 @@ on:
branches:
- main
permissions:
contents: read
jobs:
lint:
name: DevSkim
@@ -21,12 +24,14 @@ jobs:
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Run DevSkim scanner
uses: microsoft/DevSkim-Action@v1
uses: microsoft/DevSkim-Action@4b5047945a44163b94642a1cecc0d93a3f428cc6 # v1.0.16
- name: Upload DevSkim scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
with:
sarif_file: devskim-results.sarif

View File

@@ -1,27 +0,0 @@
on:
release:
types: [published, edited]
name: Check SemVer compliance
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2022-08-03
override: true
components: rustfmt, clippy
semver:
runs-on: ubuntu-latest
steps:
- uses: actions-rs/cargo@v1
with:
command: install
args: --git https://github.com/rust-lang/rust-semverver
- run: eval "current_version=$(grep -e '^version = .*$' Cargo.toml | cut -d ' ' -f 3)"
- run: cargo semver | tee semver_out
- run: (head -n 1 semver_out | grep "\-> $current_version") || (echo "versioning mismatch" && return 1)

View File

@@ -7,32 +7,93 @@ on:
name: CI
env:
RUST_VER: 'stable'
CROSS_VER: '0.2.5'
CARGO_NET_RETRY: 3
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
toolchain: '${{ env.RUST_VER }}'
components: rustfmt
persist-credentials: false
- name: Run cargo fmt
env:
TERM: xterm-256color
run: |
rustup component add rustfmt
cargo fmt --all -- --check
custom-checks:
name: Custom checks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Check if `Step` enum is sorted
run: |
ENUM_NAME="Step"
FILE="src/step.rs"
awk "/enum $ENUM_NAME/,/}/" "$FILE" | \
grep -E '^\s*[A-Za-z_][A-Za-z0-9_]*\s*,?$' | \
sed 's/[, ]//g' > original.txt
sort original.txt > sorted.txt
diff original.txt sorted.txt
- name: Check if `Step::run()`'s match is sorted
run: |
FILE="src/step.rs"
awk '/[[:alpha:]] =>/{print $1}' $FILE > original.txt
sort original.txt > sorted.txt
diff original.txt sorted.txt
- name: Check if `default_steps` contains every step
run: |
# Extract all variants from enum Step
all_variants=$(sed -n '/^pub enum Step {/,/^}/p' src/step.rs | grep -Po '^\s*\K[A-Z][A-Za-z0-9_]*' | sort)
# Extract variants used inside default_steps
used_variants=$(sed -n '/^pub(crate) fn default_steps()/,/^}/p' src/step.rs | \
grep -Po '\b[A-Z][A-Za-z0-9_]*\b' | \
grep -Fx -f <(echo "$all_variants") | \
sort)
# Check for missing variants
missing=$(comm -23 <(echo "$all_variants") <(echo "$used_variants"))
if [[ -z "$missing" ]]; then
echo "All variants are used."
else
echo "Missing variants:"
echo "$missing"
exit 1
fi
# Check for duplicates
duplicates=$(echo "$used_variants" | uniq -c | awk '$1 > 1 {print $2}')
if [[ -z "$duplicates" ]]; then
echo "No duplicates found."
else
echo "Duplicates found:"
echo "$duplicates"
# We allow duplicates, but lets keep this check for potential future usefulness
# exit 1
fi
main:
needs: fmt
needs: [ fmt, custom-checks ]
name: ${{ matrix.target_name }} (check, clippy)
runs-on: ${{ matrix.os }}
strategy:
@@ -55,7 +116,7 @@ jobs:
- target: x86_64-apple-darwin
target_name: macOS-x86_64
os: macos-13
os: macos-15-intel
- target: aarch64-apple-darwin
target_name: macOS-aarch64
@@ -69,32 +130,36 @@ jobs:
- target: x86_64-pc-windows-msvc
target_name: Windows
os: windows-latest
env:
cargo_cmd: ${{ matrix.use_cross == true && 'cross' || 'cargo' }}
matrix_target: ${{ matrix.target }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
toolchain: '${{ env.RUST_VER }}'
components: clippy
persist-credentials: false
- name: Setup Rust Cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
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
run: |
curl -fL --retry 3 "https://github.com/cross-rs/cross/releases/download/v${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 }}
run: |
"${cargo_cmd}" check --locked --target "${matrix_target}"
- name: Run cargo/cross clippy
run: ${{ matrix.use_cross == true && 'cross' || 'cargo' }} clippy --locked --target ${{ matrix.target }} --all-features -- -D warnings
run: |
rustup component add clippy
"${cargo_cmd}" clippy --locked --target "${matrix_target}" --all-features -- -D warnings
- name: Run cargo test
# ONLY run test with cargo
if: matrix.use_cross == false
run: cargo test --locked --target ${{ matrix.target }}
run: |
cargo test --locked --target "${matrix_target}"

View File

@@ -1,27 +1,51 @@
name: Publish release files for CD native environments
name: Publish release files for CD native and non-cd-native environments
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
release:
types: [ created ]
repository_dispatch:
types: [ release-created ]
permissions:
# Write permissions to call the repository dispatch
contents: write
defaults:
run:
shell: bash
jobs:
build:
# Publish release files for CD native environments
native_build:
permissions:
# Use to sign the release artifacts
id-token: write
# Used to upload release artifacts
contents: write
# Used to generate artifact attestations
attestations: write
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-latest, macos-latest, macos-13, windows-latest ]
# Use the Ubuntu 22.04 image to link with a low version of glibc
#
# https://github.com/topgrade-rs/topgrade/issues/1095
platform: [ ubuntu-22.04, macos-latest, macos-15-intel, windows-latest ]
runs-on: ${{ matrix.platform }}
env:
tag: ${{ github.event.client_payload.tag }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Install needed components
run: |
rustup component add rustfmt
rustup component add clippy
- name: Install cargo-deb
run: cargo install cargo-deb
if: ${{ startsWith(matrix.platform, 'ubuntu-') }}
shell: bash
- name: Check format
run: cargo fmt --all -- --check
@@ -35,14 +59,38 @@ jobs:
- name: Run tests
run: cargo test
# Used `https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/release.yml`
# as a reference.
- name: Build debug binary to create release assets
shell: bash
run: |
cargo build --all-features
bin="target/debug/topgrade"
echo "BIN=$bin" >> $GITHUB_ENV
- name: Create deployment directory
shell: bash
run: |
dir=deployment/deb
mkdir -p "$dir"
echo "DEPLOY_DIR=$dir" >> $GITHUB_ENV
- name: Generate man page and shell completions
shell: bash
run: |
"$BIN" --gen-manpage > "$DEPLOY_DIR/topgrade.1"
"$BIN" --gen-completion bash > "$DEPLOY_DIR/topgrade.bash"
"$BIN" --gen-completion fish > "$DEPLOY_DIR/topgrade.fish"
"$BIN" --gen-completion zsh > "$DEPLOY_DIR/_topgrade"
- 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 assets
FILENAME=topgrade-${{github.event.release.tag_name}}-$(default-target)
mkdir -p assets
FILENAME=topgrade-${tag}-$(default-target)
mv target/release/topgrade assets
cd assets
tar --format=ustar -czf $FILENAME.tar.gz topgrade
@@ -51,11 +99,29 @@ jobs:
if: ${{ matrix.platform != 'windows-latest' }}
shell: bash
- name: Build Debian-based system binary and create package
# First remove the binary built by previous steps
# because we don't want the auto-update feature,
# then build the new binary without auto-updating.
run: |
rm -rf target/release
cargo build --release
cargo deb --no-build --no-strip
if: ${{ startsWith(matrix.platform, 'ubuntu-') }}
shell: bash
- name: Move Debian-based system package
run: |
mkdir -p assets
mv target/debian/*.deb assets
if: ${{ startsWith(matrix.platform, 'ubuntu-') }}
shell: bash
- name: Rename Release (Windows)
run: |
cargo install default-target
mkdir assets
FILENAME=topgrade-${{github.event.release.tag_name}}-$(default-target)
FILENAME=topgrade-${tag}-$(default-target)
mv target/release/topgrade.exe assets/topgrade.exe
cd assets
powershell Compress-Archive -Path * -Destination ${FILENAME}.zip
@@ -64,7 +130,174 @@ jobs:
if: ${{ matrix.platform == 'windows-latest' }}
shell: bash
- name: Release
uses: softprops/action-gh-release@v2
- name: Upload assets
run: |
gh release upload "${tag}" assets/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate artifact attestations
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
files: assets/*
subject-path: assets/*
# Publish release files for non-CD-native environments
cross_build:
permissions:
# Use to sign the release artifacts
id-token: write
# Used to upload release artifacts
contents: write
# Used to generate artifact attestations
attestations: write
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",
]
# Run this one on an older version as well, to limit glibc to 2.34 instead of 2.39.
# Even though this is cross-compiled, it links to the libc6-<arch>-cross installed on the host
# (see the apt-get install calls below)
runs-on: ubuntu-22.04
env:
matrix_target: ${{ matrix.target }}
tag: ${{ github.event.client_payload.tag }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Install needed components
run: |
rustup component add rustfmt
rustup component add clippy
- name: Install cargo-deb cross compilation dependencies
run: sudo apt-get install libc6-arm64-cross libgcc-s1-arm64-cross
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
shell: bash
- name: Install cargo-deb cross compilation dependencies for armv7
run: sudo apt-get install libc6-armhf-cross libgcc-s1-armhf-cross
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
shell: bash
- name: Install cargo-deb
run: cargo install cargo-deb
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'armv7-unknown-linux-gnueabihf' }}
shell: bash
- name: install targets
run: rustup target add "${matrix_target}"
- name: install cross
# Install from source to fix `ld: cannot find -lgeom` for freebsd build
run: cargo +stable install --git https://github.com/cross-rs/cross cross
- 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}"
# Running tests on FreeBSD is impossible; see https://github.com/cross-rs/cross/wiki/FAQ#running-bsd-tests
# Not that this is *NOT* the same as the original issue with `ld: cannot find -lgeom`, but a new issue:
# error: test failed, to rerun pass `--lib`
# Caused by:
# could not execute process `/target/x86_64-unknown-freebsd/debug/deps/topgrade-9b1670d87ca863dd` (never executed)
# Caused by:
# No such file or directory (os error 2)
# TODO: I have not tested this in GHA yet, only locally
if: ${{ matrix.target != 'x86_64-unknown-freebsd' }}
# Used `https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/release.yml`
# as a reference.
- name: Build debug binary to create release assets
shell: bash
run: |
# This build is not using the target arch since this binary is only needed in CI. It needs
# to be the compiled for the runner since it has the run the binary to generate completion
# scripts.
cargo build --all-features
bin="target/debug/topgrade"
echo "BIN=$bin" >> $GITHUB_ENV
- name: Create deployment directory
shell: bash
run: |
dir=deployment/deb
mkdir -p "$dir"
echo "DEPLOY_DIR=$dir" >> $GITHUB_ENV
- name: Generate man page and shell completions
shell: bash
run: |
"$BIN" --gen-manpage > "$DEPLOY_DIR/topgrade.1"
"$BIN" --gen-completion bash > "$DEPLOY_DIR/topgrade.bash"
"$BIN" --gen-completion fish > "$DEPLOY_DIR/topgrade.fish"
"$BIN" --gen-completion zsh > "$DEPLOY_DIR/_topgrade"
- 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-${tag}-${matrix_target}
mv "target/${matrix_target}/release/topgrade" assets
cd assets
tar --format=ustar -czf "$FILENAME.tar.gz" topgrade
rm topgrade
ls .
- name: Build Debian-based system package without autoupdate feature
# First remove the binary built by previous steps
# because we don't want the auto-update feature,
# then build the new binary without auto-updating.
run: |
rm -rf "target/${matrix_target}"
cross build --release --target "${matrix_target}"
cargo deb --target="${matrix_target}" --no-build --no-strip
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'armv7-unknown-linux-gnueabihf' }}
shell: bash
- name: Move Debian-based system package
run: |
mkdir -p assets
mv target/"${matrix_target}"/debian/*.deb assets
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'armv7-unknown-linux-gnueabihf' }}
shell: bash
- name: Upload assets
run:
gh release upload "${tag}" assets/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate artifact attestations
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-path: assets/*
triggers:
runs-on: ubuntu-latest
needs: [ native_build, cross_build ]
env:
tag: ${{ github.event.client_payload.tag }}
steps:
- name: Trigger workflows
run: |
gh api "repos/${GITHUB_REPOSITORY}/dispatches" \
-f "event_type=release-assets-built" \
-F "client_payload[tag]=${tag}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,68 +0,0 @@
name: Publish release files for non-cd-native environments
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
release:
types: [ created ]
jobs:
build:
strategy:
fail-fast: false
matrix:
target: [
"aarch64-unknown-linux-gnu",
"armv7-unknown-linux-gnueabihf",
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-musl",
"x86_64-unknown-freebsd",
]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- 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 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@v2
with:
files: assets/*

25
.github/workflows/dependency-review.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request,
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
# Once installed, if the workflow run is marked as required,
# PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: 'Dependency Review'
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2

20
.github/workflows/lint_pr.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: 'Lint PR'
on:
pull_request_target: # zizmor: ignore[dangerous-triggers] this is the only way, and we're not running user code
types:
- opened
- edited
- reopened
- synchronize
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

67
.github/workflows/release-plz.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
name: Release-plz
on:
push:
branches:
- main
jobs:
# Release unpublished packages.
release-plz-release:
name: Release-plz release
runs-on: ubuntu-latest
environment: crates_io
permissions:
contents: write
id-token: write # For trusted publishing
steps:
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
fetch-depth: 0
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
id: release-plz
uses: release-plz/action@d529f731ae3e89610ada96eda34e5c6ba3b12214 # v0.5
with:
command: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger workflows
if: steps.release-plz.outputs.releases_created == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ fromJSON(steps.release-plz.outputs.releases)[0].tag }}
run: |
gh api "repos/${GITHUB_REPOSITORY}/dispatches" \
-f "event_type=release-created" \
-F "client_payload[tag]=${tag}"
# Create a PR with the new versions and changelog, preparing the next release.
release-plz-pr:
name: Release-plz PR
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
concurrency:
group: release-plz-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
fetch-depth: 0
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: release-plz/action@d529f731ae3e89610ada96eda34e5c6ba3b12214 # v0.5
with:
command: release-pr
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,22 +1,38 @@
name: Publish to AUR
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
push:
tags:
- "v*"
repository_dispatch:
types: [ release-assets-built ]
permissions:
contents: read
jobs:
aur-publish:
runs-on: ubuntu-latest
steps:
- name: Publish AUR package
uses: ATiltedTree/create-aur-release@v1
- name: Determine version
id: determine_version
env:
tag: ${{ github.event.client_payload.tag }}
run: |
# tag should be something like "v16.0.4", remove the prefix v here
echo "version=${tag#v}" >> $GITHUB_OUTPUT
- name: Publish source AUR package
uses: varabyte/update-aur-package@572e31b1972fa289a27b1926c06a489eb89c7fd7
with:
version: ${{ steps.determine_version.outputs.version }}
package_name: topgrade
commit_username: "Thomas Schönauer"
commit_email: t.schoenauer@hgs-wt.at
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
- name: Publish binary AUR package
uses: varabyte/update-aur-package@572e31b1972fa289a27b1926c06a489eb89c7fd7
with:
version: ${{ steps.determine_version.outputs.version }}
package_name: topgrade-bin
commit_username: "Thomas Schönauer"
commit_email: t.schoenauer@hgs-wt.at
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}

View File

@@ -1,29 +0,0 @@
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
release:
types: [published]
name: Publish to crates.io on release
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
publish:
runs-on: ubuntu-latest
steps:
- uses: katyo/publish-crates@v2
with:
dry-run: true
check-repo: ${{ github.event_name == 'push' }}
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
ignore-unpublished-changes: true

View File

@@ -1,39 +1,22 @@
name: Publish to Homebrew
on:
# workflow_run:
# workflows: ["Check SemVer compliance"]
# types:
# - completed
workflow_dispatch:
push:
tags:
- "v*"
repository_dispatch:
types: [ release-created ]
permissions:
contents: read
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
uses: dawidd6/action-homebrew-bump-formula@c5ddc585e75f0f750a8b4f610688b4bec9e80915 # v6
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
formula: topgrade
tag: ${{ github.event.client_payload.tag }}
# We cannot use an org because org forks cannot give push access to maintainers, which Homebrew requires.
# org: topgrade-rs

View File

@@ -1,31 +1,34 @@
name: Update PyPi
on:
release:
types: [published]
repository_dispatch:
types: [ release-created ]
permissions:
contents: read
jobs:
# TODO: make linux/windows/macos/sdist a matrix. See how other workflows do it.
linux:
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64, x86, aarch64]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Build wheels
uses: PyO3/maturin-action@v1
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.target }}
args: --release --out dist
sccache: 'true'
manylinux: auto
- name: Upload wheels
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: wheels
name: wheels-linux-${{ matrix.target }}
path: dist
windows:
@@ -34,17 +37,19 @@ jobs:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Build wheels
uses: PyO3/maturin-action@v1
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.target }}
args: --release --out dist
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: wheels
name: wheels-windows-${{ matrix.target }}
path: dist
macos:
@@ -53,47 +58,62 @@ jobs:
matrix:
target: [x86_64, aarch64]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Build wheels
uses: PyO3/maturin-action@v1
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.target }}
args: --release --out dist
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: wheels
name: wheels-macos-${{ matrix.target }}
path: dist
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Build sdist
uses: PyO3/maturin-action@v1
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
command: sdist
args: --out dist
- name: Upload sdist
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: wheels
name: wheels-sdist
path: dist
release:
name: Release
runs-on: ubuntu-latest
if: "startsWith(github.ref, 'refs/tags/')"
needs: [linux, windows, macos, sdist]
permissions:
# Use to sign the release artifacts
id-token: write
# Used to upload release artifacts
contents: write
# Used to generate artifact attestation
attestations: write
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
- name: Generate artifact attestation
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
name: wheels
subject-path: 'wheels-*/*'
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
command: upload
args: --skip-existing *
args: --non-interactive --skip-existing wheels-*/*

View File

@@ -1,13 +1,19 @@
name: Publish to WinGet
on:
release:
types: [released]
repository_dispatch:
types: [ release-created ]
permissions:
contents: read
jobs:
publish:
runs-on: windows-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
- uses: vedantmgoyal2009/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f # main
with:
release-tag: ${{ github.event.client_payload.tag }}
identifier: topgrade-rs.topgrade
max-versions-to-keep: 5 # keep only latest 5 versions
token: ${{ secrets.WINGET_TOKEN }}
token: ${{ secrets.WINGET_TOKEN }}

76
.github/workflows/scorecards.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
# 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: Scorecard supply-chain security
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '20 7 * * 2'
push:
branches: ["main"]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results and get a badge (see publish_results below).
id-token: write
contents: read
actions: read
# To allow GraphQL ListCommits to work
issues: read
pull-requests: read
# To detect SAST tools
checks: read
steps:
- name: "Checkout code"
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecards on a *private* repository
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
# Public repositories:
# - Publish results to OpenSSF REST API for easy access by consumers
# - Allows the repository to include the Scorecard badge.
# - See https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories:
# - `publish_results` will always be set to `false`, regardless
# of the value entered here.
publish_results: true
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
with:
sarif_file: results.sarif

25
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,25 @@
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.29.0
hooks:
- id: gitleaks
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1
hooks:
- id: shellcheck
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/crate-ci/typos
rev: v1.39.2
hooks:
- id: typos
ci:
autoupdate_commit_msg: "chore(pre-commit): autoupdate"

20
.typos.toml Normal file
View File

@@ -0,0 +1,20 @@
# Typos configuration (minimal, conservative)
# Exclude locales and OS fingerprint data to avoid false positives
# - Recognize a few project-specific proper nouns
[files]
extend-exclude = [
"src/steps/os/os_release/**",
"locales/**",
# Include only English locale files - TODO: Split locales/app.yml into a Separate english File
# "!locales/en/**"
]
[default]
# Mark specific words as always valid by mapping them to themselves
check-file = true
check-filename = true
[default.extend-words]
# Add project-specific terms that should not be flagged as typos
# Example: topgrade = "topgrade"

View File

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

View File

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

296
CHANGELOG.md Normal file
View File

@@ -0,0 +1,296 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [16.4.2](https://github.com/topgrade-rs/topgrade/compare/v16.4.1...v16.4.2) - 2025-11-20
### Other
- *(deps)* update dawidd6/action-homebrew-bump-formula action to v6 ([#1543](https://github.com/topgrade-rs/topgrade/pull/1543))
## [16.4.1](https://github.com/topgrade-rs/topgrade/compare/v16.4.0...v16.4.1) - 2025-11-20
### Other
- refactor run_containers error handling ([#1541](https://github.com/topgrade-rs/topgrade/pull/1541))
## [16.4.0](https://github.com/topgrade-rs/topgrade/compare/v16.3.0...v16.4.0) - 2025-11-20
### Added
- *(os)* add Origami Linux support ([#1530](https://github.com/topgrade-rs/topgrade/pull/1530))
- *(containers)* add option to run `system prune` ([#1523](https://github.com/topgrade-rs/topgrade/pull/1523))
### Fixed
- *(deps)* restore custom commands order ([#1535](https://github.com/topgrade-rs/topgrade/pull/1535))
### Other
- *(deps)* update clap, clap_builder, clap_complete ([#1540](https://github.com/topgrade-rs/topgrade/pull/1540))
- *(deps)* update github/codeql-action action to v4.31.4 ([#1531](https://github.com/topgrade-rs/topgrade/pull/1531))
- *(config)* add custom commands order test ([#1536](https://github.com/topgrade-rs/topgrade/pull/1536))
- make Config methods more consistent by utilizing `#[derive(Default)]` ([#1534](https://github.com/topgrade-rs/topgrade/pull/1534))
- *(issue templates)* use issue types ([#1533](https://github.com/topgrade-rs/topgrade/pull/1533))
- *(deps)* lock file maintenance ([#1505](https://github.com/topgrade-rs/topgrade/pull/1505))
- *(deps)* update actions/checkout digest to 93cb6ef ([#1526](https://github.com/topgrade-rs/topgrade/pull/1526))
- *(deps)* update actions/checkout action to v5.0.1 ([#1527](https://github.com/topgrade-rs/topgrade/pull/1527))
## [16.3.0](https://github.com/topgrade-rs/topgrade/compare/v16.2.1...v16.3.0) - 2025-11-16
### Added
- print summary and run post commands when (q)uit is used ([#1254](https://github.com/topgrade-rs/topgrade/pull/1254))
- run pre_sudo before pre_commands ([#1469](https://github.com/topgrade-rs/topgrade/pull/1469))
- *(chezmoi)* add `exclude_encrypted` config ([#1453](https://github.com/topgrade-rs/topgrade/pull/1453))
### Fixed
- *(elan)* skip running elan update on elan >=4.0.0 ([#1507](https://github.com/topgrade-rs/topgrade/pull/1507))
- *(deps)* Fix non-locked install on older version of Rust ([#1485](https://github.com/topgrade-rs/topgrade/pull/1485))
- *(deps)* Fix non-locked install on older version of Rust ([#1482](https://github.com/topgrade-rs/topgrade/pull/1482))
- *(bun)* skip self-update if not installed via official script ([#1476](https://github.com/topgrade-rs/topgrade/pull/1476))
- *(openbsd)* fix compilation on OpenBSD ([#1473](https://github.com/topgrade-rs/topgrade/pull/1473))
### Other
- *(license)* switch license variant to GPL-3.0-or-later ([#1518](https://github.com/topgrade-rs/topgrade/pull/1518))
- *(deps)* update some dependencies ([#1512](https://github.com/topgrade-rs/topgrade/pull/1512))
- *(deps)* update github/codeql-action action to v4.31.3 ([#1483](https://github.com/topgrade-rs/topgrade/pull/1483))
- remove unnecessary cfg-if dependency ([#1509](https://github.com/topgrade-rs/topgrade/pull/1509))
- *(lint_pr)* run on synchronize, and add zizmor ignore ([#1508](https://github.com/topgrade-rs/topgrade/pull/1508))
- *(pre-commit)* autoupdate ([#1464](https://github.com/topgrade-rs/topgrade/pull/1464))
- improve issue templates ([#1235](https://github.com/topgrade-rs/topgrade/pull/1235))
- *(deps)* bump mac-notification-sys, use main branch temporarily ([#1506](https://github.com/topgrade-rs/topgrade/pull/1506))
- *(deps)* lock file maintenance ([#1481](https://github.com/topgrade-rs/topgrade/pull/1481))
- *(deps)* pin dependencies ([#1478](https://github.com/topgrade-rs/topgrade/pull/1478))
- *(deps)* update actions/dependency-review-action action to v4.8.2 ([#1479](https://github.com/topgrade-rs/topgrade/pull/1479))
- Add Renovate ([#1477](https://github.com/topgrade-rs/topgrade/pull/1477))
- Replace main's self update with a proper step call ([#1470](https://github.com/topgrade-rs/topgrade/pull/1470))
- *(release)* Fix homebrew releases ([#1468](https://github.com/topgrade-rs/topgrade/pull/1468))
## [16.2.1](https://github.com/topgrade-rs/topgrade/compare/v16.2.0...v16.2.1) - 2025-11-10
### Fixed
- *(release)* Use bash in Windows to fix powershell issues ([#1461](https://github.com/topgrade-rs/topgrade/pull/1461))
- *(release)* Fix .deb distribution ([#1460](https://github.com/topgrade-rs/topgrade/pull/1460))
- *(release)* Fix .deb distribution ([#1458](https://github.com/topgrade-rs/topgrade/pull/1458))
## [16.2.0](https://github.com/topgrade-rs/topgrade/compare/v16.1.2...v16.2.0) - 2025-11-10
### Added
- *(mise)* run `mise self-update` ([#1450](https://github.com/topgrade-rs/topgrade/pull/1450))
- *(falconf)* add falconf step ([#1219](https://github.com/topgrade-rs/topgrade/pull/1219))
- *(hyprpm)* add hyprpm step ([#1213](https://github.com/topgrade-rs/topgrade/pull/1213))
- *(doom)* add doom.aot option ([#1214](https://github.com/topgrade-rs/topgrade/pull/1214))
- add show_distribution_summary config option ([#1259](https://github.com/topgrade-rs/topgrade/pull/1259))
- *(rustup)* add rustup.channels config ([#1206](https://github.com/topgrade-rs/topgrade/pull/1206))
- *(os)* add AOSC OS support ([#1424](https://github.com/topgrade-rs/topgrade/pull/1424))
- add damp run type ([#1217](https://github.com/topgrade-rs/topgrade/pull/1217))
### Fixed
- *(release)* fix homebrew releases by migrating to dawidd6/action-homebrew-bump-formula ([#1457](https://github.com/topgrade-rs/topgrade/pull/1457))
- *(mise)* fix mise self-update failing when installed via a package manager ([#1456](https://github.com/topgrade-rs/topgrade/pull/1456))
- *(release)* Add man page to .deb distribution ([#1455](https://github.com/topgrade-rs/topgrade/pull/1455))
- *(self-update)* fix windows self-update reporting failure on successful self-update ([#1452](https://github.com/topgrade-rs/topgrade/pull/1452))
- *(pkgfile)* make pkgfile opt-in ([#1449](https://github.com/topgrade-rs/topgrade/pull/1449))
- *(vcpkg)* fix permission denied when updating vcpkg if it's installed as root ([#1447](https://github.com/topgrade-rs/topgrade/pull/1447))
- *(zh_TW)* fixed zh_TW strings ([#1446](https://github.com/topgrade-rs/topgrade/pull/1446))
- *(git)* fix shellexpand::tilde in git_repos in topgrade.d/* ([#1223](https://github.com/topgrade-rs/topgrade/pull/1223))
- *(auto-cpufreq)* skip when install script is not used ([#1215](https://github.com/topgrade-rs/topgrade/pull/1215))
- *(vim)* change nvimrc base_dir for windows ([#1433](https://github.com/topgrade-rs/topgrade/pull/1433))
- *(guix)* fix overcomplicated Guix step ([#1290](https://github.com/topgrade-rs/topgrade/pull/1290))
- *(gem)* fix incorrectly placed debug message in `gem` step ([#1212](https://github.com/topgrade-rs/topgrade/pull/1212))
- *(conda)* replace deprecated `auto_activate_base` ([#1158](https://github.com/topgrade-rs/topgrade/pull/1158))
- *(containers)* fix panic in `containers` step ([#1150](https://github.com/topgrade-rs/topgrade/pull/1150))
- *(jetbrains-toolbox)* fix step not dry running ([#1253](https://github.com/topgrade-rs/topgrade/pull/1253))
### Other
- comment run_config_update ([#1448](https://github.com/topgrade-rs/topgrade/pull/1448))
- Expand LLM guidelines in CONTRIBUTING.md ([#1445](https://github.com/topgrade-rs/topgrade/pull/1445))
- Add AI guidelines to CONTRIBUTING.md ([#1444](https://github.com/topgrade-rs/topgrade/pull/1444))
- add comments to Config::allowed_steps ([#1291](https://github.com/topgrade-rs/topgrade/pull/1291))
- *(nix)* Deduplicate run_nix and run_nix_self_upgrade nix --version checking ([#1376](https://github.com/topgrade-rs/topgrade/pull/1376))
- remove commented-out library code and unnecessary bin declaration ([#1373](https://github.com/topgrade-rs/topgrade/pull/1373))
- Simplify target cfgs ([#1346](https://github.com/topgrade-rs/topgrade/pull/1346))
- tidy up binary-conflict code ([#1329](https://github.com/topgrade-rs/topgrade/pull/1329))
- Improve installation section ([#1442](https://github.com/topgrade-rs/topgrade/pull/1442))
- *(deps)* Update jetbrains-toolbox-updater ([#1438](https://github.com/topgrade-rs/topgrade/pull/1438))
- remove template expansion in code contexts ([#1434](https://github.com/topgrade-rs/topgrade/pull/1434))
- *(deps)* bump github/codeql-action from 4.31.0 to 4.31.2 ([#1427](https://github.com/topgrade-rs/topgrade/pull/1427))
- don't persist credentials in actions/checkout ([#1422](https://github.com/topgrade-rs/topgrade/pull/1422))
- Improve CONTRIBUTING.md ([#1420](https://github.com/topgrade-rs/topgrade/pull/1420))
- Update SECURITY.md ([#1421](https://github.com/topgrade-rs/topgrade/pull/1421))
- Enforce conventional commits in PR titles ([#1418](https://github.com/topgrade-rs/topgrade/pull/1418))
- Improve contributing section
- Remove roadmap
- Reformat README.md
- Update installation methods
- *(release)* Fix dispatch error in create_release_assets.yml ([#1406](https://github.com/topgrade-rs/topgrade/pull/1406))
## [16.1.2](https://github.com/topgrade-rs/topgrade/compare/v16.1.1...v16.1.2) - 2025-11-01
### Fixed
- *(release)* Fix cross-compilation for arm requiring glibc>=2.39 ([#1405](https://github.com/topgrade-rs/topgrade/pull/1405))
- *(release)* Fix FreeBSD build ([#1404](https://github.com/topgrade-rs/topgrade/pull/1404))
- *(release)* Fix FreeBSD build ([#1402](https://github.com/topgrade-rs/topgrade/pull/1402))
- *(release)* Fix manual workflow trigger ([#1401](https://github.com/topgrade-rs/topgrade/pull/1401))
- *(release)* Fix FreeBSD build and add manual workflow trigger ([#1399](https://github.com/topgrade-rs/topgrade/pull/1399))
### Other
- *(release)* Fix cross trying to fmt ([#1403](https://github.com/topgrade-rs/topgrade/pull/1403))
## [16.1.1](https://github.com/topgrade-rs/topgrade/compare/v16.1.0...v16.1.1) - 2025-11-01
### Fixed
- *(typst)* Skip typst when self-update is disabled ([#1397](https://github.com/topgrade-rs/topgrade/pull/1397))
- *(release)* Fix winget release workflow ([#1395](https://github.com/topgrade-rs/topgrade/pull/1395))
- *(release)* Fix FreeBSD release ([#1393](https://github.com/topgrade-rs/topgrade/pull/1393))
- *(release)* Fix FreeBSD release ([#1391](https://github.com/topgrade-rs/topgrade/pull/1391))
### Other
- Update from deprecated macos-13 to macos-15-intel ([#1394](https://github.com/topgrade-rs/topgrade/pull/1394))
## [16.1.0](https://github.com/topgrade-rs/topgrade/compare/v16.0.4...v16.1.0) - 2025-10-31
### Added
- *(deb-get)* Skip non-deb-get packages by passing --dg-only ([#1386](https://github.com/topgrade-rs/topgrade/pull/1386))
- *(typst)* add typst step ([#1374](https://github.com/topgrade-rs/topgrade/pull/1374))
- *(step)* Add atuin step ([#1367](https://github.com/topgrade-rs/topgrade/pull/1367))
- *(nix)* support upgrading Determinate Nix ([#1366](https://github.com/topgrade-rs/topgrade/pull/1366))
- *(sudo)* print warning if Windows Sudo is misconfigured
- *(sudo)* print warning if steps were skipped due to missing sudo
- *(sudo)* add SudoKind::Null
- detect and warn if running as root
- add `--no-tmux` flag ([#1328](https://github.com/topgrade-rs/topgrade/pull/1328))
- add step for mandb - user and system (update man entries) ([#1319](https://github.com/topgrade-rs/topgrade/pull/1319))
- support for pkgfile ([#1306](https://github.com/topgrade-rs/topgrade/pull/1306))
- add "show_skipped" option in config file #1280 ([#1286](https://github.com/topgrade-rs/topgrade/pull/1286))
- fix typos ([#1221](https://github.com/topgrade-rs/topgrade/pull/1221))
- *(conda)* allow configuring additional envs to update ([#1048](https://github.com/topgrade-rs/topgrade/pull/1048))
- *(step)* nix-helper ([#1045](https://github.com/topgrade-rs/topgrade/pull/1045))
- *(winget)* winget uses sudo when `[windows] winget_use_sudo = true` ([#1061](https://github.com/topgrade-rs/topgrade/pull/1061))
- suppress pixi release notes by default ([#1225](https://github.com/topgrade-rs/topgrade/pull/1225))
### Fixed
- *(freshclam)* run with sudo when running without sudo fails ([#1118](https://github.com/topgrade-rs/topgrade/pull/1118))
- *(tldr)* move tldr to be a generic step ([#1370](https://github.com/topgrade-rs/topgrade/pull/1370))
- *(nix)* fix nix upgrade command selection for profiles in XDG_STATE_HOME ([#1354](https://github.com/topgrade-rs/topgrade/pull/1354))
- *(containers)* Docker update fails on M Macs due to platform / ([#1360](https://github.com/topgrade-rs/topgrade/pull/1360))
- *(sudo)* reorder require_sudo() after print_separator()
- *(sudo)* use require_sudo for windows commands
- *(sudo)* prevent sudo_command = "sudo" finding gsudo
- *(sudo)* set sudo flags depending on kind
- skip gcloud update step if component manager is disabled ([#1237](https://github.com/topgrade-rs/topgrade/pull/1237))
- *(i18n)* use double-quotes for translations with newlines
- *(powershell)* run microsoft_store command directly
- *(powershell)* remove mentions of USOClient
- *(powershell)* execution policy check breaks when run in pwsh
- *(powershell)* don't use sudo with Update-Module for pwsh
- *(powershell)* add -Command to module update cmdline
- *(tmux)* support all default `tpm` locations (xdg and both hardcoded locations) ([#1146](https://github.com/topgrade-rs/topgrade/pull/1146))
- fixed the German translation for "y/n/s/q" ([#1220](https://github.com/topgrade-rs/topgrade/pull/1220))
### Other
- *(release)* switch to release-plz ([#1333](https://github.com/topgrade-rs/topgrade/pull/1333))
- *(pre-commit)* Make pre-commit.ci use conventional commits ([#1388](https://github.com/topgrade-rs/topgrade/pull/1388))
- *(pre-commit)* pre-commit autoupdate ([#1383](https://github.com/topgrade-rs/topgrade/pull/1383))
- *(deps)* bump actions/upload-artifact from 4.6.2 to 5.0.0 ([#1382](https://github.com/topgrade-rs/topgrade/pull/1382))
- *(deps)* bump github/codeql-action from 4.30.9 to 4.31.0 ([#1379](https://github.com/topgrade-rs/topgrade/pull/1379))
- *(deps)* bump actions/download-artifact from 5.0.0 to 6.0.0 ([#1380](https://github.com/topgrade-rs/topgrade/pull/1380))
- *(deps)* bump taiki-e/install-action from 2.62.33 to 2.62.38 ([#1381](https://github.com/topgrade-rs/topgrade/pull/1381))
- *(pre-commit)* Fix pre-commit-config.yaml ([#1378](https://github.com/topgrade-rs/topgrade/pull/1378))
- *(release)* Add .deb auto completion script ([#1353](https://github.com/topgrade-rs/topgrade/pull/1353))
- *(deps)* bump github/codeql-action from 4.30.8 to 4.30.9 ([#1369](https://github.com/topgrade-rs/topgrade/pull/1369))
- *(deps)* bump taiki-e/install-action from 2.62.28 to 2.62.33 ([#1368](https://github.com/topgrade-rs/topgrade/pull/1368))
- *(deps)* bump actions/dependency-review-action from 4.8.0 to 4.8.1 ([#1362](https://github.com/topgrade-rs/topgrade/pull/1362))
- *(deps)* bump softprops/action-gh-release from 2.3.4 to 2.4.1 ([#1364](https://github.com/topgrade-rs/topgrade/pull/1364))
- *(deps)* bump taiki-e/install-action from 2.62.21 to 2.62.28 ([#1363](https://github.com/topgrade-rs/topgrade/pull/1363))
- *(deps)* bump github/codeql-action from 3.30.6 to 4.30.8 ([#1365](https://github.com/topgrade-rs/topgrade/pull/1365))
- *(deps)* bump github/codeql-action from 3.30.5 to 3.30.6 ([#1355](https://github.com/topgrade-rs/topgrade/pull/1355))
- *(deps)* bump softprops/action-gh-release from 2.3.3 to 2.3.4 ([#1356](https://github.com/topgrade-rs/topgrade/pull/1356))
- *(deps)* bump taiki-e/install-action from 2.62.13 to 2.62.21 ([#1357](https://github.com/topgrade-rs/topgrade/pull/1357))
- *(deps)* bump ossf/scorecard-action from 2.4.2 to 2.4.3 ([#1358](https://github.com/topgrade-rs/topgrade/pull/1358))
- *(deps)* bump actions/dependency-review-action from 4.7.3 to 4.8.0 ([#1350](https://github.com/topgrade-rs/topgrade/pull/1350))
- *(deps)* bump github/codeql-action from 3.30.3 to 3.30.5 ([#1349](https://github.com/topgrade-rs/topgrade/pull/1349))
- *(deps)* bump taiki-e/install-action from 2.62.1 to 2.62.13 ([#1351](https://github.com/topgrade-rs/topgrade/pull/1351))
- *(deps)* bump actions/cache from 4.2.4 to 4.3.0 ([#1352](https://github.com/topgrade-rs/topgrade/pull/1352))
- Fix WSL distribution name cleanup ([#1348](https://github.com/topgrade-rs/topgrade/pull/1348))
- *(pyproject)* mark version as dynamic ([#1347](https://github.com/topgrade-rs/topgrade/pull/1347))
- *(deps)* replace winapi with windows
- *(sudo)* rename interactive to login_shell
- Fix "WSL already reported" panic ([#1344](https://github.com/topgrade-rs/topgrade/pull/1344))
- Move step logic out of Powershell struct ([#1345](https://github.com/topgrade-rs/topgrade/pull/1345))
- *(deps)* bump taiki-e/install-action from 2.61.5 to 2.62.1 ([#1335](https://github.com/topgrade-rs/topgrade/pull/1335))
- *(deps)* bump Swatinem/rust-cache from 2.8.0 to 2.8.1 ([#1336](https://github.com/topgrade-rs/topgrade/pull/1336))
- Fixes for #1188; custom_commands broken ([#1332](https://github.com/topgrade-rs/topgrade/pull/1332))
- use login shell when executing topgrade ([#1327](https://github.com/topgrade-rs/topgrade/pull/1327))
- *(deps)* bump taiki-e/install-action from 2.60.0 to 2.61.5 ([#1325](https://github.com/topgrade-rs/topgrade/pull/1325))
- *(deps)* bump github/codeql-action from 3.30.1 to 3.30.3 ([#1324](https://github.com/topgrade-rs/topgrade/pull/1324))
- *(pre-commit)* add typos with conservative excludes; no content changes ([#1317](https://github.com/topgrade-rs/topgrade/pull/1317))
- fix simple typos in code and comments (split var, whether, Extensions) ([#1318](https://github.com/topgrade-rs/topgrade/pull/1318))
- *(deps)* bump github/codeql-action from 3.29.11 to 3.30.1 ([#1301](https://github.com/topgrade-rs/topgrade/pull/1301))
- *(deps)* bump softprops/action-gh-release from 2.3.2 to 2.3.3 ([#1302](https://github.com/topgrade-rs/topgrade/pull/1302))
- *(deps)* bump taiki-e/install-action from 2.58.21 to 2.60.0 ([#1303](https://github.com/topgrade-rs/topgrade/pull/1303))
- *(deps)* bump actions/dependency-review-action from 4.7.2 to 4.7.3 ([#1304](https://github.com/topgrade-rs/topgrade/pull/1304))
- *(deps)* bump actions/attest-build-provenance from 2.4.0 to 3.0.0 ([#1305](https://github.com/topgrade-rs/topgrade/pull/1305))
- update tracing-subscriber to ~0.3.20 (ANSI escape injection fix, GHSA-xwfj-jgwm-7wp5) ([#1288](https://github.com/topgrade-rs/topgrade/pull/1288))
- *(deps)* bump github/codeql-action from 3.29.8 to 3.29.11 ([#1281](https://github.com/topgrade-rs/topgrade/pull/1281))
- *(deps)* bump actions/dependency-review-action from 4.7.1 to 4.7.2 ([#1282](https://github.com/topgrade-rs/topgrade/pull/1282))
- *(deps)* bump taiki-e/install-action from 2.58.9 to 2.58.21 ([#1283](https://github.com/topgrade-rs/topgrade/pull/1283))
- *(deps)* bump PyO3/maturin-action from 1.49.3 to 1.49.4 ([#1285](https://github.com/topgrade-rs/topgrade/pull/1285))
- *(deps)* bump actions/cache from 4.2.3 to 4.2.4 ([#1284](https://github.com/topgrade-rs/topgrade/pull/1284))
- Support "Insiders" versions of VSCode and VSCodium ([#1279](https://github.com/topgrade-rs/topgrade/pull/1279))
- Sudo preserve env list argument is `--preserve-env` ([#1276](https://github.com/topgrade-rs/topgrade/pull/1276))
- Clippy fixes from rust 1.91 nightly ([#1267](https://github.com/topgrade-rs/topgrade/pull/1267))
- *(deps)* bump actions/checkout from 4.2.2 to 5.0.0 ([#1264](https://github.com/topgrade-rs/topgrade/pull/1264))
- *(deps)* bump actions/download-artifact from 4.3.0 to 5.0.0 ([#1263](https://github.com/topgrade-rs/topgrade/pull/1263))
- *(deps)* bump taiki-e/install-action from 2.58.0 to 2.58.9 ([#1261](https://github.com/topgrade-rs/topgrade/pull/1261))
- *(deps)* bump ossf/scorecard-action from 2.4.0 to 2.4.2 ([#1262](https://github.com/topgrade-rs/topgrade/pull/1262))
- *(deps)* bump github/codeql-action from 3.29.5 to 3.29.8 ([#1265](https://github.com/topgrade-rs/topgrade/pull/1265))
- *(ci)* Dependabot, workflow security ([#1257](https://github.com/topgrade-rs/topgrade/pull/1257))
- replace once_cell crate with std equivalent ([#1260](https://github.com/topgrade-rs/topgrade/pull/1260))
- *(deps)* bump tokio from 1.38 to 1.47 ([#1256](https://github.com/topgrade-rs/topgrade/pull/1256))
- *(app.yml)* fix fr language #1248
- *(sudo)* add SudoKind::WinSudo
- *(sudo)* add SudoExecuteOpts builder functions and preserve_env enum
- *(yarn)* remove unnecessary Yarn::yarn field
- *(apt)* extract detect_apt() function
- route sudo usage through Sudo::execute*
- move RunType::execute to ExecutionContext
- *(powershell)* store powershell path directly
- *(powershell)* cleanup and simplify code
- Move step running into enum for dynamic ordering ([#1188](https://github.com/topgrade-rs/topgrade/pull/1188))
- Generate artifact attestations for release assets ([#1216](https://github.com/topgrade-rs/topgrade/pull/1216))
- windows update, use explicit reboot policy ([#1143](https://github.com/topgrade-rs/topgrade/pull/1143))
- add Discord invite link to README ([#1203](https://github.com/topgrade-rs/topgrade/pull/1203))
- Catch secondary uv self-update error ([#1201](https://github.com/topgrade-rs/topgrade/pull/1201))
- Handle another format change in asdf version ([#1194](https://github.com/topgrade-rs/topgrade/pull/1194))
- Preserve custom command order from config instead of sorting alphabetically ([#1182](https://github.com/topgrade-rs/topgrade/pull/1182))
- Add support for multiple binary names and idea having multiple binaries ([#1167](https://github.com/topgrade-rs/topgrade/pull/1167))
- fix the invalid action version ([#1185](https://github.com/topgrade-rs/topgrade/pull/1185))
- allow us to re-run AUR CI ([#1184](https://github.com/topgrade-rs/topgrade/pull/1184))
- Update Yazi upgrade step to use ya pkg. ([#1163](https://github.com/topgrade-rs/topgrade/pull/1163))
- use the new tag name and specify shell to bash ([#1183](https://github.com/topgrade-rs/topgrade/pull/1183))
- allow specifying tag when manually run 'create_release_assets.yml' ([#1180](https://github.com/topgrade-rs/topgrade/pull/1180))
- fix homebrew ci, remove duplicate trigger event ([#1179](https://github.com/topgrade-rs/topgrade/pull/1179))
- fix PyPI pipeline duplicate wheel name ([#1178](https://github.com/topgrade-rs/topgrade/pull/1178))
- add event workflow_dispatch to release pipelines ([#1177](https://github.com/topgrade-rs/topgrade/pull/1177))
- fix pipeline release to PyPI ([#1176](https://github.com/topgrade-rs/topgrade/pull/1176))
- Install rustfmt and clippy where necessary ([#1171](https://github.com/topgrade-rs/topgrade/pull/1171))

View File

@@ -7,24 +7,47 @@ We welcome and encourage contributions of all kinds, such as:
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.
### LLM/AI guidelines
## Adding a new `step`
You may use LLMs (AI tools) for:
In `topgrade`'s term, package manager is called `step`.
To add a new `step` to `topgrade`:
* Inspiration, problem solving, help with Rust, translation, etc.
* Generating small and self-contained snippets of code (e.g., shell scripts or utility functions)
Do **not** use LLMs to:
* Generate ("vibe code") entire pull requests
* Write or generate issue or pull request descriptions
### General guidelines
**Please use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) for your PR title**.
We use [pre-commit](https://github.com/pre-commit/pre-commit). It runs in CI, but you can optionally install the hook
locally with `pre-commit install`. If you don't want to use pre-commit, make sure the following pass before submitting
your PR:
```shell
$ cargo fmt
$ cargo clippy
$ cargo test
```
### Adding a new step
In `topgrade`'s terms, a package manager (or something else that can be upgraded) is called a 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)
[`enum Step`](https://github.com/topgrade-rs/topgrade/blob/main/src/step.rs)
```rust
pub enum Step {
// Existed steps
// Existing steps
// ...
// Your new step here!
// You may want it to be sorted alphabetically because that looks great:)
// Make sure it stays sorted alphabetically because that looks great :)
Xxx,
}
```
@@ -32,9 +55,10 @@ To add a new `step` to `topgrade`:
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).
a file under [`src/steps`](https://github.com/topgrade-rs/topgrade/tree/main/src/steps),
the file names are self-explanatory, for example, steps related to `zsh` are
placed in [`steps/zsh.rs`](https://github.com/topgrade-rs/topgrade/blob/main/src/steps/zsh.rs), and steps that run on
Linux only are placed in [`steps/linux.rs`](https://github.com/topgrade-rs/topgrade/blob/main/src/steps/linux.rs).
Then you implement the update function, and put it in the file where it belongs.
@@ -47,38 +71,35 @@ To add a new `step` to `topgrade`:
print_separator("xxx");
// Invoke the new step to get things updated!
ctx.run_type()
.execute("xxx")
ctx.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)).
Such an update function would be conventionally named `run_xxx()`, where `xxx`
is the name of the new step, and it should take an argument of type
`&ExecutionContext`.
Update function would usually do 3 things:
1. Check if the step is installed
2. Output the Separator
3. Invoke the step
The update function should usually do 3 things:
1. Check if the step is installed
2. Output the separator
3. Execute commands
Still, this is sufficient for most tools, but you may need some extra stuff
with complicated `step`.
This is sufficient for most tools, but you may need some extra stuff
for complicated steps.
3. Finally, invoke that update function in `main.rs`
3. Add a match arm to `Step::run()`
```rust
runner.execute(Step::Xxx, "xxx", || ItsModule::run_xxx(&ctx))?;
Xxx => runner.execute(*self, "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
to separate the steps. For example, for steps that are Linux-only, it goes
like this:
```
```rust
#[cfg(target_os = "linux")]
{
// Xxx is Linux-only
@@ -86,25 +107,31 @@ To add a new `step` to `topgrade`:
}
```
Congrats, you just added a new `step`:)
4. Finally, add the step to `default_steps()` in `step.rs`
```rust
steps.push(Xxx)
```
Keep the conditional compilation the same as in the above step 3.
## Modification to the configuration entries
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))
(in [`src/config.rs`](https://github.com/topgrade-rs/topgrade/blob/main/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),
[`config.example.toml`](https://github.com/topgrade-rs/topgrade/blob/main/config.example.toml),
and have some basic documentations guiding user how to use these options.
## Breaking changes
### 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:
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.
@@ -116,37 +143,40 @@ it should be written in Markdown and wrapped at 80, for example:
[bc_dev]: https://github.com/topgrade-rs/topgrade/blob/main/BREAKINGCHANGES_dev.md
## Before you submit your PR
### I18n
Make sure your patch passes the following tests on your host:
If your PR introduces user-facing messages, we need to ensure they are translated.
Please add the translations to [`locales/app.yml`][app_yml]. For simple messages
without arguments (e.g., "hello world"), we can simply translate them according
(Tip: LLMs are good at translation). If a message contains
arguments, e.g., "hello <NAME>", please follow this convention:
```shell
$ cargo build
$ cargo fmt
$ cargo clippy
$ cargo test
```yml
"hello {name}": # key
en: "hello %{name}" # translation
```
Don't worry about other platforms, we have most of them covered in our CI.
Arguments in the key should be in format `{argument_name}`, and they will have
a preceding `%` when used in translations.
## Some tips
[app_yml]: https://github.com/topgrade-rs/topgrade/blob/main/locales/app.yml
1. Locale
### Locales
Some `step` respects locale, which means their output can be in language other
than English, we should not do check on it.
Some steps respect locale, which means their output can be in language other
than English. In those cases, we cannot rely on the output of a command.
For example, one may want to check if a tool works by doing this:
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");
```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 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.
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.

2617
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,54 +3,53 @@ name = "topgrade"
description = "Upgrade all the things"
categories = ["os"]
keywords = ["upgrade", "update"]
license = "GPL-3.0"
license = "GPL-3.0-or-later"
repository = "https://github.com/topgrade-rs/topgrade"
version = "15.0.0"
rust-version = "1.87.0"
version = "16.4.2"
authors = ["Roey Darwish Dror <roey.ghost@gmail.com>", "Thomas Schönauer <t.schoenauer@hgs-wt.at>"]
exclude = ["doc/screenshot.gif", "BREAKINGCHANGES_dev.md"]
edition = "2021"
edition = "2024"
readme = "README.md"
[[bin]]
name = "topgrade"
path = "src/main.rs"
##[lib]
##name = "topgrade_lib"
[dependencies]
home = "~0.5"
etcetera = "~0.8"
once_cell = "~1.19"
etcetera = "~0.11.0"
serde = { version = "~1.0", features = ["derive"] }
toml = "0.8"
which_crate = { version = "~6.0", package = "which" }
toml = { version = "~0.9.8", features = ["preserve_order"] }
which_crate = { version = "~8.0", package = "which" }
shellexpand = "~3.1"
clap = { version = "~4.5", features = ["cargo", "derive"] }
clap_complete = "~4.5"
clap_mangen = "~0.2"
walkdir = "~2.5"
console = "~0.15"
lazy_static = "~1.4"
console = "~0.16"
chrono = "~0.4"
glob = "~0.3"
strum = { version = "~0.26", features = ["derive"] }
thiserror = "~1.0"
tempfile = "~3.10"
cfg-if = "~1.0"
tokio = { version = "~1.38", features = ["process", "rt-multi-thread"] }
strum = { version = "~0.27", features = ["derive"] }
thiserror = "~2.0"
tempfile = "~3.23"
tokio = { version = "~1.48", features = ["process", "rt-multi-thread"] }
futures = "~0.3"
regex = "~1.10"
regex = "~1.12"
semver = "~1.0"
shell-words = "~1.1"
color-eyre = "~0.6"
tracing = { version = "~0.1", features = ["attributes", "log"] }
tracing-subscriber = { version = "~0.3", features = ["env-filter", "time"] }
tracing-subscriber = { version = "~0.3.20", 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"
jetbrains-toolbox-updater = "5.0.0"
indexmap = { version = "2.9.0", features = ["serde"] }
serde_json = "1.0.145"
clap-cargo = "0.18.0"
[patch.crates-io]
mac-notification-sys = { git = "https://github.com/h4llow3En/mac-notification-sys" }
[package.metadata.generate-rpm]
assets = [{ source = "target/release/topgrade", dest = "/usr/bin/topgrade" }]
@@ -59,17 +58,37 @@ assets = [{ source = "target/release/topgrade", dest = "/usr/bin/topgrade" }]
git = "*"
[package.metadata.deb]
depends = "$auto,git"
name = "topgrade"
maintainer = "Chris Gelatt <kreeblah@gmail.com>"
copyright = "2025, 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
assets = [
["target/release/topgrade", "usr/bin/", "755"],
["README.md", "usr/share/doc/topgrade/README.md", "644"],
# The man page and shell completions are automatically generated by topgrade's build process in CI,
# so these files aren't actually committed.
["deployment/deb/topgrade.1", "usr/share/man/man1/topgrade.1", "644"],
["deployment/deb/topgrade.bash", "usr/share/bash-completion/completions/topgrade", "644"],
["deployment/deb/topgrade.fish", "usr/share/fish/vendor_completions.d/topgrade.fish", "644"],
["deployment/deb/_topgrade", "usr/share/zsh/vendor-completions/", "644"],
]
[target.'cfg(unix)'.dependencies]
nix = { version = "~0.29", features = ["hostname", "signal", "user"] }
nix = { version = "~0.30", features = ["hostname", "signal", "user"] }
rust-ini = "~0.21"
self_update_crate = { version = "~0.40", default-features = false, optional = true, package = "self_update", features = ["archive-tar", "compression-flate2", "rustls"] }
self_update_crate = { version = "~0.42", default-features = false, optional = true, package = "self_update", features = ["archive-tar", "compression-flate2", "rustls"] }
[target.'cfg(windows)'.dependencies]
self_update_crate = { version = "~0.40", default-features = false, optional = true, package = "self_update", features = ["archive-zip", "compression-zip-deflate", "rustls"] }
winapi = "~0.3"
is_elevated = "~0.1"
parselnk = "~0.1"
self_update_crate = { version = "~0.42", default-features = false, optional = true, package = "self_update", features = ["archive-zip", "compression-zip-deflate", "rustls"] }
windows = { version = "~0.62", features = ["Win32_System_Console"] }
windows-registry = "~0.6"
[profile.release]
lto = true

View File

@@ -2,16 +2,15 @@
<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://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://formulae.brew.sh/formula/topgrade"><img alt="Homebrew" src="https://img.shields.io/homebrew/v/topgrade.svg"></a>
<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://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://formulae.brew.sh/formula/topgrade"><img alt="Homebrew" src="https://img.shields.io/homebrew/v/topgrade.svg"></a>
<img alt="Demo" src="doc/topgrade_demo.gif">
</div>
## Introduction
> **Note**
@@ -25,34 +24,45 @@ To remedy this, **Topgrade** detects which tools you use and runs the appropriat
[![Packaging status](https://repology.org/badge/vertical-allrepos/topgrade.svg)](https://repology.org/project/topgrade/versions)
- 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: [Scoop][scoop] or [Winget][winget]
- PyPi: [pip](https://pypi.org/project/topgrade/)
### Official
[scoop]: https://scoop.sh/#/apps?q=topgrade
[winget]: https://winstall.app/apps/topgrade-rs.topgrade
- Self-updating binary (all platforms): [releases](https://github.com/topgrade-rs/topgrade/releases)
- Install from source (all platforms): [`cargo install topgrade`](https://crates.io/crates/topgrade)
- Debian/Ubuntu ([deb-get](https://github.com/wimpysworld/deb-get)):
[`deb-get install topgrade`](https://github.com/wimpysworld/deb-get/blob/main/01-main/packages/topgrade)
- Arch Linux (AUR): [topgrade](https://aur.archlinux.org/packages/topgrade)
or [topgrade-bin](https://aur.archlinux.org/packages/topgrade-bin)
- [PyPi](https://pypi.org/): `pip`, `pipx`, or `uv tool` [
`install topgrade`](https://pypi.org/project/topgrade/)
- Windows ([Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/)): [
`winget install --id=topgrade-rs.topgrade -e`](https://winstall.app/apps/topgrade-rs.topgrade)
- macOS or Linux ([Homebrew](https://brew.sh/)): [`brew install topgrade`](https://formulae.brew.sh/formula/topgrade)
- Fedora/RHEL/AlmaLinux/CentOS-Stream ([Copr](https://copr.fedorainfracloud.org/)): [
`sudo dnf copr enable lilay/topgrade && sudo dnf install topgrade`](https://copr.fedorainfracloud.org/coprs/lilay/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.
### Community-maintained
> Currently, Topgrade requires Rust 1.65 or above. In general, Topgrade tracks
> the latest stable toolchain.
- Windows ([Chocolatey](https://chocolatey.org/)): [
`choco install topgrade`](https://community.chocolatey.org/packages/topgrade)
- Windows ([Scoop](https://scoop.sh/)): [
`scoop bucket add main && scoop install main/topgrade`](https://scoop.sh/#/apps?q=topgrade)
- macOS ([MacPorts](https://www.macports.org/)): [
`sudo port install topgrade`](https://ports.macports.org/port/topgrade/)
- NixOS or Nix (nixpkgs): [topgrade](https://search.nixos.org/packages?show=topgrade)
- Void Linux: [`sudo xbps-install -S topgrade`](https://voidlinux.org/packages/?arch=x86_64&q=topgrade)
## Usage
Just run `topgrade`.
## Configuration
## Configuration
See `config.example.toml` for an example configuration file.
## Migration and Breaking Changes
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
and we will document these changes in the release note, please take a look at
it when updated to a major release.
> Got a question? Feel free to open an issue or discussion!
@@ -60,6 +70,7 @@ it when updated to a major release.
### Configuration Path
#### `CONFIG_DIR` on each platform
- **Windows**: `%APPDATA%`
- **macOS** and **other Unix systems**: `${XDG_CONFIG_HOME:-~/.config}`
@@ -68,16 +79,21 @@ it when updated to a major release.
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.
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.
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`).
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.
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
@@ -96,18 +112,14 @@ Open a new issue describing your problem and if possible provide a solution.
### Missing a feature or found an unsupported tool/distro?
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?
Just fork the repository and start coding.
### Contribution Guidelines
### Want to contribute?
See [CONTRIBUTING.md](https://github.com/topgrade-rs/topgrade/blob/master/CONTRIBUTING.md)
## Roadmap
## Discord server
- [ ] Add a proper testing framework to the code base.
- [ ] Add unit tests for package managers.
- [ ] Split up code into more maintainable parts, eg. putting every linux package manager in a own submodule of linux.rs.
Welcome to [join](https://discord.gg/Q8HGGWundY) our Discord server if you want
to discuss Topgrade!

View File

@@ -1,65 +1,21 @@
> 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.
Non-major versions go via release-plz.
1. bumps the version number.
> If there are breaking changes, the major version number should be increased.
2. Overwrite [`BREAKINGCHANGES`][breaking_changes] with
2. If the major versioin number gets bumped, update [SECURITY.md][SECURITY_file_link].
[SECURITY_file_link]: https://github.com/topgrade-rs/topgrade/blob/main/SECURITY.md
3. Overwrite [`BREAKINGCHANGES`][breaking_changes] with
[`BREAKINGCHANGES_dev`][breaking_changes_dev], and create a new dev file:
```sh'
$ cd topgrade
$ cp BREAKINGCHANGES_dev.md BREAKINGCHANGES.md
$ mv BREAKINGCHANGES_dev.md BREAKINGCHANGES.md
$ touch BREAKINGCHANGES_dev.md
```
[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

View File

@@ -1,11 +1,9 @@
# Security Policy
## Reporting a vulnerability
To report a security vulnerability, go to [the security tab](https://github.com/topgrade-rs/topgrade/security) and click "Report a vulnerability".
## Supported Versions
We only support the latest major version and each subversion.
| Version | Supported |
| -------- | ------------------ |
| 10.0.x | :white_check_mark: |
| < 10.0 | :x: |
We only support the latest version of Topgrade. Fixes are not backported.

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env sh
#!/usr/bin/env bash
build_function() {
rustup update
cargo install cross
@@ -20,7 +21,7 @@ build_function() {
package_function() {
cd build
cd build || exit 1
mkdir x86_64-unknown-linux-gnu/
mkdir x86_64-unknown-linux-musl/
mkdir x86_64-unknown-freebsd/
@@ -35,28 +36,28 @@ package_function() {
cp ../target/aarch64-unknown-linux-musl/release/topgrade aarch64-unknown-linux-musl/topgrade
cp ../target/x86_64-pc-windows-gnu/release/topgrade.exe x86_64-pc-windows-gnu/topgrade.exe
cd x86_64-unknown-linux-gnu/
tar -czf ../topgrade-${ans}-x86_64-linux-gnu.tar.gz topgrade
cd x86_64-unknown-linux-gnu/ || exit 1
tar -czf "../topgrade-${ans}-x86_64-linux-gnu.tar.gz" topgrade
cd ..
cd x86_64-unknown-linux-musl
tar -czf ../topgrade-${ans}-x86_64-linux-musl.tar.gz topgrade
cd x86_64-unknown-linux-musl/ || exit 1
tar -czf "../topgrade-${ans}-x86_64-linux-musl.tar.gz" topgrade
cd ..
cd x86_64-unknown-freebsd/
tar -czf ../topgrade-${ans}-x86_64-freebsd.tar.gz topgrade
cd x86_64-unknown-freebsd/ || exit 1
tar -czf "../topgrade-${ans}-x86_64-freebsd.tar.gz" topgrade
cd ..
cd aarch64-unknown-linux-gnu/
tar -czf ../topgrade-${ans}-aarch64-linux-gnu.tar.gz topgrade
cd aarch64-unknown-linux-gnu/ || exit 1
tar -czf "../topgrade-${ans}-aarch64-linux-gnu.tar.gz" topgrade
cd ..
cd aarch64-unknown-linux-musl/
tar -czf ../topgrade-${ans}-aarch64-linux-musl.tar.gz topgrade
cd aarch64-unknown-linux-musl/ || exit 1
tar -czf "../topgrade-${ans}-aarch64-linux-musl.tar.gz" topgrade
cd ..
cd x86_64-pc-windows-gnu/
zip -q ../topgrade-${ans}-x86_64-windows.zip topgrade.exe
cd x86_64-pc-windows-gnu/ || exit 1
zip -q "../topgrade-${ans}-x86_64-windows.zip" topgrade.exe
cd ..
cd ..
@@ -65,17 +66,19 @@ package_function() {
print_checksums() {
cd build/
sha256sum topgrade-${ans}-*
cd build/ || exit 1
sha256sum topgrade-"${ans}"-*
cd ../
}
while true; do
echo "You should always have a look on scripts you download from the internet."
# shellcheck disable=SC2162
read -p "Do you still want to proceed? (y/n) " yn
echo -n "Input version number: "
# shellcheck disable=SC2162
read ans
mkdir build

View File

@@ -6,6 +6,13 @@
[misc]
# On Unix systems, Topgrade should not be run as root, it
# will run commands with sudo or equivalent where needed.
# Set this to true to suppress the warning and confirmation
# prompt if Topgrade detects it is being run as root.
# (default: false)
# allow_root = false
# 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)
@@ -44,9 +51,19 @@
# Do not ask to retry failed steps (default: false)
# no_retry = true
# Show the reason for skipped steps (default: false)
# This has no effect if the "only" option is specified
# show_skipped = true
# Run inside tmux (default: false)
# run_in_tmux = true
# Changes the way topgrade interacts with
# the tmux session, creating the session
# and only attaching to it if not inside tmux
# (default: "attach_if_not_in_session", allowed values: "attach_if_not_in_session", "attach_always")
# tmux_session_mode = "attach_if_not_in_session"
# Cleanup temporary or old files (default: false)
# cleanup = true
@@ -74,6 +91,11 @@
# See: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives
# log_filters = ["topgrade::command=debug", "warn"]
# Whether to show a distribution-specific summary if applicable, e.g. listing
# Pacman backup configuration files (*.pacsave and *.pacnew)
# (default: true)
# show_distribution_summary = false
# Commands to run before anything
[pre_commands]
@@ -97,6 +119,26 @@
# enable_pipupgrade = true ###disabled by default
# pipupgrade_arguments = "-y -u --pip-path pip" ###disabled by default
# For the poetry step, by default, Topgrade skips its update if poetry is not
# installed with the official script. This configuration entry forces Topgrade
# to run the update in this case.
#
# (default: false)
# poetry_force_self_update = true
[conda]
# Additional named conda environments to update (`conda env update -n env_name`)
# env_names = [
# "Toolbox",
# "PyTorch"
# ]
# Additional conda environment paths to update (`conda env update -p env_path`)
# env_paths = [
# "~/webserver/.conda/",
# "~/experiments/.conda/"
# ]
[composer]
# self_update = true
@@ -114,6 +156,12 @@
# both of them, they won't clash with each other.
# greedy_latest = true
# For the BrewCask step
# If `Repo Cask Upgrade` does not exist, then use the `--greedy_auto_updates` option.
# NOTE: the above entry `greedy_cask` contains this entry, though you can enable
# both of them, they won't clash with each other.
# greedy_auto_updates = true
# For the BrewFormula step
# Execute `brew autoremove` after the step.
# autoremove = true
@@ -160,6 +208,11 @@
# rpm_ostree = false
# For Fedora/CentOS/RHEL Atomic variants, if `bootc` is available and this configuration entry is set to true, use
# it to do the update - Will also supersede rpm-ostree if enabled
# (default: false)
# bootc = false
# nix_arguments = "--flake"
# nix_env_arguments = "--prebuilt-only"
@@ -168,6 +221,20 @@
# home_manager_arguments = ["--flake", "file"]
[mandb]
# Enable the mandb step (to update manual entries).
# Mandb is updated in the background by a service on most systems by default.
# (default: false)
# enable = true
[pkgfile]
# Enable the pkgfile step (to update the pkgfile database).
# Pkgfile is sometimes installed by default, but often not used and heavy to update.
# (default: false)
# enable = true
[git]
# How many repos to pull at max in parallel
# max_concurrency = 5
@@ -189,17 +256,54 @@
# Manually select Windows updates
# accept_all_updates = false
# Controls whether to automatically reboot the computer when updates are
# installed that request it. (default: "no", allowed values: "yes", "no", "ask")
# updates_auto_reboot = "yes"
# open_remotes_in_new_terminal = true
# wsl_update_pre_release = true
# wsl_update_use_web_download = true
# The default for winget_install_silently is true,
# this example turns off silent install.
# winget_install_silently = false
# Causes Topgrade to rename itself during the run to allow package managers
# to upgrade it. Use this only if you installed Topgrade by using a package
# manager such as Scoop or Cargo
# self_rename = true
# Use sudo to elevate privileges for the Windows Package Manager (winget)
# Only use this option if you want to run the Winget step in sudo-mode.
# Running winget in sudo-mode is generally not recommended, as not every
# package supports installing / upgrading in sudo-mode and it may cause issues
# with some packages or may even cause the Winget-step to fail.
# If any problems occur, please try running Topgrade without this option first
# before reporting an issue.
# (default: false)
# winget_use_sudo = true
[chezmoi]
# Exclude encrypted files from update
# (default: false)
# exclude_encrypted = false
[mise]
# Upgrades to the latest version available, bumping the version in mise.toml
# (default: false)
# bump = false
# Number of jobs to run in parallel
# (default: 4)
# jobs = 4
# Run interactively
# (default: false)
# interactive = false
[npm]
# Use sudo if the NPM directory isn't owned by the current user
@@ -211,6 +315,11 @@
# use_sudo = true
[deno]
# Upgrade deno executable to the given version.
# version = "stable"
[vim]
# For `vim-plug`, execute `PlugUpdate!` instead of `PlugUpdate`
# force_plug_update = true
@@ -244,10 +353,72 @@
[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"
# Run 'docker system prune' to clean up unused containers, networks, and build cache
# (default: false)
# system_prune = false
[lensfun]
# If disabled, Topgrade invokes `lensfunupdatedata` without root priviledge,
# If disabled, Topgrade invokes `lensfunupdatedata` without root privilege,
# then the update will be only available to you. Otherwise, `sudo` is required,
# and the update will be installed system-wide, i.e., available to all users.
# (default: false)
# use_sudo = false
[julia]
# If disabled, Topgrade invokes julia with the --startup-file=no CLI option.
#
# This may be desirable to avoid loading outdated packages with "using" directives
# in the startup file, which might cause the update run to fail.
# (default: true)
# startup_file = true
[zigup]
# Version strings passed to zigup.
# These may be pinned versions such as "0.13.0" or branches such as "master".
# Each one will be updated in its own zigup invocation.
# (default: ["master"])
# target_versions = ["master", "0.13.0"]
# Specifies the directory that the zig files will be installed to.
# If defined, passed with the --install-dir command line flag.
# If not defined, zigup will use its default behaviour.
# (default: not defined)
# install_dir = "~/.zig"
# Specifies the path of the symlink which will be set to point at the default compiler version.
# If defined, passed with the --path-link command line flag.
# If not defined, zigup will use its default behaviour.
# This is not meaningful if set_default is not enabled.
# (default: not defined)
# path_link = "~/.bin/zig"
# If enabled, run `zigup clean` after updating all versions.
# If enabled, each updated version above will be marked with `zigup keep`.
# (default: false)
# cleanup = false
[vscode]
# If this is set and is a non-empty string, it specifies the profile the
# extensions should be updated for.
# (default: this won't be set by default)
# profile = ""
[pixi]
# Show the release notes of the latest pixi release
# during the pixi step
# (default: false)
# include_release_notes = false
[doom]
# If this is set to true, the `--aot` flag is added to `doom upgrade`,
# which enables ahead-of-time native compilation of packages.
# (default: false)
# aot = true
[rustup]
# If set, updates only these channels.
# (default: [] (all channels))
# channels = ["stable"]

1420
locales/app.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,21 @@ build-backend = "maturin"
[project]
name = "topgrade"
dynamic = ["version"]
description = "Upgrade all the things"
readme = "README.md"
license = "GPL-3.0-or-later"
requires-python = ">=3.7"
classifiers = [
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
urls.bugs = "https://github.com/topgrade-rs/topgrade/issues"
urls.homepage = "https://github.com/topgrade-rs/topgrade"
[tool.maturin]
bindings = "bin"

10
renovate.json Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:best-practices",
":semanticCommits"
],
"lockFileMaintenance": {
"enabled": true
}
}

2
rust-toolchain.toml Normal file
View File

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

View File

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

View File

@@ -5,8 +5,8 @@ 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 color_eyre::eyre::eyre;
use crate::error::TopgradeError;
@@ -45,13 +45,13 @@ impl TryFrom<&Output> for Utf8Output {
type Error = eyre::Error;
fn try_from(Output { status, stdout, stderr }: &Output) -> Result<Self, Self::Error> {
let stdout = String::from_utf8(stdout.to_vec()).map_err(|err| {
let stdout = String::from_utf8(stdout.clone()).map_err(|err| {
eyre!(
"Stdout contained invalid UTF-8: {}",
String::from_utf8_lossy(err.as_bytes())
)
})?;
let stderr = String::from_utf8(stderr.to_vec()).map_err(|err| {
let stderr = String::from_utf8(stderr.clone()).map_err(|err| {
eyre!(
"Stderr contained invalid UTF-8: {}",
String::from_utf8_lossy(err.as_bytes())
@@ -115,6 +115,9 @@ pub trait CommandExt {
///
/// Returns an `Err` if the command failed to execute, if `succeeded` returns an `Err`, or if
/// the output contains invalid UTF-8.
// This function is currently unused, but is useful and makes sense with `output_checked_with`
// and `output_checked_utf8` existing.
#[allow(dead_code)]
#[track_caller]
fn output_checked_with_utf8(
&mut self,
@@ -149,6 +152,7 @@ pub trait CommandExt {
/// Like [`Command::spawn`], but gives a nice error message if the command fails to
/// execute.
#[track_caller]
#[allow(dead_code)]
fn spawn_checked(&mut self) -> eyre::Result<Self::Child>;
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,10 +1,10 @@
//! SIGINT handling in Unix systems.
use crate::ctrlc::interrupted::set_interrupted;
use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal};
use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal, sigaction};
/// Handle SIGINT. Set the interruption flag.
extern "C" fn handle_sigint(_: i32) {
set_interrupted()
set_interrupted();
}
/// Set the necessary signal handlers.

View File

@@ -1,22 +1,21 @@
//! A stub for Ctrl + C handling.
use crate::ctrlc::interrupted::set_interrupted;
use tracing::error;
use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE};
use winapi::um::consoleapi::SetConsoleCtrlHandler;
use winapi::um::wincon::CTRL_C_EVENT;
use windows::Win32::System::Console::{CTRL_C_EVENT, SetConsoleCtrlHandler};
use windows::core::BOOL;
extern "system" fn handler(ctrl_type: DWORD) -> BOOL {
extern "system" fn handler(ctrl_type: u32) -> BOOL {
match ctrl_type {
CTRL_C_EVENT => {
set_interrupted();
TRUE
true.into()
}
_ => FALSE,
_ => false.into(),
}
}
pub fn set_handler() {
if 0 == unsafe { SetConsoleCtrlHandler(Some(handler), TRUE) } {
error!("Cannot set a control C handler")
if let Err(e) = unsafe { SetConsoleCtrlHandler(Some(handler), true) } {
error!("Cannot set a control C handler: {e}")
}
}

View File

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

View File

@@ -1,12 +1,48 @@
#![allow(dead_code)]
use crate::executor::RunType;
use crate::sudo::Sudo;
use crate::utils::{require_option, REQUIRE_SUDO};
use crate::{config::Config, executor::Executor};
use color_eyre::eyre::Result;
use std::env::var;
use std::path::Path;
use std::sync::Mutex;
use std::ffi::OsStr;
use std::process::Command;
use std::sync::{LazyLock, Mutex};
use clap::ValueEnum;
use color_eyre::eyre::Result;
use rust_i18n::t;
use serde::Deserialize;
use strum::EnumString;
use crate::config::Config;
use crate::error::MissingSudo;
use crate::executor::{DryCommand, Executor};
use crate::powershell::Powershell;
#[cfg(target_os = "linux")]
use crate::steps::linux::Distribution;
use crate::sudo::Sudo;
use crate::utils::require_option;
/// An enum telling whether Topgrade should perform dry runs or actually perform the steps.
#[derive(Clone, Copy, Debug, Deserialize, Default, EnumString, ValueEnum)]
pub enum RunType {
/// Executing commands will just print the command with its argument.
Dry,
/// Executing commands will perform actual execution.
#[default]
Wet,
/// Executing commands will print the command and perform actual execution.
Damp,
}
impl RunType {
/// Tells whether we're performing a dry run.
pub fn dry(self) -> bool {
match self {
RunType::Dry => true,
RunType::Wet => false,
RunType::Damp => false,
}
}
}
pub struct ExecutionContext<'a> {
run_type: RunType,
@@ -18,10 +54,18 @@ pub struct ExecutionContext<'a> {
tmux_session: Mutex<Option<String>>,
/// True if topgrade is running under ssh.
under_ssh: bool,
#[cfg(target_os = "linux")]
distribution: &'a Result<Distribution>,
powershell: LazyLock<Option<Powershell>>,
}
impl<'a> ExecutionContext<'a> {
pub fn new(run_type: RunType, sudo: Option<Sudo>, config: &'a Config) -> Self {
pub fn new(
run_type: RunType,
sudo: Option<Sudo>,
config: &'a Config,
#[cfg(target_os = "linux")] distribution: &'a Result<Distribution>,
) -> Self {
let under_ssh = var("SSH_CLIENT").is_ok() || var("SSH_TTY").is_ok();
Self {
run_type,
@@ -29,12 +73,19 @@ impl<'a> ExecutionContext<'a> {
config,
tmux_session: Mutex::new(None),
under_ssh,
#[cfg(target_os = "linux")]
distribution,
powershell: LazyLock::new(Powershell::new),
}
}
pub fn execute_elevated(&self, command: &Path, interactive: bool) -> Result<Executor> {
let sudo = require_option(self.sudo.as_ref(), REQUIRE_SUDO.to_string())?;
Ok(sudo.execute_elevated(self, command, interactive))
/// Create an instance of `Executor` that should run `program`.
pub fn execute<S: AsRef<OsStr>>(&self, program: S) -> Executor {
match self.run_type {
RunType::Dry => Executor::Dry(DryCommand::new(program)),
RunType::Wet => Executor::Wet(Command::new(program)),
RunType::Damp => Executor::Damp(Command::new(program)),
}
}
pub fn run_type(&self) -> RunType {
@@ -45,6 +96,14 @@ impl<'a> ExecutionContext<'a> {
&self.sudo
}
pub fn require_sudo(&self) -> Result<&Sudo> {
if let Some(value) = self.sudo() {
Ok(value)
} else {
Err(MissingSudo().into())
}
}
pub fn config(&self) -> &Config {
self.config
}
@@ -60,4 +119,17 @@ impl<'a> ExecutionContext<'a> {
pub fn get_tmux_session(&self) -> Option<String> {
self.tmux_session.lock().unwrap().clone()
}
#[cfg(target_os = "linux")]
pub fn distribution(&self) -> &Result<Distribution> {
self.distribution
}
pub fn powershell(&self) -> &Option<Powershell> {
&self.powershell
}
pub fn require_powershell(&self) -> Result<&Powershell> {
require_option(self.powershell.as_ref(), t!("Powershell is not installed").to_string())
}
}

View File

@@ -1,59 +1,23 @@
//! Utilities for command execution
use std::ffi::{OsStr, OsString};
use std::fmt::Debug;
use std::iter;
use std::path::Path;
use std::process::{Child, Command, ExitStatus, Output};
use color_eyre::eyre::Result;
use tracing::debug;
use rust_i18n::t;
use tracing::{Level, debug, enabled};
use crate::command::CommandExt;
use crate::error::DryRun;
/// An enum telling whether Topgrade should perform dry runs or actually perform the steps.
#[derive(Clone, Copy, Debug)]
pub enum RunType {
/// Executing commands will just print the command with its argument.
Dry,
/// Executing commands will perform actual execution.
Wet,
}
impl RunType {
/// Create a new instance from a boolean telling whether to dry run.
pub fn new(dry_run: bool) -> Self {
if dry_run {
RunType::Dry
} else {
RunType::Wet
}
}
/// Create an instance of `Executor` that should run `program`.
pub fn execute<S: AsRef<OsStr>>(self, program: S) -> Executor {
match self {
RunType::Dry => Executor::Dry(DryCommand {
program: program.as_ref().into(),
..Default::default()
}),
RunType::Wet => Executor::Wet(Command::new(program)),
}
}
/// Tells whether we're performing a dry run.
pub fn dry(self) -> bool {
match self {
RunType::Dry => true,
RunType::Wet => false,
}
}
}
/// An enum providing a similar interface to `std::process::Command`.
/// If the enum is set to `Wet`, execution will be performed with `std::process::Command`.
/// If the enum is set to `Dry`, execution will just print the command with its arguments.
pub enum Executor {
Wet(Command),
Damp(Command),
Dry(DryCommand),
}
@@ -63,7 +27,7 @@ impl Executor {
/// 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::Wet(c) | Executor::Damp(c) => c.get_program().to_string_lossy().into_owned(),
Executor::Dry(c) => c.program.to_string_lossy().into_owned(),
}
}
@@ -71,7 +35,7 @@ impl Executor {
/// See `std::process::Command::arg`
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Executor {
match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
c.arg(arg);
}
Executor::Dry(c) => {
@@ -89,7 +53,7 @@ impl Executor {
S: AsRef<OsStr>,
{
match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
c.args(args);
}
Executor::Dry(c) => {
@@ -104,7 +68,7 @@ impl Executor {
/// See `std::process::Command::current_dir`
pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Executor {
match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
c.current_dir(dir);
}
Executor::Dry(c) => c.directory = Some(dir.as_ref().into()),
@@ -120,7 +84,7 @@ impl Executor {
K: AsRef<OsStr>,
{
match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
c.env_remove(key);
}
Executor::Dry(_) => (),
@@ -137,7 +101,7 @@ impl Executor {
V: AsRef<OsStr>,
{
match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
c.env(key, val);
}
Executor::Dry(_) => (),
@@ -148,15 +112,16 @@ impl Executor {
/// See `std::process::Command::spawn`
pub fn spawn(&mut self) -> Result<ExecutorChild> {
self.log_command();
let result = match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
debug!("Running {:?}", c);
c.spawn_checked().map(ExecutorChild::Wet)?
}
Executor::Dry(c) => {
c.dry_run();
ExecutorChild::Dry
// We should use `spawn()` here rather than `spawn_checked()` since
// their semantics and behaviors are different.
#[allow(clippy::disallowed_methods)]
c.spawn().map(ExecutorChild::Wet)?
}
Executor::Dry(_) => ExecutorChild::Dry,
};
Ok(result)
@@ -164,12 +129,15 @@ impl Executor {
/// See `std::process::Command::output`
pub fn output(&mut self) -> Result<ExecutorOutput> {
self.log_command();
match self {
Executor::Wet(c) => Ok(ExecutorOutput::Wet(c.output_checked()?)),
Executor::Dry(c) => {
c.dry_run();
Ok(ExecutorOutput::Dry)
Executor::Wet(c) | Executor::Damp(c) => {
// We should use `output()` here rather than `output_checked()` since
// their semantics and behaviors are different.
#[allow(clippy::disallowed_methods)]
Ok(ExecutorOutput::Wet(c.output()?))
}
Executor::Dry(_) => Ok(ExecutorOutput::Dry),
}
}
@@ -177,18 +145,38 @@ impl Executor {
/// that can indicate success of a script
#[allow(dead_code)]
pub fn status_checked_with_codes(&mut self, codes: &[i32]) -> Result<()> {
self.log_command();
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) {
Executor::Wet(c) | Executor::Damp(c) => c.status_checked_with(|status| {
if status.success() || status.code().as_ref().is_some_and(|c| codes.contains(c)) {
Ok(())
} else {
Err(())
}
}),
Executor::Dry(c) => {
c.dry_run();
Ok(())
Executor::Dry(_) => Ok(()),
}
}
fn log_command(&self) {
match self {
Executor::Wet(_) => (),
Executor::Damp(c) => {
log_command(
"Executing: {program_name} {arguments}",
c.get_program(),
c.get_args(),
c.get_envs(),
c.get_current_dir(),
);
}
Executor::Dry(c) => log_command(
"Dry running: {program_name} {arguments}",
&c.program,
&c.args,
iter::empty(),
c.directory.as_ref(),
),
}
}
}
@@ -198,8 +186,7 @@ pub enum ExecutorOutput {
Dry,
}
/// A struct represending a command. Trying to execute it will just print its arguments.
#[derive(Default)]
/// A struct representing a command. Trying to execute it will just print its arguments.
pub struct DryCommand {
program: OsString,
args: Vec<OsString>,
@@ -207,26 +194,18 @@ pub struct DryCommand {
}
impl DryCommand {
fn dry_run(&self) {
print!(
"Dry running: {} {}",
self.program.to_string_lossy(),
shell_words::join(
self.args
.iter()
.map(|a| String::from(a.to_string_lossy()))
.collect::<Vec<String>>()
)
);
match &self.directory {
Some(dir) => println!(" in {}", dir.to_string_lossy()),
None => println!(),
};
pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
Self {
program: program.as_ref().to_os_string(),
args: Vec::new(),
directory: None,
}
}
}
/// The Result of spawn. Contains an actual `std::process::Child` if executed by a wet command.
pub enum ExecutorChild {
// Both RunType::Wet and RunType::Damp use this variant
#[allow(unused)] // this type has not been used
Wet(Child),
Dry,
@@ -239,22 +218,18 @@ impl CommandExt for Executor {
// variant for wet/dry runs.
fn output_checked_with(&mut self, succeeded: impl Fn(&Output) -> Result<(), ()>) -> Result<Output> {
self.log_command();
match self {
Executor::Wet(c) => c.output_checked_with(succeeded),
Executor::Dry(c) => {
c.dry_run();
Err(DryRun().into())
}
Executor::Wet(c) | Executor::Damp(c) => c.output_checked_with(succeeded),
Executor::Dry(_) => Err(DryRun().into()),
}
}
fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> Result<()> {
self.log_command();
match self {
Executor::Wet(c) => c.status_checked_with(succeeded),
Executor::Dry(c) => {
c.dry_run();
Ok(())
}
Executor::Wet(c) | Executor::Damp(c) => c.status_checked_with(succeeded),
Executor::Dry(_) => Ok(()),
}
}
@@ -262,3 +237,42 @@ impl CommandExt for Executor {
self.spawn()
}
}
fn log_command<
'a,
I: ExactSizeIterator<Item = (&'a (impl Debug + 'a + ?Sized), Option<&'a (impl Debug + 'a + ?Sized)>)>,
>(
prefix: &str,
exec: &OsStr,
args: impl IntoIterator<Item = &'a (impl AsRef<OsStr> + ?Sized + 'a)>,
env: impl IntoIterator<Item = (&'a OsStr, Option<&'a OsStr>), IntoIter = I>,
dir: Option<&'a (impl AsRef<Path> + ?Sized)>,
) {
println!(
"{}",
t!(
prefix,
program_name = exec.to_string_lossy(),
arguments = shell_words::join(args.into_iter().map(|s| s.as_ref().to_string_lossy()))
)
);
let env_iter = env.into_iter();
if env_iter.len() != 0 && enabled!(Level::DEBUG) {
println!(
" {}",
t!(
"with env: {env}",
env = env_iter
.filter(|(_, val)| val.is_some())
.map(|(key, val)| format!("{:?}={:?}", key, val.unwrap()))
.collect::<Vec<_>>()
.join(" ")
)
)
}
if let Some(d) = dir {
println!(" {}", t!("in {directory}", directory = d.as_ref().display()));
}
}

View File

@@ -1,2 +0,0 @@
//pub mod steps;
//pub mod utils;

View File

@@ -1,6 +1,7 @@
#![allow(clippy::cognitive_complexity)]
use std::env;
use std::env::home_dir;
use std::io;
use std::path::PathBuf;
use std::process::exit;
@@ -8,26 +9,27 @@ use std::time::Duration;
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::{Parser, crate_version};
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use console::Key;
use etcetera::base_strategy::BaseStrategy;
#[cfg(windows)]
use etcetera::base_strategy::Windows;
#[cfg(unix)]
use etcetera::base_strategy::Xdg;
use once_cell::sync::Lazy;
use rust_i18n::{i18n, t};
use std::sync::LazyLock;
use tracing::debug;
use self::config::{CommandLineArgs, Config, Step};
use self::config::{CommandLineArgs, Config};
use self::error::StepFailed;
#[cfg(all(windows, feature = "self-update"))]
use self::error::Upgraded;
use self::runner::StepResult;
#[allow(clippy::wildcard_imports)]
use self::steps::{remote::*, *};
use self::sudo::{Sudo, SudoCreateError, SudoKind};
#[allow(clippy::wildcard_imports)]
use self::terminal::*;
use self::utils::{hostname, install_color_eyre, install_tracing, update_tracing};
use self::utils::{install_color_eyre, install_tracing, is_elevated, update_tracing};
mod breaking_changes;
mod command;
@@ -36,24 +38,28 @@ mod ctrlc;
mod error;
mod execution_context;
mod executor;
mod report;
mod runner;
#[cfg(windows)]
mod self_renamer;
#[cfg(feature = "self-update")]
mod self_update;
mod step;
mod steps;
mod sudo;
mod terminal;
mod utils;
pub(crate) static HOME_DIR: Lazy<PathBuf> = Lazy::new(|| home::home_dir().expect("No home directory"));
pub(crate) static HOME_DIR: LazyLock<PathBuf> = LazyLock::new(|| home_dir().expect("No home directory"));
#[cfg(unix)]
pub(crate) static XDG_DIRS: Lazy<Xdg> = Lazy::new(|| Xdg::new().expect("No home directory"));
pub(crate) static XDG_DIRS: LazyLock<Xdg> = LazyLock::new(|| Xdg::new().expect("No home directory"));
#[cfg(windows)]
pub(crate) static WINDOWS_DIRS: Lazy<Windows> = Lazy::new(|| Windows::new().expect("No home directory"));
pub(crate) static WINDOWS_DIRS: LazyLock<Windows> = LazyLock::new(|| Windows::new().expect("No home directory"));
// Init and load the i18n files
i18n!("locales", fallback = "en");
#[allow(clippy::too_many_lines)]
fn run() -> Result<()> {
install_color_eyre()?;
ctrlc::set_handler();
@@ -72,6 +78,11 @@ fn run() -> Result<()> {
// and `Config::tracing_filter_directives()`.
let reload_handle = install_tracing(&opt.tracing_filter_directives())?;
// Get current system locale and set it as the default locale
let system_locale = sys_locale::get_locale().unwrap_or("en".to_string());
rust_i18n::set_locale(&system_locale);
debug!("Current system locale is {system_locale}");
if let Some(shell) = opt.gen_completion {
let cmd = &mut CommandLineArgs::command();
clap_complete::generate(shell, cmd, clap::crate_name!(), &mut io::stdout());
@@ -85,10 +96,10 @@ fn run() -> Result<()> {
}
for env in opt.env_variables() {
let mut splitted = env.split('=');
let var = splitted.next().unwrap();
let value = splitted.next().unwrap();
env::set_var(var, value);
let mut parts = env.split('=');
let var = parts.next().unwrap();
let value = parts.next().unwrap();
unsafe { env::set_var(var, value) };
}
if opt.edit_config() {
@@ -110,57 +121,73 @@ fn run() -> Result<()> {
debug!("Version: {}", crate_version!());
debug!("OS: {}", env!("TARGET"));
debug!("{:?}", std::env::args());
debug!("Binary path: {:?}", std::env::current_exe());
debug!("{:?}", env::args());
debug!("Binary path: {:?}", env::current_exe());
debug!("self-update Feature Enabled: {:?}", cfg!(feature = "self-update"));
debug!("Configuration: {:?}", config);
if config.run_in_tmux() && env::var("TOPGRADE_INSIDE_TMUX").is_err() {
#[cfg(unix)]
{
tmux::run_in_tmux(config.tmux_arguments()?)?;
tmux::run_in_tmux(config.tmux_config()?)?;
return Ok(());
}
}
let powershell = powershell::Powershell::new();
let should_run_powershell = powershell.profile().is_some() && config.should_run(Step::Powershell);
let emacs = emacs::Emacs::new();
let elevated = is_elevated();
#[cfg(unix)]
if !config.allow_root() && elevated {
print_warning(t!(
"Topgrade should not be run as root, it will run commands with sudo or equivalent where needed."
));
if !prompt_yesno(&t!("Continue?"))? {
exit(1)
}
}
let sudo = match config.sudo_command() {
Some(kind) => Sudo::new(kind),
None if elevated => Sudo::new(SudoKind::Null),
None => Sudo::detect(),
};
debug!("Sudo: {:?}", sudo);
let (sudo, sudo_err) = match sudo {
Ok(sudo) => (Some(sudo), None),
Err(e) => (None, Some(e)),
};
#[cfg(target_os = "linux")]
let distribution = linux::Distribution::detect();
let sudo = config.sudo_command().map_or_else(sudo::Sudo::detect, sudo::Sudo::new);
let run_type = executor::RunType::new(config.dry_run());
let ctx = execution_context::ExecutionContext::new(run_type, sudo, &config);
let run_type = config.run_type();
let ctx = execution_context::ExecutionContext::new(
run_type,
sudo,
&config,
#[cfg(target_os = "linux")]
&distribution,
);
let mut runner = runner::Runner::new(&ctx);
// If
//
// 1. the breaking changes notification shouldnot be skipped
// 1. the breaking changes notification shouldn't 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?")? {
if prompt_yesno(&t!("Continue?"))? {
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")]
{
let should_self_update = env::var("TOPGRADE_NO_SELF_UPGRADE").is_err() && !config.no_self_update();
if should_self_update {
runner.execute(Step::SelfUpdate, "Self Update", || self_update::self_update(&ctx))?;
}
}
step::Step::SelfUpdate.run(&mut runner, &ctx)?;
#[cfg(windows)]
let _self_rename = if config.self_rename() {
@@ -169,315 +196,113 @@ fn run() -> Result<()> {
None
};
if let Some(commands) = config.pre_commands() {
for (name, command) in commands {
generic::run_custom_command(name, command, &ctx)?;
}
}
if config.pre_sudo() {
if let Some(sudo) = ctx.sudo() {
sudo.elevate(&ctx)?;
}
}
if let Some(topgrades) = config.remote_topgrades() {
for remote_topgrade in topgrades.iter().filter(|t| config.should_execute_remote(hostname(), t)) {
runner.execute(Step::Remotes, format!("Remote ({remote_topgrade})"), || {
ssh::ssh_step(&ctx, remote_topgrade)
})?;
}
}
#[cfg(windows)]
{
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 {
Ok(distribution) => {
runner.execute(Step::System, "System update", || distribution.upgrade(&ctx))?;
}
Err(e) => {
println!("Error detecting current distribution: {e}");
}
}
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", || {
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))?;
runner.execute(Step::AutoCpufreq, "auto-cpufreq", || linux::run_auto_cpufreq(&ctx))?;
}
#[cfg(target_os = "macos")]
{
runner.execute(Step::BrewFormula, "Brew (ARM)", || {
unix::run_brew_formula(&ctx, unix::BrewVariant::MacArm)
})?;
runner.execute(Step::BrewFormula, "Brew (Intel)", || {
unix::run_brew_formula(&ctx, unix::BrewVariant::MacIntel)
})?;
runner.execute(Step::BrewFormula, "Brew", || {
unix::run_brew_formula(&ctx, unix::BrewVariant::Path)
})?;
runner.execute(Step::BrewCask, "Brew Cask (ARM)", || {
unix::run_brew_cask(&ctx, unix::BrewVariant::MacArm)
})?;
runner.execute(Step::BrewCask, "Brew Cask (Intel)", || {
unix::run_brew_cask(&ctx, unix::BrewVariant::MacIntel)
})?;
runner.execute(Step::BrewCask, "Brew Cask", || {
unix::run_brew_cask(&ctx, unix::BrewVariant::Path)
})?;
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)]
{
runner.execute(Step::Yadm, "yadm", || unix::run_yadm(&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::HomeManager, "home-manager", || unix::run_home_manager(&ctx))?;
runner.execute(Step::Asdf, "asdf", || unix::run_asdf(&ctx))?;
runner.execute(Step::Mise, "mise", || unix::run_mise(&ctx))?;
runner.execute(Step::Pkgin, "pkgin", || unix::run_pkgin(&ctx))?;
runner.execute(Step::Bun, "bun", || unix::run_bun(&ctx))?;
runner.execute(Step::BunPackages, "bun-packages", || unix::run_bun_packages(&ctx))?;
runner.execute(Step::Shell, "zr", || zsh::run_zr(&ctx))?;
runner.execute(Step::Shell, "antibody", || zsh::run_antibody(&ctx))?;
runner.execute(Step::Shell, "antidote", || zsh::run_antidote(&ctx))?;
runner.execute(Step::Shell, "antigen", || zsh::run_antigen(&ctx))?;
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))?;
runner.execute(Step::Shell, "zi", || zsh::run_zi(&ctx))?;
runner.execute(Step::Shell, "zim", || zsh::run_zim(&ctx))?;
runner.execute(Step::Shell, "oh-my-zsh", || zsh::run_oh_my_zsh(&ctx))?;
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, "oh-my-fish", || unix::run_oh_my_fish(&ctx))?;
runner.execute(Step::Shell, "fish-plug", || unix::run_fish_plug(&ctx))?;
runner.execute(Step::Shell, "fundle", || unix::run_fundle(&ctx))?;
runner.execute(Step::Tmux, "tmux", || tmux::run_tpm(&ctx))?;
runner.execute(Step::Tldr, "TLDR", || unix::run_tldr(&ctx))?;
runner.execute(Step::Pearl, "pearl", || unix::run_pearl(&ctx))?;
#[cfg(not(any(target_os = "macos", target_os = "android")))]
runner.execute(Step::GnomeShellExtensions, "Gnome Shell Extensions", || {
unix::upgrade_gnome_extensions(&ctx)
})?;
runner.execute(Step::Pyenv, "pyenv", || unix::run_pyenv(&ctx))?;
runner.execute(Step::Sdkman, "SDKMAN!", || unix::run_sdkman(&ctx))?;
runner.execute(Step::Rcm, "rcm", || unix::run_rcm(&ctx))?;
runner.execute(Step::Maza, "maza", || unix::run_maza(&ctx))?;
}
#[cfg(not(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
{
runner.execute(Step::Atom, "apm", || generic::run_apm(&ctx))?;
}
// 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::Choosenim, "choosenim", || generic::run_choosenim(&ctx))?;
runner.execute(Step::Cargo, "cargo", || generic::run_cargo_update(&ctx))?;
runner.execute(Step::Flutter, "Flutter", || generic::run_flutter_upgrade(&ctx))?;
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::Opam, "opam", || generic::run_opam_update(&ctx))?;
runner.execute(Step::Vcpkg, "vcpkg", || generic::run_vcpkg_update(&ctx))?;
runner.execute(Step::Pipx, "pipx", || generic::run_pipx_update(&ctx))?;
runner.execute(Step::Vscode, "Visual Studio Code extensions", || {
generic::run_vscode_extensions_update(&ctx)
})?;
runner.execute(Step::Conda, "conda", || generic::run_conda_update(&ctx))?;
runner.execute(Step::Mamba, "mamba", || generic::run_mamba_update(&ctx))?;
runner.execute(Step::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::Myrepos, "myrepos", || generic::run_myrepos_update(&ctx))?;
runner.execute(Step::Chezmoi, "chezmoi", || generic::run_chezmoi_update(&ctx))?;
runner.execute(Step::Jetpack, "jetpack", || generic::run_jetpack(&ctx))?;
runner.execute(Step::Vim, "vim", || vim::upgrade_vim(&ctx))?;
runner.execute(Step::Vim, "Neovim", || vim::upgrade_neovim(&ctx))?;
runner.execute(Step::Vim, "The Ultimate vimrc", || vim::upgrade_ultimate_vimrc(&ctx))?;
runner.execute(Step::Vim, "voom", || vim::run_voom(&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::Yarn, "yarn", || node::run_yarn_upgrade(&ctx))?;
runner.execute(Step::Pnpm, "pnpm", || node::run_pnpm_upgrade(&ctx))?;
runner.execute(Step::Containers, "Containers", || containers::run_containers(&ctx))?;
runner.execute(Step::Deno, "deno", || node::deno_upgrade(&ctx))?;
runner.execute(Step::Composer, "composer", || generic::run_composer_update(&ctx))?;
runner.execute(Step::Krew, "krew", || generic::run_krew_upgrade(&ctx))?;
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::Haxelib, "haxelib", || generic::run_haxelib_update(&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::Bin, "bin", || generic::bin_update(&ctx))?;
runner.execute(Step::Gcloud, "gcloud", || generic::run_gcloud_components_update(&ctx))?;
runner.execute(Step::Micro, "micro", || generic::run_micro(&ctx))?;
runner.execute(Step::Raco, "raco", || generic::run_raco_update(&ctx))?;
runner.execute(Step::Spicetify, "spicetify", || generic::spicetify_upgrade(&ctx))?;
runner.execute(Step::GithubCliExtensions, "GitHub CLI Extensions", || {
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)
})?;
if should_run_powershell {
runner.execute(Step::Powershell, "Powershell Modules Update", || {
powershell.update_modules(&ctx)
})?;
}
if let Some(commands) = config.commands() {
if let Some(commands) = config.pre_commands() {
for (name, command) in commands {
if config.should_run_custom_command(name) {
runner.execute(Step::CustomCommands, name, || {
generic::run_custom_command(name, command, &ctx)
})?;
}
generic::run_custom_command(name, command, &ctx)?;
}
}
if config.should_run(Step::Vagrant) {
if let Ok(boxes) = vagrant::collect_boxes(&ctx) {
for vagrant_box in boxes {
runner.execute(Step::Vagrant, format!("Vagrant ({})", vagrant_box.smart_name()), || {
vagrant::topgrade_vagrant_box(&ctx, &vagrant_box)
})?;
for step in step::default_steps() {
match step.run(&mut runner, &ctx) {
Ok(()) => (),
Err(error)
if error
.downcast_ref::<io::Error>()
.is_some_and(|e| e.kind() == io::ErrorKind::Interrupted) =>
{
println!();
debug!("Interrupted (possibly with 'q' during retry prompt). Printing summary.");
break;
}
Err(error) => return Err(error),
}
}
runner.execute(Step::Vagrant, "Vagrant boxes", || vagrant::upgrade_vagrant_boxes(&ctx))?;
if !runner.report().data().is_empty() {
print_separator("Summary");
let mut failed = false;
for (key, result) in runner.report().data() {
let report = runner.report();
if !report.is_empty() {
print_separator(t!("Summary"));
let mut skipped_missing_sudo = false;
for (key, result) in report {
if !failed && result.failed() {
failed = true;
}
if let StepResult::SkippedMissingSudo = result {
skipped_missing_sudo = true;
}
print_result(key, result);
}
#[cfg(target_os = "linux")]
{
if let Ok(distribution) = &distribution {
distribution.show_summary();
if skipped_missing_sudo {
print_warning(t!(
"\nSome steps were skipped as sudo or equivalent could not be found."
));
// Steps can only fail with SkippedMissingSudo if sudo is None,
// therefore we must have a sudo_err
match sudo_err.unwrap() {
SudoCreateError::CannotFindBinary => {
#[cfg(unix)]
print_warning(t!(
"Install one of `sudo`, `doas`, `pkexec`, `run0` or `please` to run these steps."
));
// if this windows version supported Windows Sudo, the error would have been WinSudoDisabled
#[cfg(windows)]
print_warning(t!("Install gsudo to run these steps."));
}
#[cfg(windows)]
SudoCreateError::WinSudoDisabled => {
print_warning(t!(
"Install gsudo or enable Windows Sudo to run these steps.\nFor Windows Sudo, the default 'In a new window' mode is not supported as it prevents Topgrade from waiting for commands to finish. Please configure it to use 'Inline' mode instead.\nGo to https://go.microsoft.com/fwlink/?linkid=2257346 to learn more."
));
}
#[cfg(windows)]
SudoCreateError::WinSudoNewWindowMode => {
print_warning(t!(
"Windows Sudo was found, but it is set to 'In a new window' mode, which prevents Topgrade from waiting for commands to finish. Please configure it to use 'Inline' mode instead.\nGo to https://go.microsoft.com/fwlink/?linkid=2257346 to learn more."
));
}
}
}
}
let mut post_command_failed = false;
#[cfg(target_os = "linux")]
if config.show_distribution_summary() {
if let Ok(distribution) = &distribution {
distribution.show_summary();
}
}
if let Some(commands) = config.post_commands() {
for (name, command) in commands {
if generic::run_custom_command(name, command, &ctx).is_err() {
post_command_failed = true;
let result = generic::run_custom_command(name, command, &ctx);
if !failed && result.is_err() {
failed = true;
}
}
}
if config.keep_at_end() {
print_info("\n(R)eboot\n(S)hell\n(Q)uit");
print_info(t!("\n(R)eboot\n(S)hell\n(Q)uit"));
loop {
match get_key() {
Ok(Key::Char('s')) | Ok(Key::Char('S')) => {
Ok(Key::Char('s' | 'S')) => {
run_shell().context("Failed to execute shell")?;
}
Ok(Key::Char('r')) | Ok(Key::Char('R')) => {
reboot().context("Failed to reboot")?;
Ok(Key::Char('r' | 'R')) => {
println!("{}", t!("Rebooting..."));
reboot(&ctx).context("Failed to reboot")?;
}
Ok(Key::Char('q')) | Ok(Key::Char('Q')) => (),
Ok(Key::Char('q' | 'Q')) => (),
_ => {
continue;
}
@@ -486,23 +311,18 @@ fn run() -> Result<()> {
}
}
let failed = post_command_failed || runner.report().data().iter().any(|(_, result)| result.failed());
if !config.skip_notify() {
notify_desktop(
format!(
"Topgrade finished {}",
if failed { "with errors" } else { "successfully" }
),
if failed {
t!("Topgrade finished with errors")
} else {
t!("Topgrade finished successfully")
},
Some(Duration::from_secs(10)),
)
);
}
if failed {
Err(StepFailed.into())
} else {
Ok(())
}
if failed { Err(StepFailed.into()) } else { Ok(()) }
}
fn main() {
@@ -511,13 +331,6 @@ fn main() {
exit(0);
}
Err(error) => {
#[cfg(all(windows, feature = "self-update"))]
{
if let Some(Upgraded(status)) = error.downcast_ref::<Upgraded>() {
exit(status.code().unwrap());
}
}
let skip_print = (error.downcast_ref::<StepFailed>().is_some())
|| (error
.downcast_ref::<io::Error>()
@@ -528,7 +341,7 @@ fn main() {
// The `Debug` implementation of `eyre::Result` prints a multi-line
// error message that includes all the 'causes' added with
// `.with_context(...)` calls.
println!("Error: {error:?}");
println!("{}", t!("Error: {error}", error = format!("{:?}", error)));
}
exit(1);
}

View File

@@ -1,45 +0,0 @@
use std::borrow::Cow;
pub enum StepResult {
Success,
Failure,
Ignored,
Skipped(String),
}
impl StepResult {
pub fn failed(&self) -> bool {
match self {
StepResult::Success | StepResult::Ignored | StepResult::Skipped(_) => false,
StepResult::Failure => true,
}
}
}
type CowString<'a> = Cow<'a, str>;
type ReportData<'a> = Vec<(CowString<'a>, StepResult)>;
pub struct Report<'a> {
data: ReportData<'a>,
}
impl<'a> Report<'a> {
pub fn new() -> Self {
Self { data: Vec::new() }
}
pub fn push_result<M>(&mut self, result: Option<(M, StepResult)>)
where
M: Into<CowString<'a>>,
{
if let Some((key, success)) = result {
let key = key.into();
debug_assert!(!self.data.iter().any(|(k, _)| k == &key), "{key} already reported");
self.data.push((key, success));
}
}
pub fn data(&self) -> &ReportData<'a> {
&self.data
}
}

View File

@@ -1,14 +1,37 @@
use crate::ctrlc;
use crate::error::{DryRun, SkipStep};
use crate::execution_context::ExecutionContext;
use crate::report::{Report, StepResult};
use crate::terminal::print_error;
use crate::{config::Step, terminal::should_retry};
use color_eyre::eyre::Result;
use color_eyre::eyre::{Result, WrapErr};
use rust_i18n::t;
use std::borrow::Cow;
use std::fmt::Debug;
use std::io;
use tracing::debug;
use crate::ctrlc;
use crate::error::{DryRun, MissingSudo, SkipStep};
use crate::execution_context::ExecutionContext;
use crate::step::Step;
use crate::terminal::{ShouldRetry, print_error, print_warning, should_retry};
pub enum StepResult {
Success,
Failure,
Ignored,
SkippedMissingSudo,
Skipped(String),
}
impl StepResult {
pub fn failed(&self) -> bool {
use StepResult::*;
match self {
Success | Ignored | Skipped(_) | SkippedMissingSudo => false,
Failure => true,
}
}
}
type Report<'a> = Vec<(Cow<'a, str>, StepResult)>;
pub struct Runner<'a> {
ctx: &'a ExecutionContext<'a>,
report: Report<'a>,
@@ -18,20 +41,25 @@ impl<'a> Runner<'a> {
pub fn new(ctx: &'a ExecutionContext) -> Runner<'a> {
Runner {
ctx,
report: Report::new(),
report: Vec::new(),
}
}
pub fn execute<F, M>(&mut self, step: Step, key: M, func: F) -> Result<()>
fn push_result(&mut self, key: Cow<'a, str>, result: StepResult) {
debug_assert!(!self.report.iter().any(|(k, _)| k == &key), "{key} already reported");
self.report.push((key, result));
}
pub fn execute<K, F>(&mut self, step: Step, key: K, func: F) -> Result<()>
where
K: Into<Cow<'a, str>> + Debug,
F: Fn() -> Result<()>,
M: Into<Cow<'a, str>> + Debug,
{
if !self.ctx.config().should_run(step) {
return Ok(());
}
let key = key.into();
let key: Cow<'a, str> = key.into();
debug!("Step {:?}", key);
// alter the `func` to put it in a span
@@ -45,13 +73,18 @@ impl<'a> Runner<'a> {
loop {
match func() {
Ok(()) => {
self.report.push_result(Some((key, StepResult::Success)));
self.push_result(key, StepResult::Success);
break;
}
Err(e) if e.downcast_ref::<DryRun>().is_some() => break,
Err(e) if e.downcast_ref::<MissingSudo>().is_some() => {
print_warning(t!("Skipping step, sudo is required"));
self.push_result(key, StepResult::SkippedMissingSudo);
break;
}
Err(e) if e.downcast_ref::<SkipStep>().is_some() => {
if self.ctx.config().verbose() || self.ctx.config().show_skipped() {
self.report.push_result(Some((key, StepResult::Skipped(e.to_string()))));
self.push_result(key, StepResult::Skipped(e.to_string()));
}
break;
}
@@ -66,21 +99,28 @@ impl<'a> Runner<'a> {
let should_ask = interrupted || !(self.ctx.config().no_retry() || ignore_failure);
let should_retry = if should_ask {
print_error(&key, format!("{e:?}"));
should_retry(interrupted, key.as_ref())?
should_retry(key.as_ref())?
} else {
false
ShouldRetry::No
};
if !should_retry {
self.report.push_result(Some((
key,
if ignore_failure {
StepResult::Ignored
} else {
StepResult::Failure
},
)));
break;
match should_retry {
ShouldRetry::No | ShouldRetry::Quit => {
self.push_result(
key,
if ignore_failure {
StepResult::Ignored
} else {
StepResult::Failure
},
);
if let ShouldRetry::Quit = should_retry {
return Err(io::Error::from(io::ErrorKind::Interrupted))
.context("Quit from user input");
}
break;
}
ShouldRetry::Yes => (),
}
}
}
@@ -89,7 +129,7 @@ impl<'a> Runner<'a> {
Ok(())
}
pub fn report(&self) -> &Report {
pub fn report(&self) -> &Report<'_> {
&self.report
}
}

View File

@@ -2,23 +2,25 @@ use std::env;
#[cfg(unix)]
use std::os::unix::process::CommandExt as _;
use std::process::Command;
#[cfg(windows)]
use std::process::exit;
use crate::config::Step;
use color_eyre::eyre::{bail, Result};
use crate::step::Step;
use color_eyre::eyre::Result;
#[cfg(unix)]
use color_eyre::eyre::bail;
use rust_i18n::t;
use self_update_crate::backends::github::Update;
use self_update_crate::update::UpdateStatus;
use super::terminal::*;
#[cfg(windows)]
use crate::error::Upgraded;
use super::terminal::{print_info, print_separator};
use crate::execution_context::ExecutionContext;
pub fn self_update(ctx: &ExecutionContext) -> Result<()> {
print_separator("Self update");
print_separator(t!("Self update"));
if ctx.run_type().dry() {
println!("Would self-update");
println!("{}", t!("Would self-update"));
Ok(())
} else {
let assume_yes = ctx.config().yes(Step::SelfUpdate);
@@ -38,17 +40,17 @@ pub fn self_update(ctx: &ExecutionContext) -> Result<()> {
.update_extended()?;
if let UpdateStatus::Updated(release) = &result {
println!("\nTopgrade upgraded to {}:\n", release.version);
println!("{}", t!("Topgrade upgraded to {version}:\n", version = release.version));
if let Some(body) = &release.body {
println!("{body}");
}
} else {
println!("Topgrade is up-to-date");
println!("{}", t!("Topgrade is up-to-date"));
}
{
if result.updated() {
print_info("Respawning...");
print_info(t!("Respawning..."));
let mut command = Command::new(current_exe?);
command.args(env::args().skip(1)).env("TOPGRADE_NO_SELF_UPGRADE", "");
@@ -62,7 +64,7 @@ pub fn self_update(ctx: &ExecutionContext) -> Result<()> {
{
#[allow(clippy::disallowed_methods)]
let status = command.status()?;
bail!(Upgraded(status));
exit(status.code().expect("This cannot return None on Windows"));
}
}
}

902
src/step.rs Normal file
View File

@@ -0,0 +1,902 @@
use crate::execution_context::ExecutionContext;
use crate::runner::Runner;
use clap::ValueEnum;
use color_eyre::Result;
#[cfg(target_os = "linux")]
use rust_i18n::t;
use serde::Deserialize;
use strum::{EnumCount, EnumIter, EnumString, VariantNames};
#[cfg(feature = "self-update")]
use crate::self_update;
use crate::steps::remote::vagrant;
#[allow(clippy::wildcard_imports)]
use crate::steps::*;
use crate::utils::hostname;
#[derive(ValueEnum, EnumString, VariantNames, Debug, Clone, PartialEq, Eq, Deserialize, EnumIter, Copy, EnumCount)]
#[clap(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum Step {
AM,
AndroidStudio,
AppMan,
Aqua,
Asdf,
Atom,
Atuin,
Audit,
AutoCpufreq,
Bin,
Bob,
BrewCask,
BrewFormula,
Bun,
BunPackages,
Cargo,
Certbot,
Chezmoi,
Chocolatey,
Choosenim,
CinnamonSpices,
ClamAvDb,
Composer,
Conda,
ConfigUpdate,
Containers,
CustomCommands,
DebGet,
Deno,
Distrobox,
DkpPacman,
Dotnet,
Elan,
Emacs,
Falconf,
Firmware,
Flatpak,
Flutter,
Fossil,
Gcloud,
Gem,
Ghcup,
GitRepos,
GithubCliExtensions,
GnomeShellExtensions,
Go,
Guix,
Haxelib,
Helix,
Helm,
HomeManager,
Hyprpm,
// These names are miscapitalized on purpose, so the CLI name is
// `jetbrains_pycharm` instead of `jet_brains_py_charm`.
JetbrainsAqua,
JetbrainsClion,
JetbrainsDatagrip,
JetbrainsDataspell,
JetbrainsGateway,
JetbrainsGoland,
JetbrainsIdea,
JetbrainsMps,
JetbrainsPhpstorm,
JetbrainsPycharm,
JetbrainsRider,
JetbrainsRubymine,
JetbrainsRustrover,
JetbrainsToolbox,
JetbrainsWebstorm,
Jetpack,
Julia,
Juliaup,
Kakoune,
Krew,
Lensfun,
Lure,
Macports,
Mamba,
Mandb,
Mas,
Maza,
Micro,
MicrosoftStore,
Miktex,
Mise,
Myrepos,
Nix,
NixHelper,
Node,
Opam,
Pacdef,
Pacstall,
Pearl,
Pip3,
PipReview,
PipReviewLocal,
Pipupgrade,
Pipx,
Pipxu,
Pixi,
Pkg,
Pkgfile,
Pkgin,
PlatformioCore,
Pnpm,
Poetry,
Powershell,
Protonup,
Pyenv,
Raco,
Rcm,
Remotes,
Restarts,
Rtcl,
RubyGems,
Rustup,
Rye,
Scoop,
Sdkman,
SelfUpdate,
Sheldon,
Shell,
Snap,
Sparkle,
Spicetify,
Stack,
Stew,
System,
Tldr,
Tlmgr,
Tmux,
Toolbx,
Typst,
Uv,
Vagrant,
Vcpkg,
Vim,
VoltaPackages,
Vscode,
VscodeInsiders,
Vscodium,
VscodiumInsiders,
Waydroid,
Winget,
Wsl,
WslUpdate,
Xcodes,
Yadm,
Yarn,
Yazi,
Zigup,
Zvm,
}
impl Step {
#[allow(clippy::too_many_lines)]
pub fn run(&self, runner: &mut Runner, ctx: &ExecutionContext) -> Result<()> {
use Step::*;
match *self {
AM =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "am", || linux::run_am(ctx))?
}
AndroidStudio => runner.execute(*self, "Android Studio Plugins", || generic::run_android_studio(ctx))?,
AppMan =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "appman", || linux::run_appman(ctx))?
}
Aqua => runner.execute(*self, "aqua", || generic::run_aqua(ctx))?,
Asdf =>
{
#[cfg(unix)]
runner.execute(*self, "asdf", || unix::run_asdf(ctx))?
}
Atom =>
{
#[cfg(not(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
runner.execute(*self, "apm", || generic::run_apm(ctx))?
}
Atuin =>
{
#[cfg(unix)]
runner.execute(*self, "atuin", || unix::run_atuin(ctx))?
}
Audit => {
#[cfg(target_os = "dragonfly")]
runner.execute(*self, "DragonFly Audit", || dragonfly::audit_packages(ctx))?;
#[cfg(target_os = "freebsd")]
runner.execute(*self, "FreeBSD Audit", || freebsd::audit_packages(ctx))?
}
AutoCpufreq =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "auto-cpufreq", || linux::run_auto_cpufreq(ctx))?
}
Bin => runner.execute(*self, "bin", || generic::bin_update(ctx))?,
Bob => runner.execute(*self, "Bob", || generic::run_bob(ctx))?,
BrewCask => {
#[cfg(any(target_os = "linux", target_os = "macos"))]
runner.execute(*self, "Brew Cask", || unix::run_brew_cask(ctx, unix::BrewVariant::Path))?;
#[cfg(target_os = "macos")]
runner.execute(*self, "Brew Cask (Intel)", || {
unix::run_brew_cask(ctx, unix::BrewVariant::MacIntel)
})?;
#[cfg(target_os = "macos")]
runner.execute(*self, "Brew Cask (ARM)", || {
unix::run_brew_cask(ctx, unix::BrewVariant::MacArm)
})?
}
BrewFormula => {
#[cfg(target_os = "linux")]
runner.execute(*self, "Brew", || unix::run_brew_formula(ctx, unix::BrewVariant::Path))?;
#[cfg(target_os = "macos")]
runner.execute(*self, "Brew (ARM)", || {
unix::run_brew_formula(ctx, unix::BrewVariant::MacArm)
})?;
#[cfg(target_os = "macos")]
runner.execute(*self, "Brew (Intel)", || {
unix::run_brew_formula(ctx, unix::BrewVariant::MacIntel)
})?
}
Bun => runner.execute(*self, "bun", || generic::run_bun(ctx))?,
BunPackages =>
{
#[cfg(unix)]
runner.execute(*self, "bun-packages", || unix::run_bun_packages(ctx))?
}
Cargo => runner.execute(*self, "cargo", || generic::run_cargo_update(ctx))?,
Certbot => runner.execute(*self, "Certbot", || generic::run_certbot(ctx))?,
Chezmoi => runner.execute(*self, "chezmoi", || generic::run_chezmoi_update(ctx))?,
Chocolatey =>
{
#[cfg(windows)]
runner.execute(*self, "Chocolatey", || windows::run_chocolatey(ctx))?
}
Choosenim => runner.execute(*self, "choosenim", || generic::run_choosenim(ctx))?,
CinnamonSpices =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "Cinnamon spices", || linux::run_cinnamon_spices_updater(ctx))?
}
ClamAvDb => runner.execute(*self, "ClamAV Databases", || generic::run_freshclam(ctx))?,
Composer => runner.execute(*self, "composer", || generic::run_composer_update(ctx))?,
Conda => runner.execute(*self, "conda", || generic::run_conda_update(ctx))?,
ConfigUpdate =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "config-update", || linux::run_config_update(ctx))?
}
Containers => runner.execute(*self, "Containers", || containers::run_containers(ctx))?,
CustomCommands => {
if let Some(commands) = ctx.config().commands() {
for (name, command) in commands
.iter()
.filter(|(n, _)| ctx.config().should_run_custom_command(n))
{
runner.execute(*self, name.clone(), || generic::run_custom_command(name, command, ctx))?;
}
}
}
DebGet =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "deb-get", || linux::run_deb_get(ctx))?
}
Deno => runner.execute(*self, "deno", || node::deno_upgrade(ctx))?,
Distrobox =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "distrobox", || linux::run_distrobox_update(ctx))?
}
DkpPacman =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "dkp-pacman", || linux::run_dkp_pacman_update(ctx))?
}
Dotnet => runner.execute(*self, ".NET", || generic::run_dotnet_upgrade(ctx))?,
Elan => runner.execute(*self, "elan", || generic::run_elan(ctx))?,
Emacs => runner.execute(*self, "Emacs", || emacs::Emacs::new().upgrade(ctx))?,
Falconf => runner.execute(*self, "falconf sync", || generic::run_falconf(ctx))?,
Firmware =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "Firmware", || linux::run_fwupdmgr(ctx))?
}
Flatpak =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "Flatpak", || linux::run_flatpak(ctx))?
}
Flutter => runner.execute(*self, "Flutter", || generic::run_flutter_upgrade(ctx))?,
Fossil => runner.execute(*self, "fossil", || generic::run_fossil(ctx))?,
Gcloud => runner.execute(*self, "gcloud", || generic::run_gcloud_components_update(ctx))?,
Gem => runner.execute(*self, "gem", || generic::run_gem(ctx))?,
Ghcup => runner.execute(*self, "ghcup", || generic::run_ghcup_update(ctx))?,
GitRepos => runner.execute(*self, "Git Repositories", || git::run_git_pull(ctx))?,
GithubCliExtensions => runner.execute(*self, "GitHub CLI Extensions", || {
generic::run_ghcli_extensions_upgrade(ctx)
})?,
GnomeShellExtensions =>
{
#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
runner.execute(*self, "Gnome Shell Extensions", || unix::upgrade_gnome_extensions(ctx))?
}
Go => {
runner.execute(*self, "go-global-update", || go::run_go_global_update(ctx))?;
runner.execute(*self, "gup", || go::run_go_gup(ctx))?
}
Guix =>
{
#[cfg(unix)]
runner.execute(*self, "guix", || unix::run_guix(ctx))?
}
Haxelib => runner.execute(*self, "haxelib", || generic::run_haxelib_update(ctx))?,
Helix => runner.execute(*self, "helix", || generic::run_helix_grammars(ctx))?,
Helm => runner.execute(*self, "helm", || generic::run_helm_repo_update(ctx))?,
HomeManager =>
{
#[cfg(unix)]
runner.execute(*self, "home-manager", || unix::run_home_manager(ctx))?
}
Hyprpm =>
{
#[cfg(unix)]
runner.execute(*self, "hyprpm", || unix::run_hyprpm(ctx))?
}
JetbrainsAqua => runner.execute(*self, "JetBrains Aqua Plugins", || generic::run_jetbrains_aqua(ctx))?,
JetbrainsClion => runner.execute(*self, "JetBrains CL", || generic::run_jetbrains_clion(ctx))?,
JetbrainsDatagrip => {
runner.execute(*self, "JetBrains DataGrip", || generic::run_jetbrains_datagrip(ctx))?
}
JetbrainsDataspell => runner.execute(*self, "JetBrains DataSpell Plugins", || {
generic::run_jetbrains_dataspell(ctx)
})?,
JetbrainsGateway => runner.execute(*self, "JetBrains Gateway Plugins", || {
generic::run_jetbrains_gateway(ctx)
})?,
JetbrainsGoland => {
runner.execute(*self, "JetBrains GoLand Plugins", || generic::run_jetbrains_goland(ctx))?
}
JetbrainsIdea => runner.execute(*self, "JetBrains IntelliJ IDEA Plugins", || {
generic::run_jetbrains_idea(ctx)
})?,
JetbrainsMps => runner.execute(*self, "JetBrains MPS Plugins", || generic::run_jetbrains_mps(ctx))?,
JetbrainsPhpstorm => runner.execute(*self, "JetBrains PhpStorm Plugins", || {
generic::run_jetbrains_phpstorm(ctx)
})?,
JetbrainsPycharm => runner.execute(*self, "JetBrains PyCharm Plugins", || {
generic::run_jetbrains_pycharm(ctx)
})?,
JetbrainsRider => runner.execute(*self, "JetBrains Rider Plugins", || generic::run_jetbrains_rider(ctx))?,
JetbrainsRubymine => runner.execute(*self, "JetBrains RubyMine Plugins", || {
generic::run_jetbrains_rubymine(ctx)
})?,
JetbrainsRustrover => runner.execute(*self, "JetBrains RustRover Plugins", || {
generic::run_jetbrains_rustrover(ctx)
})?,
JetbrainsToolbox => runner.execute(*self, "JetBrains Toolbox", || generic::run_jetbrains_toolbox(ctx))?,
JetbrainsWebstorm => runner.execute(*self, "JetBrains WebStorm Plugins", || {
generic::run_jetbrains_webstorm(ctx)
})?,
Jetpack => runner.execute(*self, "jetpack", || generic::run_jetpack(ctx))?,
Julia => runner.execute(*self, "julia", || generic::update_julia_packages(ctx))?,
Juliaup => runner.execute(*self, "juliaup", || generic::run_juliaup(ctx))?,
Kakoune => runner.execute(*self, "Kakoune", || kakoune::upgrade_kak_plug(ctx))?,
Krew => runner.execute(*self, "krew", || generic::run_krew_upgrade(ctx))?,
Lensfun => runner.execute(*self, "Lensfun's database update", || {
generic::run_lensfun_update_data(ctx)
})?,
Lure =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "LURE", || linux::run_lure_update(ctx))?
}
Macports =>
{
#[cfg(target_os = "macos")]
runner.execute(*self, "MacPorts", || macos::run_macports(ctx))?
}
Mamba => runner.execute(*self, "mamba", || generic::run_mamba_update(ctx))?,
Mandb =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "Manual Entries", || linux::run_mandb(ctx))?
}
Mas =>
{
#[cfg(target_os = "macos")]
runner.execute(*self, "App Store", || macos::run_mas(ctx))?
}
Maza =>
{
#[cfg(unix)]
runner.execute(*self, "maza", || unix::run_maza(ctx))?
}
Micro => runner.execute(*self, "micro", || generic::run_micro(ctx))?,
MicrosoftStore =>
{
#[cfg(windows)]
runner.execute(*self, "Microsoft Store", || windows::microsoft_store(ctx))?
}
Miktex => runner.execute(*self, "miktex", || generic::run_miktex_packages_update(ctx))?,
Mise =>
{
#[cfg(unix)]
runner.execute(*self, "mise", || unix::run_mise(ctx))?
}
Myrepos => runner.execute(*self, "myrepos", || generic::run_myrepos_update(ctx))?,
Nix => {
#[cfg(unix)]
runner.execute(*self, "nix", || unix::run_nix(ctx))?;
#[cfg(unix)]
runner.execute(*self, "nix upgrade-nix", || unix::run_nix_self_upgrade(ctx))?
}
NixHelper =>
{
#[cfg(unix)]
runner.execute(*self, "nh", || unix::run_nix_helper(ctx))?
}
Node => runner.execute(*self, "npm", || node::run_npm_upgrade(ctx))?,
Opam => runner.execute(*self, "opam", || generic::run_opam_update(ctx))?,
Pacdef =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "pacdef", || linux::run_pacdef(ctx))?
}
Pacstall =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "pacstall", || linux::run_pacstall(ctx))?
}
Pearl =>
{
#[cfg(unix)]
runner.execute(*self, "pearl", || unix::run_pearl(ctx))?
}
Pip3 => runner.execute(*self, "pip3", || generic::run_pip3_update(ctx))?,
PipReview => runner.execute(*self, "pip-review", || generic::run_pip_review_update(ctx))?,
PipReviewLocal => runner.execute(*self, "pip-review (local)", || {
generic::run_pip_review_local_update(ctx)
})?,
Pipupgrade => runner.execute(*self, "pipupgrade", || generic::run_pipupgrade_update(ctx))?,
Pipx => runner.execute(*self, "pipx", || generic::run_pipx_update(ctx))?,
Pipxu => runner.execute(*self, "pipxu", || generic::run_pipxu_update(ctx))?,
Pixi => runner.execute(*self, "pixi", || generic::run_pixi_update(ctx))?,
Pkg => {
#[cfg(target_os = "dragonfly")]
runner.execute(*self, "Dragonfly BSD Packages", || dragonfly::upgrade_packages(ctx))?;
#[cfg(target_os = "freebsd")]
runner.execute(*self, "FreeBSD Packages", || freebsd::upgrade_packages(ctx))?;
#[cfg(target_os = "openbsd")]
runner.execute(*self, "OpenBSD Packages", || openbsd::upgrade_packages(ctx))?;
#[cfg(target_os = "android")]
runner.execute(*self, "Termux Packages", || android::upgrade_packages(ctx))?
}
Pkgfile =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "pkgfile", || linux::run_pkgfile(ctx))?
}
Pkgin =>
{
#[cfg(unix)]
runner.execute(*self, "pkgin", || unix::run_pkgin(ctx))?
}
PlatformioCore => runner.execute(*self, "PlatformIO Core", || generic::run_platform_io(ctx))?,
Pnpm => runner.execute(*self, "pnpm", || node::run_pnpm_upgrade(ctx))?,
Poetry => runner.execute(*self, "Poetry", || generic::run_poetry(ctx))?,
Powershell => runner.execute(*self, "Powershell Modules Update", || generic::run_powershell(ctx))?,
Protonup =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "protonup", || linux::run_protonup_update(ctx))?
}
Pyenv =>
{
#[cfg(unix)]
runner.execute(*self, "pyenv", || unix::run_pyenv(ctx))?
}
Raco => runner.execute(*self, "raco", || generic::run_raco_update(ctx))?,
Rcm =>
{
#[cfg(unix)]
runner.execute(*self, "rcm", || unix::run_rcm(ctx))?
}
Remotes => {
if let Some(topgrades) = ctx.config().remote_topgrades() {
for remote_topgrade in topgrades
.iter()
.filter(|t| ctx.config().should_execute_remote(hostname(), t))
{
runner.execute(*self, format!("Remote ({remote_topgrade})"), || {
crate::ssh::ssh_step(ctx, remote_topgrade)
})?;
}
}
}
Restarts =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "Restarts", || linux::run_needrestart(ctx))?
}
Rtcl => runner.execute(*self, "rtcl", || generic::run_rtcl(ctx))?,
RubyGems => runner.execute(*self, "rubygems", || generic::run_rubygems(ctx))?,
Rustup => runner.execute(*self, "rustup", || generic::run_rustup(ctx))?,
Rye => runner.execute(*self, "rye", || generic::run_rye(ctx))?,
Scoop =>
{
#[cfg(windows)]
runner.execute(*self, "Scoop", || windows::run_scoop(ctx))?
}
Sdkman =>
{
#[cfg(unix)]
runner.execute(*self, "SDKMAN!", || unix::run_sdkman(ctx))?
}
SelfUpdate => {
// 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")]
{
if std::env::var("TOPGRADE_NO_SELF_UPGRADE").is_err() && !ctx.config().no_self_update() {
runner.execute(*self, "Self Update", || self_update::self_update(ctx))?;
}
}
}
Sheldon => runner.execute(*self, "sheldon", || generic::run_sheldon(ctx))?,
Shell => {
#[cfg(unix)]
{
runner.execute(*self, "zr", || zsh::run_zr(ctx))?;
runner.execute(*self, "antibody", || zsh::run_antibody(ctx))?;
runner.execute(*self, "antidote", || zsh::run_antidote(ctx))?;
runner.execute(*self, "antigen", || zsh::run_antigen(ctx))?;
runner.execute(*self, "zgenom", || zsh::run_zgenom(ctx))?;
runner.execute(*self, "zplug", || zsh::run_zplug(ctx))?;
runner.execute(*self, "zinit", || zsh::run_zinit(ctx))?;
runner.execute(*self, "zi", || zsh::run_zi(ctx))?;
runner.execute(*self, "zim", || zsh::run_zim(ctx))?;
runner.execute(*self, "oh-my-zsh", || zsh::run_oh_my_zsh(ctx))?;
runner.execute(*self, "oh-my-bash", || unix::run_oh_my_bash(ctx))?;
runner.execute(*self, "fisher", || unix::run_fisher(ctx))?;
runner.execute(*self, "bash-it", || unix::run_bashit(ctx))?;
runner.execute(*self, "oh-my-fish", || unix::run_oh_my_fish(ctx))?;
runner.execute(*self, "fish-plug", || unix::run_fish_plug(ctx))?;
runner.execute(*self, "fundle", || unix::run_fundle(ctx))?
}
}
Snap =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "snap", || linux::run_snap(ctx))?
}
Sparkle =>
{
#[cfg(target_os = "macos")]
runner.execute(*self, "Sparkle", || macos::run_sparkle(ctx))?
}
Spicetify => runner.execute(*self, "spicetify", || generic::spicetify_upgrade(ctx))?,
Stack => runner.execute(*self, "stack", || generic::run_stack_update(ctx))?,
Stew => runner.execute(*self, "stew", || generic::run_stew(ctx))?,
System => {
#[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(Shell, "packer.nu", || linux::run_packer_nu(ctx))?;
match ctx.distribution() {
Ok(distribution) => {
runner.execute(*self, "System update", || distribution.upgrade(ctx))?;
}
Err(e) => {
println!("{}", t!("Error detecting current distribution: {error}", error = e));
}
}
runner.execute(*self, "pihole", || linux::run_pihole_update(ctx))?;
}
#[cfg(windows)]
runner.execute(*self, "Windows update", || windows::windows_update(ctx))?;
#[cfg(target_os = "macos")]
runner.execute(*self, "System update", || macos::upgrade_macos(ctx))?;
#[cfg(target_os = "freebsd")]
runner.execute(*self, "FreeBSD Upgrade", || freebsd::upgrade_freebsd(ctx))?;
#[cfg(target_os = "openbsd")]
runner.execute(*self, "OpenBSD Upgrade", || openbsd::upgrade_openbsd(ctx))?
}
Tldr => runner.execute(*self, "TLDR", || generic::run_tldr(ctx))?,
Tlmgr => runner.execute(*self, "tlmgr", || generic::run_tlmgr_update(ctx))?,
Tmux =>
{
#[cfg(unix)]
runner.execute(*self, "tmux", || tmux::run_tpm(ctx))?
}
Toolbx =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "toolbx", || toolbx::run_toolbx(ctx))?
}
Typst => runner.execute(*self, "Typst", || generic::run_typst(ctx))?,
Uv => runner.execute(*self, "uv", || generic::run_uv(ctx))?,
Vagrant => {
if ctx.config().should_run(Vagrant) {
if let Ok(boxes) = vagrant::collect_boxes(ctx) {
for vagrant_box in boxes {
runner.execute(*self, format!("Vagrant ({})", vagrant_box.smart_name()), || {
vagrant::topgrade_vagrant_box(ctx, &vagrant_box)
})?;
}
}
}
runner.execute(*self, "Vagrant boxes", || vagrant::upgrade_vagrant_boxes(ctx))?;
}
Vcpkg => runner.execute(*self, "vcpkg", || generic::run_vcpkg_update(ctx))?,
Vim => {
runner.execute(*self, "vim", || vim::upgrade_vim(ctx))?;
runner.execute(*self, "Neovim", || vim::upgrade_neovim(ctx))?;
runner.execute(*self, "The Ultimate vimrc", || vim::upgrade_ultimate_vimrc(ctx))?;
runner.execute(*self, "voom", || vim::run_voom(ctx))?
}
VoltaPackages => runner.execute(*self, "volta packages", || node::run_volta_packages_upgrade(ctx))?,
Vscode => runner.execute(*self, "Visual Studio Code extensions", || {
generic::run_vscode_extensions_update(ctx)
})?,
VscodeInsiders => runner.execute(*self, "Visual Studio Code Insiders extensions", || {
generic::run_vscode_insiders_extensions_update(ctx)
})?,
Vscodium => runner.execute(*self, "VSCodium extensions", || {
generic::run_vscodium_extensions_update(ctx)
})?,
VscodiumInsiders => runner.execute(*self, "VSCodium Insiders extensions", || {
generic::run_vscodium_insiders_extensions_update(ctx)
})?,
Waydroid =>
{
#[cfg(target_os = "linux")]
runner.execute(*self, "Waydroid", || linux::run_waydroid(ctx))?
}
Winget =>
{
#[cfg(windows)]
runner.execute(*self, "Winget", || windows::run_winget(ctx))?
}
Wsl =>
{
#[cfg(windows)]
runner.execute(*self, "WSL", || windows::run_wsl_topgrade(ctx))?
}
WslUpdate =>
{
#[cfg(windows)]
runner.execute(*self, "Update WSL", || windows::update_wsl(ctx))?
}
Xcodes =>
{
#[cfg(target_os = "macos")]
runner.execute(*self, "Xcodes", || macos::update_xcodes(ctx))?
}
Yadm =>
{
#[cfg(unix)]
runner.execute(*self, "yadm", || unix::run_yadm(ctx))?
}
Yarn => runner.execute(*self, "yarn", || node::run_yarn_upgrade(ctx))?,
Yazi => runner.execute(*self, "Yazi packages", || generic::run_yazi(ctx))?,
Zigup => runner.execute(*self, "zigup", || generic::run_zigup(ctx))?,
Zvm => runner.execute(*self, "ZVM", || generic::run_zvm(ctx))?,
}
Ok(())
}
}
#[allow(clippy::too_many_lines)]
pub(crate) fn default_steps() -> Vec<Step> {
// For now, SelfRenamer and SelfUpdate isn't included as they're ran before the other non-steps (pre-commands, sudo, etc)
use Step::*;
// Could probably have a smaller starting capacity, but this at least ensures only 2 allocations:
// initial and shrink
let mut steps = Vec::with_capacity(Step::COUNT);
// Not combined with other generic steps to preserve the order as it was in main.rs originally,
// but this can be changed in the future.
steps.push(Remotes);
#[cfg(windows)]
steps.extend_from_slice(&[Wsl, WslUpdate, Chocolatey, Scoop, Winget, System, MicrosoftStore]);
#[cfg(target_os = "macos")]
steps.extend_from_slice(&[BrewFormula, BrewCask, Macports, Xcodes, Sparkle, Mas, System]);
#[cfg(target_os = "dragonfly")]
steps.extend_from_slice(&[Pkg, Audit]);
#[cfg(target_os = "freebsd")]
steps.extend_from_slice(&[Pkg, System, Audit]);
#[cfg(target_os = "openbsd")]
steps.extend_from_slice(&[Pkg, System]);
#[cfg(target_os = "android")]
steps.push(Pkg);
#[cfg(target_os = "linux")]
steps.extend_from_slice(&[
System,
ConfigUpdate,
AM,
AppMan,
DebGet,
Toolbx,
Snap,
Pacstall,
Pacdef,
Protonup,
Distrobox,
DkpPacman,
Firmware,
Restarts,
Flatpak,
BrewFormula,
BrewCask,
Lure,
Waydroid,
AutoCpufreq,
CinnamonSpices,
Mandb,
Pkgfile,
]);
#[cfg(unix)]
steps.extend_from_slice(&[
Yadm,
Nix,
NixHelper,
Guix,
HomeManager,
Asdf,
Mise,
Pkgin,
BunPackages,
Shell,
Tmux,
Pearl,
#[cfg(not(any(target_os = "macos", target_os = "android")))]
GnomeShellExtensions,
Pyenv,
Sdkman,
Rcm,
Maza,
Hyprpm,
Atuin,
]);
#[cfg(not(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
steps.push(Atom);
// The following update function should be executed on all OSes.
steps.extend_from_slice(&[
Fossil,
Elan,
Rye,
Rustup,
Juliaup,
Dotnet,
Choosenim,
Cargo,
Flutter,
Go,
Emacs,
Opam,
Vcpkg,
Pipx,
Pipxu,
Vscode,
VscodeInsiders,
Vscodium,
VscodiumInsiders,
Conda,
Mamba,
Pixi,
Miktex,
Pip3,
PipReview,
PipReviewLocal,
Pipupgrade,
Ghcup,
Stack,
Tldr,
Tlmgr,
Myrepos,
Chezmoi,
Jetpack,
Vim,
Kakoune,
Helix,
Node,
Yarn,
Pnpm,
VoltaPackages,
Containers,
Deno,
Composer,
Krew,
Helm,
Gem,
RubyGems,
Julia,
Haxelib,
Sheldon,
Stew,
Rtcl,
Bin,
Gcloud,
Micro,
Raco,
Spicetify,
GithubCliExtensions,
Bob,
Certbot,
GitRepos,
ClamAvDb,
PlatformioCore,
Lensfun,
Poetry,
Uv,
Zvm,
Aqua,
Bun,
Zigup,
JetbrainsToolbox,
AndroidStudio,
JetbrainsAqua,
JetbrainsClion,
JetbrainsDatagrip,
JetbrainsDataspell,
// JetBrains dotCover has no CLI
// JetBrains dotMemory has no CLI
// JetBrains dotPeek has no CLI
// JetBrains dotTrace has no CLI
// JetBrains Fleet has a different CLI without a `fleet update` command.
JetbrainsGateway,
JetbrainsGoland,
JetbrainsIdea,
JetbrainsMps,
JetbrainsPhpstorm,
JetbrainsPycharm,
// JetBrains ReSharper has no CLI (it's a VSCode extension)
// JetBrains ReSharper C++ has no CLI (it's a VSCode extension)
JetbrainsRider,
JetbrainsRubymine,
JetbrainsRustrover,
// JetBrains Space Desktop does not have a CLI
JetbrainsWebstorm,
Yazi,
Falconf,
Powershell,
CustomCommands,
Vagrant,
Typst,
]);
steps.shrink_to_fit();
steps
}

View File

@@ -1,186 +1,239 @@
use std::fmt::{Display, Formatter};
use std::path::Path;
use std::process::Command;
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use tracing::{debug, error, warn};
use wildmatch::WildMatch;
use crate::command::CommandExt;
use crate::error::{self, TopgradeError};
use crate::terminal::print_separator;
use crate::{execution_context::ExecutionContext, utils::require};
// A string found in the output of docker for containers that weren't found in
// the docker registry. We use this to gracefully handle and skip containers
// that cannot be pulled, likely because they don't exist in the registry in
// the first place. This happens e.g. when the user tags an image locally
// themselves or when using docker-compose.
const NONEXISTENT_REPO: &str = "repository does not exist";
/// Uniquely identifies a `Container`.
#[derive(Debug)]
struct Container {
/// `Repository` and `Tag`
///
/// format: `Repository:Tag`, e.g., `nixos/nix:latest`.
repo_tag: String,
/// Platform
///
/// format: `OS/Architecture`, e.g., `linux/amd64`.
platform: String,
}
impl Container {
/// Construct a new `Container`.
fn new(repo_tag: String, platform: String) -> Self {
Self { repo_tag, platform }
}
}
impl Display for Container {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
// e.g., "`fedora:latest` for `linux/amd64`"
write!(f, "`{}` for `{}`", self.repo_tag, self.platform)
}
}
/// Returns a Vector of all containers, with Strings in the format
/// "REGISTRY/[PATH/]CONTAINER_NAME:TAG"
///
/// Containers specified in `ignored_containers` will be filtered out.
fn list_containers(crt: &Path, ignored_containers: Option<&Vec<String>>) -> Result<Vec<Container>> {
let ignored_containers = ignored_containers.map(|patterns| {
patterns
.iter()
.map(|pattern| WildMatch::new(pattern))
.collect::<Vec<WildMatch>>()
});
debug!(
"Querying '{} image ls --format \"{{{{.Repository}}}}:{{{{.Tag}}}}/{{{{.ID}}}}\"' for containers",
crt.display()
);
let output = Command::new(crt)
.args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}"])
.output_checked_with_utf8(|_| Ok(()))?;
let mut retval = vec![];
for line in output.stdout.lines() {
if line.starts_with("localhost") {
// Don't know how to update self-built containers
debug!("Skipping self-built container '{}'", line);
continue;
}
if line.contains("<none>") {
// Bogus/dangling container or intermediate layer
debug!("Skipping bogus container '{}'", line);
continue;
}
if line.starts_with("vsc-") {
debug!("Skipping visual studio code dev container '{}'", line);
continue;
}
debug!("Using container '{}'", line);
// line is of format: `Repository:Tag ImageID`, e.g., `nixos/nix:latest d80fea9c32b4`
let split_res = line.split(' ').collect::<Vec<&str>>();
assert_eq!(split_res.len(), 2);
let (repo_tag, image_id) = (split_res[0], split_res[1]);
if let Some(ref ignored_containers) = ignored_containers {
if ignored_containers.iter().any(|pattern| pattern.matches(repo_tag)) {
debug!("Skipping ignored container '{}'", line);
continue;
}
}
debug!(
"Querying '{} image inspect --format \"{{{{.Os}}}}/{{{{.Architecture}}}}\"' for container {}",
crt.display(),
image_id
);
let inspect_output = Command::new(crt)
.args(["image", "inspect", image_id, "--format", "{{.Os}}/{{.Architecture}}"])
.output_checked_with_utf8(|_| Ok(()))?;
let mut platform = inspect_output.stdout;
// truncate the tailing new line character
platform.truncate(platform.len() - 1);
assert!(platform.contains('/'));
retval.push(Container::new(repo_tag.to_string(), platform));
}
Ok(retval)
}
pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
// Prefer podman, fall back to docker if not present
let crt = require("podman").or_else(|_| require("docker"))?;
debug!("Using container runtime '{}'", crt.display());
print_separator("Containers");
let mut success = true;
let containers =
list_containers(&crt, ctx.config().containers_ignored_tags()).context("Failed to list Docker containers")?;
debug!("Containers to inspect: {:?}", containers);
for container in containers.iter() {
debug!("Pulling container '{}'", container);
let args = vec![
"pull",
container.repo_tag.as_str(),
"--platform",
container.platform.as_str(),
];
let mut exec = ctx.run_type().execute(&crt);
if let Err(e) = exec.args(&args).status_checked() {
error!("Pulling container '{}' failed: {}", container, e);
// Find out if this is 'skippable'
// This is necessary e.g. for docker, because unlike podman docker doesn't tell from
// which repository a container originates (such as `docker.io`). This has the
// practical consequence that all containers, whether self-built, created by
// docker-compose or pulled from the docker hub, look exactly the same to us. We can
// only find out what went wrong by manually parsing the output of the command...
if match exec.output_checked_utf8() {
Ok(s) => s.stdout.contains(NONEXISTENT_REPO) || s.stderr.contains(NONEXISTENT_REPO),
Err(e) => match e.downcast_ref::<TopgradeError>() {
Some(TopgradeError::ProcessFailedWithOutput(_, _, stderr)) => stderr.contains(NONEXISTENT_REPO),
_ => false,
},
} {
warn!("Skipping unknown container '{}'", container);
continue;
}
success = false;
}
}
if ctx.config().cleanup() {
// Remove dangling images
debug!("Removing dangling images");
if let Err(e) = ctx
.run_type()
.execute(&crt)
.args(["image", "prune", "-f"])
.status_checked()
{
error!("Removing dangling images failed: {}", e);
success = false;
}
}
if success {
Ok(())
} else {
Err(eyre!(error::StepFailed))
}
}
use std::fmt::{Display, Formatter};
use std::io;
use std::io::Write;
use std::path::Path;
use std::process::Command;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use color_eyre::eyre::{OptionExt, eyre};
use tracing::{debug, error, warn};
use wildmatch::WildMatch;
use crate::command::CommandExt;
use crate::error::{SkipStep, TopgradeError};
use crate::terminal::print_separator;
use crate::{execution_context::ExecutionContext, utils::require};
use rust_i18n::t;
// A string found in the output of docker for containers that weren't found in
// the docker registry. We use this to gracefully handle and skip containers
// that cannot be pulled, likely because they don't exist in the registry in
// the first place. This happens e.g. when the user tags an image locally
// themselves or when using docker-compose.
const NONEXISTENT_REPO: &str = "repository does not exist";
// A string found in the output of docker when Docker Desktop is not running.
const DOCKER_NOT_RUNNING: &str = "We recommend to activate the WSL integration in Docker Desktop settings.";
/// Uniquely identifies a `Container`.
#[derive(Debug)]
struct Container {
/// `Repository` and `Tag`
///
/// format: `Repository:Tag`, e.g., `nixos/nix:latest`.
repo_tag: String,
/// Platform
///
/// format: `OS/Architecture`, e.g., `linux/amd64`.
platform: String,
}
impl Container {
/// Construct a new `Container`.
fn new(repo_tag: String, platform: String) -> Self {
Self { repo_tag, platform }
}
}
impl Display for Container {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
// e.g., "`fedora:latest` for `linux/amd64`"
write!(
f,
"{}",
t!(
"`{repo_tag}` for `{platform}`",
repo_tag = self.repo_tag,
platform = self.platform
)
)
}
}
/// Returns a Vector of all containers, with Strings in the format
/// "REGISTRY/[PATH/]CONTAINER_NAME:TAG"
///
/// Containers specified in `ignored_containers` will be filtered out.
fn list_containers(crt: &Path, ignored_containers: Option<&Vec<String>>) -> Result<Vec<Container>> {
let ignored_containers = ignored_containers.map(|patterns| {
patterns
.iter()
.map(|pattern| WildMatch::new(pattern))
.collect::<Vec<WildMatch>>()
});
debug!(
"Querying '{} image ls --format \"{{{{.Repository}}}}:{{{{.Tag}}}}/{{{{.ID}}}}\"' for containers",
crt.display()
);
let output = Command::new(crt)
.args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}"])
.output_checked_utf8()?;
let mut retval = vec![];
for line in output.stdout.lines() {
if line.starts_with("localhost") {
// Don't know how to update self-built containers
debug!("Skipping self-built container '{}'", line);
continue;
}
if line.contains("<none>") {
// Bogus/dangling container or intermediate layer
debug!("Skipping bogus container '{}'", line);
continue;
}
if line.starts_with("vsc-") {
debug!("Skipping visual studio code dev container '{}'", line);
continue;
}
debug!("Using container '{}'", line);
// line is of format: `Repository:Tag ImageID`, e.g., `nixos/nix:latest d80fea9c32b4`
let split_res = line.split(' ').collect::<Vec<&str>>();
if split_res.len() != 2 {
return Err(eyre!(format!(
"Got erroneous output from `{} image ls --format \"{{.Repository}}:{{.Tag}} {{.ID}}\"; Expected line to split into 2 parts",
crt.display()
)));
}
let (repo_tag, image_id) = (split_res[0], split_res[1]);
if let Some(ref ignored_containers) = ignored_containers {
if ignored_containers.iter().any(|pattern| pattern.matches(repo_tag)) {
debug!("Skipping ignored container '{}'", line);
continue;
}
}
debug!(
"Querying '{} image inspect --format \"{{{{.Os}}}}/{{{{.Architecture}}}}\"' for container {}",
crt.display(),
image_id
);
let inspect_output = Command::new(crt)
.args(["image", "inspect", image_id, "--format", "{{.Os}}/{{.Architecture}}"])
.output_checked_utf8()?;
let mut platform = inspect_output.stdout;
// truncate the tailing new line character
platform.truncate(platform.len() - 1);
if !platform.contains('/') {
return Err(eyre!(format!(
"Got erroneous output from `{} image ls --format \"{{.Repository}}:{{.Tag}} {{.ID}}\"; Expected platform to contain '/'",
crt.display()
)));
}
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 output = Command::new(&crt).arg("--help").output_checked_with(|_| Ok(()))?;
let status_code = output
.status
.code()
.ok_or_eyre("Couldn't get status code (terminated by signal)")?;
let stdout = std::str::from_utf8(&output.stdout).wrap_err("Expected output to be valid UTF-8")?;
if stdout.contains(DOCKER_NOT_RUNNING) && status_code == 1 {
// Write the output
io::stdout().write_all(&output.stdout)?;
io::stderr().write_all(&output.stderr)?;
// Don't crash, but don't be silent either.
// This can happen in other ways than Docker Desktop not running, but even in those cases
// we don't want to crash, since the containers step is enabled by default.
warn!(
"{} seems to be non-functional right now (see above). Is WSL integration enabled for Docker Desktop? Is Docker Desktop running?",
crt.display()
);
return Err(SkipStep(format!(
"{} seems to be non-functional right now. Possibly WSL integration is not enabled for Docker Desktop, or Docker Desktop is not running.",
crt.display()
)).into());
} else if !output.status.success() {
// Write the output
io::stdout().write_all(&output.stdout)?;
io::stderr().write_all(&output.stderr)?;
// If we saw the message, but the code is not 1 (e.g. 0, or a non-1 failure), crash, as we expect a 1.
// If we did not see the message, it's broken in some way we do not understand.
return Err(eyre!(
"{0} seems to be non-functional (`{0} --help` returned non-zero exit code {1})",
crt.display(),
status_code,
));
}
let containers =
list_containers(&crt, ctx.config().containers_ignored_tags()).context("Failed to list Docker containers")?;
debug!("Containers to inspect: {:?}", containers);
for container in &containers {
debug!("Pulling container '{}'", container);
let mut args = vec!["pull", container.repo_tag.as_str()];
if container.platform.as_str() != "/" {
args.push("--platform");
args.push(container.platform.as_str());
}
let mut exec = ctx.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;
}
return Err(e);
}
}
if ctx.config().containers_system_prune() {
// Run system prune to clean up unused containers, networks, and build cache
ctx.execute(&crt)
.args(["system", "prune", "--force"])
.status_checked()?
// Only run `image prune` if we don't run `system prune`
} else if ctx.config().cleanup() {
// Remove dangling images
debug!("Removing dangling images");
ctx.execute(&crt).args(["image", "prune", "-f"]).status_checked()?
}
Ok(())
}

View File

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

View File

@@ -4,12 +4,13 @@ use std::path::{Path, PathBuf};
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
use rust_i18n::t;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::step::Step;
use crate::terminal::print_separator;
use crate::utils::{require, require_option, PathExt};
use crate::Step;
use crate::utils::{PathExt, require, require_option};
const EMACS_UPGRADE: &str = include_str!("emacs.el");
#[cfg(windows)]
@@ -59,12 +60,16 @@ impl Emacs {
fn update_doom(doom: &Path, ctx: &ExecutionContext) -> Result<()> {
print_separator("Doom Emacs");
let mut command = ctx.run_type().execute(doom);
let mut command = ctx.execute(doom);
if ctx.config().yes(Step::Emacs) {
command.arg("--force");
}
command.args(["upgrade"]);
command.arg("upgrade");
if ctx.config().doom_aot() {
command.arg("--aot");
}
command.status_checked()
}
@@ -74,13 +79,16 @@ impl Emacs {
if let Some(doom) = &self.doom {
Emacs::update_doom(doom, ctx)?;
}
let init_file = require_option(self.directory.as_ref(), String::from("Emacs directory does not exist"))?
.join("init.el")
.require()?;
let init_file = require_option(
self.directory.as_ref(),
t!("Emacs directory does not exist").to_string(),
)?
.join("init.el")
.require()?;
print_separator("Emacs");
let mut command = ctx.run_type().execute(emacs);
let mut command = ctx.execute(emacs);
command
.args(["--batch", "--debug-init", "-l"])

File diff suppressed because it is too large Load Diff

View File

@@ -4,22 +4,23 @@ use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use color_eyre::eyre::Context;
use color_eyre::eyre::{eyre, Result};
use color_eyre::eyre::{Result, eyre};
use console::style;
use futures::stream::{iter, FuturesUnordered, StreamExt};
use glob::{glob_with, MatchOptions};
use futures::stream::{FuturesUnordered, StreamExt, iter};
use glob::{MatchOptions, glob_with};
use tokio::process::Command as AsyncCommand;
use tokio::runtime;
use tracing::{debug, error};
use crate::command::CommandExt;
use crate::config::Step;
use crate::execution_context::ExecutionContext;
use crate::step::Step;
use crate::steps::emacs::Emacs;
use crate::terminal::print_separator;
use crate::utils::{require, PathExt};
use crate::{error::SkipStep, terminal::print_warning, HOME_DIR};
use crate::utils::{PathExt, require};
use crate::{HOME_DIR, error::SkipStep, terminal::print_warning};
use etcetera::base_strategy::BaseStrategy;
use rust_i18n::t;
#[cfg(unix)]
use crate::XDG_DIRS;
@@ -57,9 +58,10 @@ pub fn run_git_pull(ctx: &ExecutionContext) -> Result<()> {
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);
if let Some(powershell) = ctx.powershell() {
if let Some(profile) = powershell.profile() {
repos.insert_if_repo(profile);
}
}
}
@@ -91,7 +93,7 @@ pub fn run_git_pull(ctx: &ExecutionContext) -> Result<()> {
// 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);
repos.glob_insert(&shellexpand::tilde(git_repo));
}
}
@@ -100,16 +102,18 @@ pub fn run_git_pull(ctx: &ExecutionContext) -> Result<()> {
// NOTE: this should be executed **before** skipping the Git step or the
// user won't receive this warning in the cases where all the paths configured
// are bad patterns.
repos
.bad_patterns
.iter()
.for_each(|pattern| print_warning(format!("Path {pattern} did not contain any git repositories")));
repos.bad_patterns.iter().for_each(|pattern| {
print_warning(t!(
"Path {pattern} did not contain any git repositories",
pattern = pattern
));
});
if repos.is_repos_empty() {
return Err(SkipStep(String::from("No repositories to pull")).into());
return Err(SkipStep(t!("No repositories to pull").to_string()).into());
}
print_separator("Git repositories");
print_separator(t!("Git repositories"));
repos.pull_repos(ctx)
}
@@ -143,7 +147,7 @@ fn get_head_revision<P: AsRef<Path>>(git: &Path, repo: P) -> Option<String> {
.output_checked_utf8()
.map(|output| output.stdout.trim().to_string())
.map_err(|e| {
error!("Error getting revision for {}: {}", repo.as_ref().display(), e);
error!("Error getting revision for {}: {e}", repo.as_ref().display(),);
e
})
@@ -204,10 +208,13 @@ impl RepoStep {
return output;
}
Err(e) => match e.kind() {
io::ErrorKind::NotFound => debug!("{} does not exist", path.as_ref().display()),
_ => error!("Error looking for {}: {}", path.as_ref().display(), e),
},
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
debug!("{} does not exist", path.as_ref().display());
} else {
error!("Error looking for {}: {e}", path.as_ref().display());
}
}
}
None
@@ -236,7 +243,7 @@ impl RepoStep {
res.map(|output| output.stdout.lines().count() > 0)
.map_err(|e| {
error!("Error getting remotes for {}: {}", repo.as_ref().display(), e);
error!("Error getting remotes for {}: {e}", repo.as_ref().display());
e
})
.ok()
@@ -264,7 +271,7 @@ impl RepoStep {
}
}
Err(e) => {
error!("Error in path {}", e);
error!("Error in path {e}");
}
}
}
@@ -273,7 +280,7 @@ impl RepoStep {
self.bad_patterns.push(String::from(pattern));
}
} else {
error!("Bad glob pattern: {}", pattern);
error!("Bad glob pattern: {pattern}");
}
}
@@ -296,7 +303,7 @@ impl RepoStep {
let before_revision = get_head_revision(&self.git, &repo);
if ctx.config().verbose() {
println!("{} {}", style("Pulling").cyan().bold(), repo.as_ref().display());
println!("{} {}", style(t!("Pulling")).cyan().bold(), repo.as_ref().display());
}
let mut command = AsyncCommand::new(&self.git);
@@ -318,17 +325,22 @@ impl RepoStep {
.output()
.await?;
let result = output_checked_utf8(pull_output)
.and_then(|_| output_checked_utf8(submodule_output))
.and_then(|()| output_checked_utf8(submodule_output))
.wrap_err_with(|| format!("Failed to pull {}", repo.as_ref().display()));
if result.is_err() {
println!("{} pulling {}", style("Failed").red().bold(), repo.as_ref().display());
println!(
"{} {} {}",
style(t!("Failed")).red().bold(),
t!("pulling"),
repo.as_ref().display()
);
} else {
let after_revision = get_head_revision(&self.git, repo.as_ref());
match (&before_revision, &after_revision) {
(Some(before), Some(after)) if before != after => {
println!("{} {}", style("Changed").yellow().bold(), repo.as_ref().display());
println!("{} {}", style(t!("Changed")).yellow().bold(), repo.as_ref().display());
Command::new(&self.git)
.stdin(Stdio::null())
@@ -345,13 +357,13 @@ impl RepoStep {
}
_ => {
if ctx.config().verbose() {
println!("{} {}", style("Up-to-date").green().bold(), repo.as_ref().display());
println!("{} {}", style(t!("Up-to-date")).green().bold(), repo.as_ref().display());
}
}
}
}
result.map(|_| ())
result
}
/// Pull the repositories specified in `self.repos`.
@@ -363,15 +375,16 @@ impl RepoStep {
if ctx.run_type().dry() {
self.repos
.iter()
.for_each(|repo| println!("Would pull {}", repo.display()));
.for_each(|repo| println!("{}", t!("Would pull {repo}", repo = repo.display())));
return Ok(());
}
if !ctx.config().verbose() {
println!(
"\n{} updated repositories will be shown...\n",
style("Only").green().bold()
"\n{} {}\n",
style(t!("Only")).green().bold(),
t!("updated repositories will be shown...")
);
}
@@ -381,9 +394,10 @@ impl RepoStep {
.filter(|repo| match self.has_remotes(repo) {
Some(false) => {
println!(
"{} {} because it has no remotes",
style("Skipping").yellow().bold(),
repo.display()
"{} {} {}",
style(t!("Skipping")).yellow().bold(),
repo.display(),
t!("because it has no remotes")
);
false
}
@@ -400,7 +414,7 @@ impl RepoStep {
let basic_rt = runtime::Runtime::new()?;
let results = basic_rt.block_on(async { stream_of_futures.collect::<Vec<Result<()>>>().await });
let error = results.into_iter().find(|r| r.is_err());
let error = results.into_iter().find(std::result::Result::is_err);
error.unwrap_or(Ok(()))
}
}

View File

@@ -15,7 +15,7 @@ pub fn run_go_global_update(ctx: &ExecutionContext) -> Result<()> {
print_separator("go-global-update");
ctx.run_type().execute(go_global_update).status_checked()
ctx.execute(go_global_update).status_checked()
}
/// <https://github.com/nao1215/gup>
@@ -24,7 +24,7 @@ pub fn run_go_gup(ctx: &ExecutionContext) -> Result<()> {
print_separator("gup");
ctx.run_type().execute(gup).arg("update").status_checked()
ctx.execute(gup).arg("update").status_checked()
}
/// Get the path of a Go binary.

View File

@@ -1,6 +1,7 @@
use crate::terminal::print_separator;
use crate::utils::require;
use color_eyre::eyre::Result;
use rust_i18n::t;
use crate::execution_context::ExecutionContext;
@@ -11,13 +12,10 @@ pub fn upgrade_kak_plug(ctx: &ExecutionContext) -> Result<()> {
print_separator("Kakoune");
// TODO: Why supress output for this command?
ctx.run_type()
.execute(kak)
.args(["-ui", "dummy", "-e", UPGRADE_KAK])
.output()?;
// TODO: Why suppress output for this command?
ctx.execute(kak).args(["-ui", "dummy", "-e", UPGRADE_KAK]).output()?;
println!("Plugins upgraded");
println!("{}", t!("Plugins upgraded"));
Ok(())
}

View File

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

View File

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

View File

@@ -3,15 +3,16 @@ use std::ffi::OsString;
use std::path::{Path, PathBuf};
use color_eyre::eyre;
use color_eyre::eyre::Result;
use color_eyre::eyre::{Context, Result};
use rust_i18n::t;
use walkdir::WalkDir;
use crate::command::CommandExt;
use crate::error::TopgradeError;
use crate::execution_context::ExecutionContext;
use crate::sudo::Sudo;
use crate::step::Step;
use crate::utils::which;
use crate::{config, Step};
use crate::{config, output_changed_message};
fn get_execution_path() -> OsString {
let mut path = OsString::from("/usr/bin:");
@@ -31,13 +32,12 @@ pub struct YayParu {
impl ArchPackageManager for YayParu {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
if ctx.config().show_arch_news() {
ctx.run_type()
.execute(&self.executable)
ctx.execute(&self.executable)
.arg("-Pw")
.status_checked_with_codes(&[1, 0])?;
}
let mut command = ctx.run_type().execute(&self.executable);
let mut command = ctx.execute(&self.executable);
command
.arg("--pacman")
@@ -52,7 +52,7 @@ impl ArchPackageManager for YayParu {
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable);
let mut command = ctx.execute(&self.executable);
command.arg("--pacman").arg(&self.pacman).arg("-Scc");
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
@@ -79,7 +79,7 @@ pub struct GarudaUpdate {
impl ArchPackageManager for GarudaUpdate {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let mut command = ctx.run_type().execute(&self.executable);
let mut command = ctx.execute(&self.executable);
command
.env("PATH", get_execution_path())
@@ -110,7 +110,7 @@ pub struct Trizen {
impl ArchPackageManager for Trizen {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let mut command = ctx.run_type().execute(&self.executable);
let mut command = ctx.execute(&self.executable);
command
.arg("-Syu")
@@ -123,7 +123,7 @@ impl ArchPackageManager for Trizen {
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable);
let mut command = ctx.execute(&self.executable);
command.arg("-Sc");
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
@@ -144,25 +144,22 @@ impl Trizen {
}
pub struct Pacman {
sudo: Sudo,
executable: PathBuf,
}
impl ArchPackageManager for Pacman {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let mut command = ctx.run_type().execute(&self.sudo);
command
.arg(&self.executable)
.arg("-Syu")
.env("PATH", get_execution_path());
let sudo = ctx.require_sudo()?;
let mut command = sudo.execute(ctx, &self.executable)?;
command.arg("-Syu").env("PATH", get_execution_path());
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.sudo);
command.arg(&self.executable).arg("-Scc");
let mut command = sudo.execute(ctx, &self.executable)?;
command.arg("-Scc");
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
}
@@ -174,10 +171,9 @@ impl ArchPackageManager for Pacman {
}
impl Pacman {
pub fn get(ctx: &ExecutionContext) -> Option<Self> {
pub fn get() -> Option<Self> {
Some(Self {
executable: which("powerpill").unwrap_or_else(|| PathBuf::from("pacman")),
sudo: ctx.sudo().to_owned()?,
})
}
}
@@ -196,7 +192,7 @@ impl Pikaur {
impl ArchPackageManager for Pikaur {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let mut command = ctx.run_type().execute(&self.executable);
let mut command = ctx.execute(&self.executable);
command
.arg("-Syu")
@@ -210,7 +206,7 @@ impl ArchPackageManager for Pikaur {
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable);
let mut command = ctx.execute(&self.executable);
command.arg("-Sc");
if ctx.config().yes(Step::System) {
command.arg("--noconfirm");
@@ -235,7 +231,7 @@ impl Pamac {
}
impl ArchPackageManager for Pamac {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let mut command = ctx.run_type().execute(&self.executable);
let mut command = ctx.execute(&self.executable);
command
.arg("upgrade")
@@ -249,7 +245,7 @@ impl ArchPackageManager for Pamac {
command.status_checked()?;
if ctx.config().cleanup() {
let mut command = ctx.run_type().execute(&self.executable);
let mut command = ctx.execute(&self.executable);
command.arg("clean");
if ctx.config().yes(Step::System) {
command.arg("--no-confirm");
@@ -263,46 +259,67 @@ impl ArchPackageManager for Pamac {
pub struct Aura {
executable: PathBuf,
sudo: Sudo,
}
impl Aura {
fn get(ctx: &ExecutionContext) -> Option<Self> {
fn get() -> Option<Self> {
Some(Self {
executable: which("aura")?,
sudo: ctx.sudo().to_owned()?,
})
}
}
impl ArchPackageManager for Aura {
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let sudo = which("sudo").unwrap_or_default();
let mut aur_update = ctx.run_type().execute(&sudo);
use semver::Version;
if sudo.ends_with("sudo") {
aur_update
.arg(&self.executable)
.arg("-Au")
let version_cmd_output = ctx.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)
.wrap_err_with(|| output_changed_message!("aura --version", "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.execute(&self.executable);
cmd.arg("-Au")
.args(ctx.config().aura_aur_arguments().split_whitespace());
if ctx.config().yes(Step::System) {
aur_update.arg("--noconfirm");
cmd.arg("--noconfirm");
}
cmd.status_checked()?;
aur_update.status_checked()?;
let mut cmd = ctx.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 {
println!("Aura requires sudo installed to work with AUR packages")
}
let sudo = ctx.require_sudo()?;
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");
let mut cmd = sudo.execute(ctx, &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 = sudo.execute(ctx, &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()?;
}
pacman_update.status_checked()?;
Ok(())
}
@@ -323,16 +340,16 @@ pub fn get_arch_package_manager(ctx: &ExecutionContext) -> Option<Box<dyn ArchPa
.or_else(|| Trizen::get().map(box_package_manager))
.or_else(|| Pikaur::get().map(box_package_manager))
.or_else(|| Pamac::get().map(box_package_manager))
.or_else(|| Pacman::get(ctx).map(box_package_manager))
.or_else(|| Aura::get(ctx).map(box_package_manager)),
.or_else(|| Pacman::get().map(box_package_manager))
.or_else(|| Aura::get().map(box_package_manager)),
config::ArchPackageManager::GarudaUpdate => GarudaUpdate::get().map(box_package_manager),
config::ArchPackageManager::Trizen => Trizen::get().map(box_package_manager),
config::ArchPackageManager::Paru => YayParu::get("paru", &pacman).map(box_package_manager),
config::ArchPackageManager::Yay => YayParu::get("yay", &pacman).map(box_package_manager),
config::ArchPackageManager::Pacman => Pacman::get(ctx).map(box_package_manager),
config::ArchPackageManager::Pacman => Pacman::get().map(box_package_manager),
config::ArchPackageManager::Pikaur => Pikaur::get().map(box_package_manager),
config::ArchPackageManager::Pamac => Pamac::get().map(box_package_manager),
config::ArchPackageManager::Aura => Aura::get(ctx).map(box_package_manager),
config::ArchPackageManager::Aura => Aura::get().map(box_package_manager),
}
}
@@ -355,7 +372,7 @@ pub fn show_pacnew() {
.peekable();
if iter.peek().is_some() {
println!("\nPacman backup configuration files found:");
println!("\n{}", t!("Pacman backup configuration files found:"));
for entry in iter {
println!("{}", entry.path().display());

View File

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

View File

@@ -1,27 +1,25 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::step::Step;
use crate::terminal::print_separator;
use crate::utils::{require_option, REQUIRE_SUDO};
use crate::Step;
use color_eyre::eyre::Result;
use std::process::Command;
use color_eyre::Result;
use rust_i18n::t;
pub fn upgrade_freebsd(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("FreeBSD Update");
ctx.run_type()
.execute(sudo)
.args(["/usr/sbin/freebsd-update", "fetch", "install"])
print_separator(t!("FreeBSD Update"));
let sudo = ctx.require_sudo()?;
sudo.execute(ctx, "/usr/sbin/freebsd-update")?
.args(["fetch", "install"])
.status_checked()
}
pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("FreeBSD Packages");
print_separator(t!("FreeBSD Packages"));
let mut command = ctx.run_type().execute(sudo);
command.args(["/usr/sbin/pkg", "upgrade"]);
let sudo = ctx.require_sudo()?;
let mut command = sudo.execute(ctx, "/usr/sbin/pkg")?;
command.arg("upgrade");
if ctx.config().yes(Step::System) {
command.arg("-y");
}
@@ -29,12 +27,10 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
}
pub fn audit_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator(t!("FreeBSD Audit"));
print_separator("FreeBSD Audit");
Command::new(sudo)
.args(["/usr/sbin/pkg", "audit", "-Fr"])
.status_checked()?;
Ok(())
let sudo = ctx.require_sudo()?;
sudo.execute(ctx, "/usr/sbin/pkg")?
.args(["audit", "-Fr"])
.status_checked()
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,28 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::step::Step;
use crate::terminal::{print_separator, prompt_yesno};
use crate::utils::{require_option, REQUIRE_SUDO};
use crate::{utils::require, Step};
use crate::utils::require;
use color_eyre::eyre::Result;
use rust_i18n::t;
use std::collections::HashSet;
use std::fs;
use std::process::Command;
use tracing::debug;
pub fn run_macports(ctx: &ExecutionContext) -> Result<()> {
require("port")?;
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
let port = require("port")?;
print_separator("MacPorts");
ctx.run_type()
.execute(sudo)
.args(["port", "selfupdate"])
.status_checked()?;
ctx.run_type()
.execute(sudo)
.args(["port", "-u", "upgrade", "outdated"])
let sudo = ctx.require_sudo()?;
sudo.execute(ctx, &port)?.arg("selfupdate").status_checked()?;
sudo.execute(ctx, &port)?
.args(["-u", "upgrade", "outdated"])
.status_checked()?;
if ctx.config().cleanup() {
ctx.run_type()
.execute(sudo)
.args(["port", "-N", "reclaim"])
.status_checked()?;
sudo.execute(ctx, &port)?.args(["-N", "reclaim"]).status_checked()?;
}
Ok(())
@@ -34,30 +30,30 @@ pub fn run_macports(ctx: &ExecutionContext) -> Result<()> {
pub fn run_mas(ctx: &ExecutionContext) -> Result<()> {
let mas = require("mas")?;
print_separator("macOS App Store");
print_separator(t!("macOS App Store"));
ctx.run_type().execute(mas).arg("upgrade").status_checked()
ctx.execute(mas).arg("upgrade").status_checked()
}
pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> {
print_separator("macOS system update");
print_separator(t!("macOS system update"));
let should_ask = !(ctx.config().yes(Step::System) || ctx.config().dry_run());
let should_ask = !(ctx.config().yes(Step::System) || ctx.run_type().dry());
if should_ask {
println!("Finding available software");
println!("{}", t!("Finding available software"));
if system_update_available()? {
let answer = prompt_yesno("A system update is available. Do you wish to install it?")?;
let answer = prompt_yesno(t!("A system update is available. Do you wish to install it?").as_ref())?;
if !answer {
return Ok(());
}
println!();
} else {
println!("No new software available.");
println!("{}", t!("No new software available."));
return Ok(());
}
}
let mut command = ctx.run_type().execute("softwareupdate");
let mut command = ctx.execute("softwareupdate");
command.args(["--install", "--all"]);
if should_ask {
@@ -86,7 +82,7 @@ pub fn run_sparkle(ctx: &ExecutionContext) -> Result<()> {
.arg(application.path())
.output_checked_utf8();
if probe.is_ok() {
let mut command = ctx.run_type().execute(&sparkle);
let mut command = ctx.execute(&sparkle);
command.args(["bundle", "--check-immediately", "--application"]);
command.arg(application.path());
command.status_checked()?;
@@ -99,14 +95,9 @@ 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 should_ask = !(ctx.config().yes(Step::Xcodes) || ctx.run_type().dry());
let releases = ctx
.run_type()
.execute(&xcodes)
.args(["update"])
.output_checked_utf8()?
.stdout;
let releases = ctx.execute(&xcodes).args(["update"]).output_checked_utf8()?.stdout;
let releases_installed: Vec<String> = releases
.lines()
@@ -115,7 +106,7 @@ pub fn update_xcodes(ctx: &ExecutionContext) -> Result<()> {
.collect();
if releases_installed.is_empty() {
println!("No Xcode releases installed.");
println!("{}", t!("No Xcode releases installed."));
return Ok(());
}
@@ -163,12 +154,7 @@ pub fn update_xcodes(ctx: &ExecutionContext) -> Result<()> {
process_xcodes_releases(releases_regular, should_ask, ctx)?;
}
let releases_new = ctx
.run_type()
.execute(&xcodes)
.args(["list"])
.output_checked_utf8()?
.stdout;
let releases_new = ctx.execute(&xcodes).args(["list"]).output_checked_utf8()?.stdout;
let releases_gm_new_installed: HashSet<_> = releases_new
.lines()
@@ -194,14 +180,14 @@ pub fn update_xcodes(ctx: &ExecutionContext) -> Result<()> {
releases_regular_new_installed,
] {
if should_ask && releases_new_installed.len() == 2 {
let answer_uninstall = prompt_yesno("Would you like to move the former Xcode release to the trash?")?;
let answer_uninstall =
prompt_yesno(t!("Would you like to move the former Xcode release to the trash?").as_ref())?;
if answer_uninstall {
let _ = ctx
.run_type()
.execute(&xcodes)
.args([
"uninstall",
releases_new_installed.iter().next().cloned().unwrap_or_default(),
releases_new_installed.iter().next().copied().unwrap_or_default(),
])
.status_checked();
}
@@ -214,21 +200,16 @@ pub fn update_xcodes(ctx: &ExecutionContext) -> Result<()> {
pub fn process_xcodes_releases(releases_filtered: Vec<String>, should_ask: bool, ctx: &ExecutionContext) -> Result<()> {
let xcodes = require("xcodes")?;
if releases_filtered
.last()
.map(|s| !s.contains("(Installed)"))
.unwrap_or(true)
&& !releases_filtered.is_empty()
{
if releases_filtered.last().is_none_or(|s| !s.contains("(Installed)")) && !releases_filtered.is_empty() {
println!(
"New Xcode release detected: {}",
"{} {}",
t!("New Xcode release detected:"),
releases_filtered.last().cloned().unwrap_or_default()
);
if should_ask {
let answer_install = prompt_yesno("Would you like to install it?")?;
let answer_install = prompt_yesno(t!("Would you like to install it?").as_ref())?;
if answer_install {
let _ = ctx
.run_type()
.execute(xcodes)
.args(["install", &releases_filtered.last().cloned().unwrap_or_default()])
.status_checked();

View File

@@ -14,7 +14,7 @@ pub mod macos;
pub mod openbsd;
#[cfg(unix)]
pub mod unix;
#[cfg(target_os = "windows")]
#[cfg(windows)]
pub mod windows;
#[cfg(windows)]

View File

@@ -1,33 +1,49 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::terminal::print_separator;
use crate::utils::{require_option, REQUIRE_SUDO};
use color_eyre::eyre::Result;
use rust_i18n::t;
use std::fs;
use tracing::debug;
fn is_openbsd_current() -> Result<bool> {
let motd_content = fs::read_to_string("/etc/motd")?;
let is_current = ["-current", "-beta"].iter().any(|&s| motd_content.contains(s));
debug!("OpenBSD is -current/-beta: {is_current}");
Ok(is_current)
}
pub fn upgrade_openbsd(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("OpenBSD Update");
ctx.run_type()
.execute(sudo)
.args(["/usr/sbin/sysupgrade", "-n"])
.status_checked()
print_separator(t!("OpenBSD Update"));
let sudo = ctx.require_sudo()?;
let is_current = is_openbsd_current()?;
if is_current {
sudo.execute(ctx, "/usr/sbin/sysupgrade")?.arg("-sn").status_checked()
} else {
sudo.execute(ctx, "/usr/sbin/syspatch")?.status_checked()
}
}
pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("OpenBSD Packages");
print_separator(t!("OpenBSD Packages"));
let sudo = ctx.require_sudo()?;
let is_current = is_openbsd_current()?;
if ctx.config().cleanup() {
ctx.run_type()
.execute(sudo)
.args(["/usr/sbin/pkg_delete", "-ac"])
.status_checked()?;
sudo.execute(ctx, "/usr/sbin/pkg_delete")?.arg("-ac").status_checked()?;
}
ctx.run_type()
.execute(sudo)
.args(["/usr/sbin/pkg_add", "-u"])
.status_checked()?;
Ok(())
let mut command = sudo.execute(ctx, "/usr/sbin/pkg_add")?;
command.arg("-u");
if is_current {
command.arg("-Dsnap");
}
command.status_checked()
}

View File

@@ -6,4 +6,4 @@ VERSION_ID="2"
PRETTY_NAME="Amazon Linux 2"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2"
HOME_URL="https://amazonlinux.com/"
HOME_URL="https://amazonlinux.com/"

View File

@@ -0,0 +1,10 @@
PRETTY_NAME="AOSC OS (12.2.2)"
NAME="AOSC OS"
VERSION_ID="12.2.2"
VERSION="12.2.2 (localhost)"
BUILD_ID="20250916"
ID=aosc
ANSI_COLOR="1;36"
HOME_URL="https://aosc.io/"
SUPPORT_URL="https://github.com/AOSC-Dev/aosc-os-abbs"
BUG_REPORT_URL="https://github.com/AOSC-Dev/aosc-os-abbs/issues"

View File

@@ -8,4 +8,4 @@ HOME_URL="https://www.archlinux32.org/"
DOCUMENTATION_URL="https://wiki.archlinux.org/"
SUPPORT_URL="https://bbs.archlinux32.org/"
BUG_REPORT_URL="https://bugs.archlinux32.org/"
LOGO=archlinux
LOGO=archlinux

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,4 +13,3 @@ CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"

View File

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

View File

@@ -20,4 +20,4 @@ REDHAT_SUPPORT_PRODUCT_VERSION=40
SUPPORT_END=2025-05-13
VARIANT="Sway Atomic"
VARIANT_ID=sway-atomic
OSTREE_VERSION='40.20240426.0'
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

@@ -7,4 +7,4 @@ HOME_URL="https://www.garudalinux.in/"
DOCUMENTATION_URL="https://wiki.archlinux.org/"
SUPPORT_URL="https://forum.garudalinux.in/"
BUG_REPORT_URL="https://gitlab.com/groups/garuda-linux/"
LOGO=garudalinux
LOGO=garudalinux

View File

@@ -4,4 +4,3 @@ PRETTY_NAME="Manjaro ARM"
ANSI_COLOR="1;32"
HOME_URL="https://www.manjaro.org/"
SUPPORT_URL="https://forum.manjaro.org/c/manjaro-arm/"

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="Origami Linux"
VERSION="43.20251117.0 (COSMIC Atomic)"
RELEASE_TYPE="stable"
ID="origami-linux"
VERSION_ID="43"
VERSION_CODENAME=""
PRETTY_NAME="Origami 折り紙"
ANSI_COLOR="0;38;2;60;110;180"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:fedoraproject:fedora:43"
DEFAULT_HOSTNAME="origami"
HOME_URL="https://origami.wf/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f43/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://gitlab.com/groups/origami-linux/-/issues"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION="43"
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION="43"
SUPPORT_END="2026-12-02"
VARIANT="COSMIC Atomic"
VARIANT_ID="cosmic-atomic"
OSTREE_VERSION="43.20251117.0"

View File

@@ -0,0 +1,23 @@
NAME="Origami Linux Nvidia"
VERSION="43.20251117.0 (COSMIC Atomic)"
RELEASE_TYPE="stable"
ID="origami-linux-nvidia"
VERSION_ID="43"
VERSION_CODENAME=""
PRETTY_NAME="Origami 折り紙"
ANSI_COLOR="0;38;2;60;110;180"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:fedoraproject:fedora:43"
DEFAULT_HOSTNAME="origami"
HOME_URL="https://origami.wf/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f43/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://gitlab.com/groups/origami-linux/-/issues"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION="43"
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION="43"
SUPPORT_END="2026-12-02"
VARIANT="COSMIC Atomic"
VARIANT_ID="cosmic-atomic"
OSTREE_VERSION="43.20251117.0"

View File

@@ -0,0 +1,23 @@
NAME="Origami Linux Test"
VERSION="43.20251117.0 (COSMIC Atomic)"
RELEASE_TYPE="stable"
ID="origami-linux-test"
VERSION_ID="43"
VERSION_CODENAME=""
PRETTY_NAME="Origami 折り紙"
ANSI_COLOR="0;38;2;60;110;180"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:fedoraproject:fedora:43"
DEFAULT_HOSTNAME="origami"
HOME_URL="https://origami.wf/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f43/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://gitlab.com/groups/origami-linux/-/issues"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION="43"
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION="43"
SUPPORT_END="2026-12-02"
VARIANT="COSMIC Atomic"
VARIANT_ID="cosmic-atomic"
OSTREE_VERSION="43.20251117.0"

View File

@@ -1,30 +1,39 @@
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use color_eyre::eyre::{OptionExt, eyre};
use etcetera::BaseStrategy;
use ini::Ini;
#[cfg(target_os = "linux")]
use nix::unistd::Uid;
use regex::Regex;
use rust_i18n::t;
use semver::Version;
use std::env::home_dir;
use std::ffi::OsStr;
use std::fs;
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::path::Component;
use std::path::PathBuf;
use std::process::Command;
use std::sync::LazyLock;
use std::{env::var, path::Path};
use std::{fs, io};
use tracing::{debug, warn};
use crate::XDG_DIRS;
use crate::command::CommandExt;
use crate::{Step, HOME_DIR};
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use home;
use ini::Ini;
use tracing::debug;
use crate::sudo::SudoExecuteOpts;
use crate::{HOME_DIR, output_changed_message};
#[cfg(target_os = "linux")]
use super::linux::Distribution;
use crate::error::SkipStep;
use crate::error::{SkipStep, StepFailed};
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::step::Step;
use crate::terminal::print_separator;
use crate::utils::{require, require_option, PathExt, REQUIRE_SUDO};
use crate::utils::{PathExt, require};
#[cfg(any(target_os = "linux", target_os = "macos"))]
const INTEL_BREW: &str = "/usr/local/bin/brew";
@@ -69,19 +78,40 @@ impl BrewVariant {
}
}
fn execute(self, run_type: RunType) -> Executor {
/// Execute an "internal" brew command, i.e. one that should always be run
/// even when dry-running. Basically just a wrapper around [`Command::new`]
/// that uses `arch` to run using the correct architecture if needed.
fn execute_internal(self) -> Command {
match self {
BrewVariant::MacIntel if cfg!(target_arch = "aarch64") => {
let mut command = run_type.execute("arch");
let mut command = Command::new("arch");
command.arg("-x86_64").arg(self.binary_name());
command
}
BrewVariant::MacArm if cfg!(target_arch = "x86_64") => {
let mut command = run_type.execute("arch");
let mut command = Command::new("arch");
command.arg("-arm64e").arg(self.binary_name());
command
}
_ => run_type.execute(self.binary_name()),
_ => Command::new(self.binary_name()),
}
}
/// Execute a brew command. Uses `arch` to run using the correct
/// architecture on macOS if needed.
fn execute(self, ctx: &ExecutionContext) -> Executor {
match self {
BrewVariant::MacIntel if cfg!(target_arch = "aarch64") => {
let mut command = ctx.execute("arch");
command.arg("-x86_64").arg(self.binary_name());
command
}
BrewVariant::MacArm if cfg!(target_arch = "x86_64") => {
let mut command = ctx.execute("arch");
command.arg("-arm64e").arg(self.binary_name());
command
}
_ => ctx.execute(self.binary_name()),
}
}
@@ -98,24 +128,23 @@ pub fn run_fisher(ctx: &ExecutionContext) -> Result<()> {
.args(["-c", "type -t fisher"])
.output_checked_utf8()
.map(|_| ())
.map_err(|_| SkipStep("`fisher` is not defined in `fish`".to_owned()))?;
.map_err(|_| SkipStep(t!("`fisher` is not defined in `fish`").to_string()))?;
Command::new(&fish)
.args(["-c", "echo \"$__fish_config_dir/fish_plugins\""])
.output_checked_utf8()
.and_then(|output| Path::new(&output.stdout.trim()).require().map(|_| ()))
.map_err(|err| SkipStep(format!("`fish_plugins` path doesn't exist: {err}")))?;
.map_err(|err| SkipStep(t!("`fish_plugins` path doesn't exist: {err}", err = err).to_string()))?;
Command::new(&fish)
.args(["-c", "fish_update_completions"])
.output_checked_utf8()
.map(|_| ())
.map_err(|_| SkipStep("`fish_update_completions` is not available".to_owned()))?;
.map_err(|_| SkipStep(t!("`fish_update_completions` is not available").to_string()))?;
print_separator("Fisher");
let version_str = ctx
.run_type()
.execute(&fish)
.args(["-c", "fisher --version"])
.output_checked_utf8()?
@@ -124,13 +153,10 @@ pub fn run_fisher(ctx: &ExecutionContext) -> Result<()> {
if version_str.starts_with("fisher version 3.") {
// v3 - see https://github.com/topgrade-rs/topgrade/pull/37#issuecomment-1283844506
ctx.run_type().execute(&fish).args(["-c", "fisher"]).status_checked()
ctx.execute(&fish).args(["-c", "fisher"]).status_checked()
} else {
// v4
ctx.run_type()
.execute(&fish)
.args(["-c", "fisher update"])
.status_checked()
ctx.execute(&fish).args(["-c", "fisher update"]).status_checked()
}
}
@@ -139,8 +165,7 @@ pub fn run_bashit(ctx: &ExecutionContext) -> Result<()> {
print_separator("Bash-it");
ctx.run_type()
.execute("bash")
ctx.execute("bash")
.args(["-lic", &format!("bash-it update {}", ctx.config().bashit_branch())])
.status_checked()
}
@@ -163,7 +188,7 @@ pub fn run_oh_my_bash(ctx: &ExecutionContext) -> Result<()> {
let mut update_script = oh_my_bash;
update_script.push_str("/tools/upgrade.sh");
ctx.run_type().execute("bash").arg(update_script).status_checked()
ctx.execute("bash").arg(update_script).status_checked()
}
pub fn run_oh_my_fish(ctx: &ExecutionContext) -> Result<()> {
@@ -172,24 +197,25 @@ pub fn run_oh_my_fish(ctx: &ExecutionContext) -> Result<()> {
print_separator("oh-my-fish");
ctx.run_type().execute(fish).args(["-c", "omf update"]).status_checked()
ctx.execute(fish).args(["-c", "omf update"]).status_checked()
}
pub fn run_pkgin(ctx: &ExecutionContext) -> Result<()> {
let pkgin = require("pkgin")?;
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
print_separator("Pkgin");
let mut command = ctx.run_type().execute(sudo);
command.arg(&pkgin).arg("update");
let sudo = ctx.require_sudo()?;
let mut command = sudo.execute(ctx, &pkgin)?;
command.arg("update");
if ctx.config().yes(Step::Pkgin) {
command.arg("-y");
}
command.status_checked()?;
let mut command = ctx.run_type().execute(sudo);
command.arg(&pkgin).arg("upgrade");
let mut command = sudo.execute(ctx, &pkgin)?;
command.arg("upgrade");
if ctx.config().yes(Step::Pkgin) {
command.arg("-y");
}
@@ -204,10 +230,7 @@ pub fn run_fish_plug(ctx: &ExecutionContext) -> Result<()> {
print_separator("fish-plug");
ctx.run_type()
.execute(fish)
.args(["-c", "plug update"])
.status_checked()
ctx.execute(fish).args(["-c", "plug update"]).status_checked()
}
/// Upgrades `fundle` and `fundle` plugins.
@@ -221,8 +244,7 @@ pub fn run_fundle(ctx: &ExecutionContext) -> Result<()> {
print_separator("fundle");
ctx.run_type()
.execute(fish)
ctx.execute(fish)
.args(["-c", "fundle self-update && fundle update"])
.status_checked()
}
@@ -230,9 +252,9 @@ pub fn run_fundle(ctx: &ExecutionContext) -> Result<()> {
#[cfg(not(any(target_os = "android", target_os = "macos")))]
pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
let gdbus = require("gdbus")?;
require_option(
crate::utils::require_option(
var("XDG_CURRENT_DESKTOP").ok().filter(|p| p.contains("GNOME")),
"Desktop doest not appear to be gnome".to_string(),
t!("Desktop does not appear to be GNOME").to_string(),
)?;
let output = Command::new("gdbus")
.args([
@@ -247,15 +269,14 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
])
.output_checked_utf8()?;
debug!("Checking for gnome extensions: {}", output);
debug!("Checking for GNOME extensions: {}", output);
if !output.stdout.contains("org.gnome.Shell.Extensions") {
return Err(SkipStep(String::from("Gnome shell extensions are unregistered in DBus")).into());
return Err(SkipStep(t!("GNOME shell extensions are unregistered in DBus").to_string()).into());
}
print_separator("Gnome Shell extensions");
print_separator(t!("GNOME Shell extensions"));
ctx.run_type()
.execute(gdbus)
ctx.execute(gdbus)
.args([
"call",
"--session",
@@ -269,6 +290,23 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> {
.status_checked()
}
#[cfg(target_os = "linux")]
pub fn brew_linux_sudo_uid() -> Option<u32> {
let linuxbrew_directory = "/home/linuxbrew/.linuxbrew";
if let Ok(metadata) = std::fs::metadata(linuxbrew_directory) {
let owner_id = metadata.uid();
let current_id = Uid::effective();
// print debug these two values
debug!("linuxbrew_directory owner_id: {}, current_id: {}", owner_id, current_id);
return if owner_id == current_id.as_raw() {
None // no need for sudo if linuxbrew is owned by the current user
} else {
Some(owner_id) // otherwise use sudo to run brew as the owner
};
}
None
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()> {
#[allow(unused_variables)]
@@ -277,16 +315,36 @@ pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<
#[cfg(target_os = "macos")]
{
if variant.is_path() && !BrewVariant::is_macos_custom(binary_name) {
return Err(SkipStep("Not a custom brew for macOS".to_string()).into());
return Err(SkipStep(t!("Not a custom brew for macOS").to_string()).into());
}
}
#[cfg(target_os = "linux")]
{
let sudo_uid = brew_linux_sudo_uid();
// if brew is owned by another user, execute "sudo -Hu <uid> brew update"
if let Some(user_id) = sudo_uid {
let uid = nix::unistd::Uid::from_raw(user_id);
let user = nix::unistd::User::from_uid(uid)
.expect("failed to call getpwuid()")
.expect("this user should exist");
let sudo_as_user = t!("sudo as user '{user}'", user = user.name);
print_separator(format!("{} ({})", variant.step_title(), sudo_as_user));
let sudo = ctx.require_sudo()?;
sudo.execute_opts(ctx, &binary_name, SudoExecuteOpts::new().set_home().user(&user.name))?
.current_dir("/tmp") // brew needs a writable current directory
.arg("update")
.status_checked()?;
return Ok(());
}
}
print_separator(variant.step_title());
let run_type = ctx.run_type();
variant.execute(run_type).arg("update").status_checked()?;
variant.execute(ctx).arg("update").status_checked()?;
let mut command = variant.execute(run_type);
let mut command = variant.execute(ctx);
command.args(["upgrade", "--formula"]);
if ctx.config().brew_fetch_head() {
@@ -296,27 +354,62 @@ pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<
command.status_checked()?;
if ctx.config().cleanup() {
variant.execute(run_type).arg("cleanup").status_checked()?;
variant.execute(ctx).arg("cleanup").status_checked()?;
}
if ctx.config().brew_autoremove() {
variant.execute(run_type).arg("autoremove").status_checked()?;
variant.execute(ctx).arg("autoremove").status_checked()?;
}
Ok(())
}
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()> {
let binary_name = require(variant.binary_name())?;
#[cfg(target_os = "macos")]
if variant.is_path() && !BrewVariant::is_macos_custom(binary_name) {
return Err(SkipStep("Not a custom brew for macOS".to_string()).into());
return Err(SkipStep(t!("Not a custom brew for macOS").to_string()).into());
}
#[cfg(target_os = "linux")]
{
// Homebrew cask support was added in version 4.5.0
let version_output = Command::new(&binary_name).arg("--version").output_checked_utf8()?;
let version_line = version_output
.stdout
.lines()
.next()
.ok_or_else(|| eyre!(output_changed_message!("brew --version", "no output lines")))?;
let version_str = version_line.split_whitespace().nth(1).ok_or_else(|| {
eyre!(output_changed_message!(
"brew --version",
"Expected version after 'Homebrew'"
))
})?;
let version = Version::parse(version_str)
.wrap_err_with(|| output_changed_message!("brew --version", "Invalid version"))?;
if version < Version::new(4, 5, 0) {
return Err(SkipStep(
t!(
"Homebrew cask support on Linux requires Homebrew 4.5.0 or later (found {version})",
version = version
)
.to_string(),
)
.into());
}
}
print_separator(format!("{} - Cask", variant.step_title()));
let run_type = ctx.run_type();
let cask_upgrade_exists = variant
.execute(RunType::Wet)
.execute_internal()
.args(["--repository", "buo/cask-upgrade"])
.output_checked_utf8()
.map(|p| Path::new(p.stdout.trim()).exists())?;
@@ -336,12 +429,15 @@ pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()>
if ctx.config().brew_greedy_latest() {
brew_args.push("--greedy-latest");
}
if ctx.config().brew_greedy_auto_updates() {
brew_args.push("--greedy-auto-updates");
}
}
variant.execute(run_type).args(&brew_args).status_checked()?;
variant.execute(ctx).args(&brew_args).status_checked()?;
if ctx.config().cleanup() {
variant.execute(run_type).arg("cleanup").status_checked()?;
variant.execute(ctx).arg("cleanup").status_checked()?;
}
Ok(())
@@ -350,28 +446,93 @@ pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()>
pub fn run_guix(ctx: &ExecutionContext) -> Result<()> {
let guix = require("guix")?;
let run_type = ctx.run_type();
let output = Command::new(&guix).arg("pull").output_checked_utf8();
debug!("guix pull output: {:?}", output);
let should_upgrade = output.is_ok();
debug!("Can Upgrade Guix: {:?}", should_upgrade);
print_separator("Guix");
if should_upgrade {
return run_type.execute(&guix).args(["package", "-u"]).status_checked();
ctx.execute(&guix).arg("pull").status_checked()?;
ctx.execute(&guix).args(["package", "-u"]).status_checked()?;
Ok(())
}
struct NixVersion {
version_string: String,
}
impl NixVersion {
fn new(ctx: &ExecutionContext, nix: &Path) -> Result<Self> {
let version_output = ctx.execute(nix).arg("--version").output_checked_utf8()?;
debug!(
output=%version_output,
"`nix --version` output"
);
let version_string = version_output
.stdout
.lines()
.next()
.ok_or_else(|| eyre!("`nix --version` output is empty"))?
.to_string();
if version_string.is_empty() {
return Err(eyre!("`nix --version` output was empty"));
}
Ok(Self { version_string })
}
fn version(&self) -> Result<Version> {
static NIX_VERSION_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^nix \([^)]*\) ([0-9.]+)").expect("Nix version regex always compiles"));
let captures = NIX_VERSION_REGEX
.captures(&self.version_string)
.ok_or_else(|| eyre!(output_changed_message!("nix --version", "regex did not match")))?;
let raw_version = &captures[1];
debug!("Raw Nix version: {raw_version}");
// Nix 2.29.0 outputs "2.29" instead of "2.29.0", so we need to add that if necessary.
let corrected_raw_version = if raw_version.chars().filter(|&c| c == '.').count() == 1 {
&format!("{raw_version}.0")
} else {
raw_version
};
debug!("Corrected raw Nix version: {corrected_raw_version}");
let version = Version::parse(corrected_raw_version)
.wrap_err_with(|| output_changed_message!("nix --version", "Invalid version"))?;
debug!("Nix version: {:?}", version);
Ok(version)
}
fn is_lix(&self) -> bool {
let is_lix = self.version_string.contains("Lix");
debug!(?is_lix);
is_lix
}
fn is_determinate_nix(&self) -> bool {
let is_determinate_nix = self.version_string.contains("Determinate Nix");
debug!(?is_determinate_nix);
is_determinate_nix
}
Err(SkipStep(String::from("Guix Pull Failed, Skipping")).into())
}
pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
let nix = require("nix")?;
let nix_channel = require("nix-channel")?;
let nix_env = require("nix-env")?;
// TODO: Is None possible here?
let profile_path = match home::home_dir() {
Some(home) => Path::new(&home).join(".nix-profile"),
// TODO: Is None possible here? Should we use HOME_DIR instead?
let profile_path = match home_dir() {
Some(home) => XDG_DIRS
.state_dir()
.map(|d| d.join("nix/profile"))
.filter(|p| p.exists())
.unwrap_or(Path::new(&home).join(".nix-profile")),
None => Path::new("/nix/var/nix/profiles/per-user/default").into(),
};
debug!("nix profile: {:?}", profile_path);
@@ -382,27 +543,34 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
#[cfg(target_os = "macos")]
{
if require("darwin-rebuild").is_ok() {
return Err(SkipStep(String::from(
"Nix-darwin on macOS must be upgraded via darwin-rebuild switch",
))
.into());
return Err(
SkipStep(t!("Nix-darwin on macOS must be upgraded via darwin-rebuild switch").to_string()).into(),
);
}
}
let run_type = ctx.run_type();
run_type.execute(nix_channel).arg("--update").status_checked()?;
ctx.execute(nix_channel).arg("--update").status_checked()?;
let nix_version = NixVersion::new(ctx, &nix)?;
// Nix since 2.21.0 uses `--all --impure` rather than `.*` to upgrade all packages.
// Lix is based on Nix 2.18, so it doesn't!
let packages = if nix_version.version()? >= Version::new(2, 21, 0) && !nix_version.is_lix() {
vec!["--all", "--impure"]
} else {
vec![".*"]
};
if Path::new(&manifest_json_path).exists() {
run_type
.execute(nix)
ctx.execute(nix)
.args(nix_args())
.arg("profile")
.arg("upgrade")
.arg(".*")
.args(&packages)
.arg("--verbose")
.status_checked()
} else {
let mut command = run_type.execute(nix_env);
let mut command = ctx.execute(nix_env);
command.arg("--upgrade");
if let Some(args) = ctx.config().nix_env_arguments() {
command.args(args.split_whitespace());
@@ -427,36 +595,48 @@ pub fn run_nix_self_upgrade(ctx: &ExecutionContext) -> Result<()> {
}
if !should_self_upgrade {
return Err(SkipStep(String::from(
"`nix upgrade-nix` can only be used on macOS or non-NixOS Linux",
))
.into());
return Err(SkipStep(t!("`nix upgrade-nix` can only be used on macOS or non-NixOS Linux").to_string()).into());
}
if nix_profile_dir(&nix)?.is_none() {
return Err(SkipStep(String::from(
"`nix upgrade-nix` cannot be run when Nix is installed in a profile",
))
.into());
return Err(
SkipStep(t!("`nix upgrade-nix` cannot be run when Nix is installed in a profile").to_string()).into(),
);
}
print_separator("Nix (self-upgrade)");
print_separator(t!("Nix (self-upgrade)"));
let nix_version = NixVersion::new(ctx, &nix)?;
if nix_version.is_determinate_nix() {
let nixd = require("determinate-nixd");
let nixd = match nixd {
Err(_) => {
println!("Found Determinate Nix, but could not find determinate-nixd");
return Err(StepFailed.into());
}
Ok(nixd) => nixd,
};
let sudo = ctx.require_sudo()?;
return sudo
.execute_opts(ctx, nixd, SudoExecuteOpts::new().login_shell())?
.arg("upgrade")
.status_checked();
}
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)?
let sudo = ctx.require_sudo()?;
sudo.execute_opts(ctx, &nix, SudoExecuteOpts::new().login_shell())?
.args(nix_args)
.arg("upgrade-nix")
.status_checked()
} else {
ctx.run_type()
.execute(&nix)
.args(nix_args)
.arg("upgrade-nix")
.status_checked()
ctx.execute(&nix).args(nix_args).arg("upgrade-nix").status_checked()
}
}
@@ -505,7 +685,6 @@ fn nix_profile_dir(nix: &Path) -> Result<Option<PathBuf>> {
}
debug!("Found Nix profile {profile_dir:?}");
let user_env = profile_dir
.canonicalize()
.wrap_err_with(|| format!("Failed to canonicalize {profile_dir:?}"))?;
@@ -514,8 +693,7 @@ fn nix_profile_dir(nix: &Path) -> Result<Option<PathBuf>> {
if user_env
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.ends_with("user-environment"))
.unwrap_or(false)
.is_some_and(|name| name.ends_with("user-environment"))
{
Some(profile_dir)
} else {
@@ -524,6 +702,84 @@ fn nix_profile_dir(nix: &Path) -> Result<Option<PathBuf>> {
)
}
/// Returns a directory from an environment variable, if and only if it is a directory which
/// contains a flake.nix
fn flake_dir(var: &'static str) -> Option<PathBuf> {
std::env::var_os(var)
.map(PathBuf::from)
.take_if(|x| std::fs::exists(x.join("flake.nix")).is_ok_and(|x| x))
}
/// Update NixOS and home-manager through a flake using `nh`
///
/// See: https://github.com/viperML/nh
pub fn run_nix_helper(ctx: &ExecutionContext) -> Result<()> {
require("nix")?;
let nix_helper = require("nh")?;
let fallback_flake_path = flake_dir("NH_FLAKE");
let darwin_flake_path = flake_dir("NH_DARWIN_FLAKE");
let home_flake_path = flake_dir("NH_HOME_FLAKE");
let nixos_flake_path = flake_dir("NH_OS_FLAKE");
let all_flake_paths: Vec<_> = [
fallback_flake_path.as_ref(),
darwin_flake_path.as_ref(),
home_flake_path.as_ref(),
nixos_flake_path.as_ref(),
]
.into_iter()
.flatten()
.collect();
// if none of the paths exist AND contain a `flake.nix`, skip
if all_flake_paths.is_empty() {
if flake_dir("FLAKE").is_some() {
warn!(
"{}",
t!("You have a flake inside of $FLAKE. This is deprecated for nh.")
);
}
return Err(SkipStep(t!("nh cannot find any configured flakes").into()).into());
}
let nh_switch = |ty: &'static str| -> Result<()> {
print_separator(format!("nh {ty}"));
let mut cmd = ctx.execute(&nix_helper);
cmd.arg(ty);
cmd.arg("switch");
cmd.arg("-u");
if !ctx.config().yes(Step::NixHelper) {
cmd.arg("--ask");
}
cmd.status_checked()?;
Ok(())
};
// We assume that if the user has set these variables, we can throw an error if nh cannot find
// a flake there. So we do not anymore perform an eval check to find out whether we should skip
// or not.
#[cfg(target_os = "macos")]
if darwin_flake_path.is_some() || fallback_flake_path.is_some() {
nh_switch("darwin")?;
}
if home_flake_path.is_some() || fallback_flake_path.is_some() {
nh_switch("home")?;
}
#[cfg(target_os = "linux")]
if matches!(Distribution::detect(), Ok(Distribution::NixOS))
&& (nixos_flake_path.is_some() || fallback_flake_path.is_some())
{
nh_switch("os")?;
}
Ok(())
}
fn nix_args() -> [&'static str; 2] {
["--extra-experimental-features", "nix-command"]
}
@@ -533,22 +789,47 @@ pub fn run_yadm(ctx: &ExecutionContext) -> Result<()> {
print_separator("yadm");
ctx.run_type().execute(yadm).arg("pull").status_checked()
ctx.execute(yadm).arg("pull").status_checked()
}
pub fn run_asdf(ctx: &ExecutionContext) -> Result<()> {
let asdf = require("asdf")?;
print_separator("asdf");
ctx.run_type()
.execute(&asdf)
.arg("update")
.status_checked_with_codes(&[42])?;
ctx.run_type()
.execute(&asdf)
.args(["plugin", "update", "--all"])
.status_checked()
// asdf (>= 0.15.0) won't support the self-update command
//
// https://github.com/topgrade-rs/topgrade/issues/1007
let version_output = Command::new(&asdf).arg("version").output_checked_utf8()?;
// Example output
//
// ```
// $ asdf version
// v0.15.0-31e8c93
//
// ```
// ```
// $ asdf version
// v0.16.7
// ```
// ```
// $ asdf version
// 0.18.0 (revision unknown)
// ```
let version_stdout = version_output.stdout.trim();
// trim the starting 'v'
let mut remaining = version_stdout.trim_start_matches('v');
// remove the hash or revision part if present
if let Some(idx) = remaining.find(['-', ' ']) {
remaining = &remaining[..idx];
}
let version =
Version::parse(remaining).wrap_err_with(|| output_changed_message!("asdf version", "invalid version"))?;
if version < Version::new(0, 15, 0) {
ctx.execute(&asdf).arg("update").status_checked_with_codes(&[42])?;
}
ctx.execute(&asdf).args(["plugin", "update", "--all"]).status_checked()
}
pub fn run_mise(ctx: &ExecutionContext) -> Result<()> {
@@ -556,12 +837,45 @@ pub fn run_mise(ctx: &ExecutionContext) -> Result<()> {
print_separator("mise");
ctx.run_type().execute(&mise).arg("upgrade").status_checked()?;
ctx.execute(&mise).args(["plugins", "update"]).status_checked()?;
ctx.run_type()
let output = ctx
.execute(&mise)
.args(["plugins", "update"])
.status_checked()
.args(["self-update"])
.output_checked_with(|_| Ok(()))?;
let status_code = output
.status
.code()
.ok_or_eyre("Couldn't get status code (terminated by signal)")?;
let stderr = std::str::from_utf8(&output.stderr).wrap_err("Expected output to be valid UTF-8")?;
if stderr.contains("mise is installed via a package manager") && status_code == 1 {
debug!("Mise self-update not available")
} else {
// Write the output
io::stdout().write_all(&output.stdout)?;
io::stderr().write_all(&output.stderr)?;
if status_code != 0 {
return Err(StepFailed.into());
}
}
let mut cmd = ctx.execute(&mise);
cmd.arg("upgrade");
if ctx.config().mise_interactive() {
cmd.arg("--interactive");
}
if ctx.config().mise_bump() {
cmd.arg("--bump");
}
if ctx.config().mise_jobs() != 4 {
cmd.args(["--jobs", &ctx.config().mise_jobs().to_string()]);
}
cmd.status_checked()
}
pub fn run_home_manager(ctx: &ExecutionContext) -> Result<()> {
@@ -569,7 +883,7 @@ pub fn run_home_manager(ctx: &ExecutionContext) -> Result<()> {
print_separator("home-manager");
let mut cmd = ctx.run_type().execute(home_manager);
let mut cmd = ctx.execute(home_manager);
cmd.arg("switch");
if let Some(extra_args) = ctx.config().home_manager() {
@@ -579,45 +893,39 @@ pub fn run_home_manager(ctx: &ExecutionContext) -> Result<()> {
cmd.status_checked()
}
pub fn run_tldr(ctx: &ExecutionContext) -> Result<()> {
let tldr = require("tldr")?;
print_separator("TLDR");
ctx.run_type().execute(tldr).arg("--update").status_checked()
}
pub fn run_pearl(ctx: &ExecutionContext) -> Result<()> {
let pearl = require("pearl")?;
print_separator("pearl");
ctx.run_type().execute(pearl).arg("update").status_checked()
ctx.execute(pearl).arg("update").status_checked()
}
pub fn run_pyenv(ctx: &ExecutionContext) -> Result<()> {
let pyenv = require("pyenv")?;
print_separator("pyenv");
let pyenv_dir = var("PYENV_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".pyenv"));
let pyenv_dir = var("PYENV_ROOT").map_or_else(|_| HOME_DIR.join(".pyenv"), PathBuf::from);
if !pyenv_dir.exists() {
return Err(SkipStep("Pyenv is installed, but $PYENV_ROOT is not set correctly".to_string()).into());
return Err(SkipStep(t!("Pyenv is installed, but $PYENV_ROOT is not set correctly").to_string()).into());
}
if !pyenv_dir.join(".git").exists() {
return Err(SkipStep("pyenv is not a git repository".to_string()).into());
return Err(SkipStep(t!("pyenv is not a git repository").to_string()).into());
}
ctx.run_type().execute(pyenv).arg("update").status_checked()
if !pyenv_dir.join("plugins").join("pyenv-update").exists() {
return Err(SkipStep(t!("pyenv-update plugin is not installed").to_string()).into());
}
ctx.execute(pyenv).arg("update").status_checked()
}
pub fn run_sdkman(ctx: &ExecutionContext) -> Result<()> {
let bash = require("bash")?;
let sdkman_init_path = var("SDKMAN_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".sdkman"))
.map_or_else(|_| HOME_DIR.join(".sdkman"), PathBuf::from)
.join("bin")
.join("sdkman-init.sh")
.require()
@@ -626,8 +934,7 @@ pub fn run_sdkman(ctx: &ExecutionContext) -> Result<()> {
print_separator("SDKMAN!");
let sdkman_config_path = var("SDKMAN_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".sdkman"))
.map_or_else(|_| HOME_DIR.join(".sdkman"), PathBuf::from)
.join("etc")
.join("config")
.require()?;
@@ -640,34 +947,25 @@ pub fn run_sdkman(ctx: &ExecutionContext) -> Result<()> {
if selfupdate_enabled == "true" {
let cmd_selfupdate = format!("source {} && sdk selfupdate", &sdkman_init_path);
ctx.run_type()
.execute(&bash)
ctx.execute(&bash)
.args(["-c", cmd_selfupdate.as_str()])
.status_checked()?;
}
let cmd_update = format!("source {} && sdk update", &sdkman_init_path);
ctx.run_type()
.execute(&bash)
.args(["-c", cmd_update.as_str()])
.status_checked()?;
ctx.execute(&bash).args(["-c", cmd_update.as_str()]).status_checked()?;
let cmd_upgrade = format!("source {} && sdk upgrade", &sdkman_init_path);
ctx.run_type()
.execute(&bash)
.args(["-c", cmd_upgrade.as_str()])
.status_checked()?;
ctx.execute(&bash).args(["-c", cmd_upgrade.as_str()]).status_checked()?;
if ctx.config().cleanup() {
let cmd_flush_archives = format!("source {} && sdk flush archives", &sdkman_init_path);
ctx.run_type()
.execute(&bash)
ctx.execute(&bash)
.args(["-c", cmd_flush_archives.as_str()])
.status_checked()?;
let cmd_flush_temp = format!("source {} && sdk flush temp", &sdkman_init_path);
ctx.run_type()
.execute(&bash)
ctx.execute(&bash)
.args(["-c", cmd_flush_temp.as_str()])
.status_checked()?;
}
@@ -675,30 +973,20 @@ pub fn run_sdkman(ctx: &ExecutionContext) -> Result<()> {
Ok(())
}
pub fn run_bun(ctx: &ExecutionContext) -> Result<()> {
let bun = require("bun")?;
print_separator("Bun");
ctx.run_type().execute(bun).arg("upgrade").status_checked()
}
pub fn run_bun_packages(ctx: &ExecutionContext) -> Result<()> {
let bun = require("bun")?;
print_separator("Bun Packages");
print_separator(t!("Bun Packages"));
let mut package_json: PathBuf = var("BUN_INSTALL")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".bun"));
let mut package_json: PathBuf = var("BUN_INSTALL").map_or_else(|_| HOME_DIR.join(".bun"), PathBuf::from);
package_json.push("install/global/package.json");
if !package_json.exists() {
println!("No global packages installed");
println!("{}", t!("No global packages installed"));
return Ok(());
}
ctx.run_type().execute(bun).args(["-g", "update"]).status_checked()
ctx.execute(bun).args(["-g", "update"]).status_checked()
}
/// Update dotfiles with `rcm(7)`.
@@ -708,17 +996,35 @@ pub fn run_rcm(ctx: &ExecutionContext) -> Result<()> {
let rcup = require("rcup")?;
print_separator("rcm");
ctx.run_type().execute(rcup).arg("-v").status_checked()
ctx.execute(rcup).arg("-v").status_checked()
}
pub fn run_maza(ctx: &ExecutionContext) -> Result<()> {
let maza = require("maza")?;
print_separator("maza");
ctx.run_type().execute(maza).arg("update").status_checked()
ctx.execute(maza).arg("update").status_checked()
}
pub fn reboot() -> Result<()> {
print!("Rebooting...");
Command::new("sudo").arg("reboot").status_checked()
pub fn run_hyprpm(ctx: &ExecutionContext) -> Result<()> {
let hyprpm = require("hyprpm")?;
print_separator("hyprpm");
ctx.execute(hyprpm).arg("update").status_checked()
}
pub fn run_atuin(ctx: &ExecutionContext) -> Result<()> {
let atuin = require("atuin-update")?;
print_separator("atuin");
ctx.execute(atuin).status_checked()
}
pub fn reboot(ctx: &ExecutionContext) -> Result<()> {
match ctx.sudo() {
Some(sudo) => sudo.execute(ctx, "reboot")?.status_checked(),
None => ctx.execute("reboot").status_checked(),
}
}

View File

@@ -3,14 +3,16 @@ use std::{ffi::OsStr, process::Command};
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
use rust_i18n::t;
use tracing::debug;
use crate::command::CommandExt;
use crate::config::UpdatesAutoReboot;
use crate::execution_context::ExecutionContext;
use crate::step::Step;
use crate::terminal::{print_separator, print_warning};
use crate::utils::{require, which};
use crate::{error::SkipStep, steps::git::RepoStep};
use crate::{powershell, Step};
pub fn run_chocolatey(ctx: &ExecutionContext) -> Result<()> {
let choco = require("choco")?;
@@ -18,15 +20,9 @@ pub fn run_chocolatey(ctx: &ExecutionContext) -> Result<()> {
print_separator("Chocolatey");
let mut command = match ctx.sudo() {
Some(sudo) => {
let mut command = ctx.run_type().execute(sudo);
command.arg(choco);
command
}
None => ctx.run_type().execute(choco),
};
let sudo = ctx.require_sudo()?;
let mut command = sudo.execute(ctx, &choco)?;
command.args(["upgrade", "all"]);
if yes {
@@ -41,10 +37,23 @@ pub fn run_winget(ctx: &ExecutionContext) -> Result<()> {
print_separator("winget");
ctx.run_type()
.execute(winget)
.args(["upgrade", "--all"])
.status_checked()
ctx.execute(&winget).args(["source", "update"]).status_checked()?;
let mut command = if ctx.config().winget_use_sudo() {
let sudo = ctx.require_sudo()?;
sudo.execute(ctx, &winget)?
} else {
ctx.execute(winget)
};
let mut args = vec!["upgrade", "--all"];
if ctx.config().winget_silent_install() {
args.push("--silent");
}
command.args(args).status_checked()?;
Ok(())
}
pub fn run_scoop(ctx: &ExecutionContext) -> Result<()> {
@@ -52,26 +61,26 @@ pub fn run_scoop(ctx: &ExecutionContext) -> Result<()> {
print_separator("Scoop");
ctx.run_type().execute(&scoop).args(["update"]).status_checked()?;
ctx.run_type().execute(&scoop).args(["update", "*"]).status_checked()?;
ctx.execute(&scoop).args(["update"]).status_checked()?;
ctx.execute(&scoop).args(["update", "*"]).status_checked()?;
if ctx.config().cleanup() {
ctx.run_type().execute(&scoop).args(["cleanup", "*"]).status_checked()?;
ctx.execute(&scoop).args(["cleanup", "*"]).status_checked()?;
ctx.execute(&scoop).args(["cache", "rm", "-a"]).status_checked()?
}
Ok(())
}
pub fn update_wsl(ctx: &ExecutionContext) -> Result<()> {
if !is_wsl_installed()? {
return Err(SkipStep("WSL not installed".to_string()).into());
return Err(SkipStep(t!("WSL not installed").to_string()).into());
}
let wsl = require("wsl")?;
print_separator("Update WSL");
print_separator(t!("Update WSL"));
let mut wsl_command = ctx.run_type().execute(wsl);
let mut wsl_command = ctx.execute(wsl);
wsl_command.args(["--update"]);
if ctx.config().wsl_update_pre_release() {
@@ -88,7 +97,7 @@ pub fn update_wsl(ctx: &ExecutionContext) -> Result<()> {
/// 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
/// versions of Windows (since windows 10 version 2004), this command is
/// installed by default.
///
/// If the command is installed and the user hasn't installed any Linux distros
@@ -113,8 +122,8 @@ fn get_wsl_distributions(wsl: &Path) -> Result<Vec<String>> {
let output = Command::new(wsl).args(["--list", "-q"]).output_checked_utf8()?.stdout;
Ok(output
.lines()
.map(|x| x.replace(['\u{0}', '\r'], "").trim().to_owned())
.filter(|s| !s.is_empty())
.map(|x| x.replace(['\u{0}', '\r'], ""))
.collect())
}
@@ -122,12 +131,12 @@ fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> R
let topgrade = Command::new(wsl)
.args(["-d", dist, "bash", "-lc", "which topgrade"])
.output_checked_utf8()
.map_err(|_| SkipStep(String::from("Could not find Topgrade installed in WSL")))?
.map_err(|_| SkipStep(t!("Could not find Topgrade installed in WSL").to_string()))?
.stdout // The normal output from `which topgrade` appends a newline, so we trim it here.
.trim_end()
.to_owned();
let mut command = ctx.run_type().execute(wsl);
let mut command = ctx.execute(wsl);
// The `arg` method automatically quotes its arguments.
// This means we can't append additional arguments to `topgrade` in WSL
@@ -137,12 +146,12 @@ fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> R
//
// ```rust
// command
// .args(["-d", dist, "bash", "-c"])
// .args(["-d", dist, "bash", "-lc"])
// .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'`
// > `C:\WINDOWS\system32\wsl.EXE -d Ubuntu bash -lc 'TOPGRADE_PREFIX=Ubuntu exec /bin/topgrade'`
//
// Adding the following:
//
@@ -151,7 +160,7 @@ fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> R
// ```
//
// appends the next argument like so:
// > `C:\WINDOWS\system32\wsl.EXE -d Ubuntu bash -c 'TOPGRADE_PREFIX=Ubuntu exec /bin/topgrade' -v`
// > `C:\WINDOWS\system32\wsl.EXE -d Ubuntu bash -lc 'TOPGRADE_PREFIX=Ubuntu exec /bin/topgrade' -v`
// which means `-v` isn't passed to `topgrade`.
let mut args = String::new();
if ctx.config().verbose() {
@@ -159,7 +168,7 @@ fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> R
}
command
.args(["-d", dist, "bash", "-c"])
.args(["-d", dist, "bash", "-lc"])
.arg(format!("TOPGRADE_PREFIX={dist} exec {topgrade} {args}"));
if ctx.config().yes(Step::Wsl) {
@@ -171,7 +180,7 @@ fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> R
pub fn run_wsl_topgrade(ctx: &ExecutionContext) -> Result<()> {
if !is_wsl_installed()? {
return Err(SkipStep("WSL not installed".to_string()).into());
return Err(SkipStep(t!("WSL not installed").to_string()).into());
}
let wsl = require("wsl")?;
@@ -194,30 +203,70 @@ pub fn run_wsl_topgrade(ctx: &ExecutionContext) -> Result<()> {
if ran {
Ok(())
} else {
Err(SkipStep(String::from("Could not find Topgrade in any WSL disribution")).into())
Err(SkipStep(t!("Could not find Topgrade in any WSL distribution").to_string()).into())
}
}
pub fn windows_update(ctx: &ExecutionContext) -> Result<()> {
let powershell = powershell::Powershell::windows_powershell();
let powershell = ctx.require_powershell()?;
print_separator("Windows Update");
print_separator(t!("Windows Update"));
if powershell.supports_windows_update() {
powershell.windows_update(ctx)
} else {
print_warning(
"Consider installing PSWindowsUpdate as the use of Windows Update via USOClient is not supported.",
);
if !powershell.has_module("PSWindowsUpdate") {
print_warning(t!(
"The PSWindowsUpdate PowerShell module isn't installed so Topgrade can't run Windows Update.\nInstall PSWindowsUpdate by running `Install-Module PSWindowsUpdate` in PowerShell."
));
Err(SkipStep("USOClient not supported.".to_string()).into())
return Err(SkipStep(t!("PSWindowsUpdate is not installed").to_string()).into());
}
let mut cmd = "Import-Module PSWindowsUpdate; Install-WindowsUpdate -Verbose".to_string();
if ctx.config().accept_all_windows_updates() {
cmd.push_str(" -AcceptAll");
}
match ctx.config().windows_updates_auto_reboot() {
UpdatesAutoReboot::Yes => cmd.push_str(" -AutoReboot"),
UpdatesAutoReboot::No => cmd.push_str(" -IgnoreReboot"),
UpdatesAutoReboot::Ask => (), // Prompting is the default for Install-WindowsUpdate
}
powershell.build_command(ctx, &cmd, true)?.status_checked()
}
pub fn reboot() -> Result<()> {
pub fn microsoft_store(ctx: &ExecutionContext) -> Result<()> {
let powershell = ctx.require_powershell()?;
print_separator(t!("Microsoft Store"));
println!("{}", t!("Scanning for updates..."));
// Scan for updates using the MDM UpdateScanMethod
// This method is also available for non-MDM devices
let cmd = r#"(Get-CimInstance -Namespace "Root\cimv2\mdm\dmmap" -ClassName "MDM_EnterpriseModernAppManagement_AppManagement01" | Invoke-CimMethod -MethodName UpdateScanMethod).ReturnValue"#;
powershell
.build_command(ctx, cmd, true)?
.output_checked_with_utf8(|output| {
if !output.status.success() {
return Err(());
}
let ret_val = output.stdout.trim();
debug!("Command return value: {}", ret_val);
if ret_val == "0" { Ok(()) } else { Err(()) }
})?;
println!(
"{}",
t!("Success, Microsoft Store apps are being updated in the background")
);
Ok(())
}
pub fn reboot(ctx: &ExecutionContext) -> Result<()> {
// 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()
ctx.execute("shutdown.exe").args(["/R", "/T", "0"]).status_checked()
}
pub fn insert_startup_scripts(git_repos: &mut RepoStep) -> Result<()> {
@@ -230,7 +279,7 @@ pub fn insert_startup_scripts(git_repos: &mut RepoStep) -> Result<()> {
if let Ok(lnk) = parselnk::Lnk::try_from(Path::new(&path)) {
debug!("Startup link: {:?}", lnk);
if let Some(path) = lnk.relative_path() {
git_repos.insert_if_repo(&startup_dir.join(path));
git_repos.insert_if_repo(startup_dir.join(path));
}
}
}

View File

@@ -1,124 +1,159 @@
#[cfg(windows)]
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use color_eyre::eyre::Result;
#[cfg(windows)]
use color_eyre::eyre::eyre;
use tracing::debug;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::terminal::{is_dumb, print_separator};
use crate::utils::{require_option, which, PathExt};
use crate::Step;
use crate::terminal;
use crate::utils::{PathExt, which};
pub struct Powershell {
path: Option<PathBuf>,
path: PathBuf,
profile: Option<PathBuf>,
is_pwsh: bool,
}
impl Powershell {
/// Returns a powershell instance.
///
/// If the powershell binary is not found, or the current terminal is dumb
/// then the instance of this struct will skip all the powershell steps.
pub fn new() -> Self {
let path = which("pwsh").or_else(|| which("powershell")).filter(|_| !is_dumb());
let profile = path.as_ref().and_then(|path| {
Command::new(path)
.args(["-NoProfile", "-Command", "Split-Path $profile"])
.output_checked_utf8()
.map(|output| PathBuf::from(output.stdout.trim()))
.and_then(|p| p.require())
.ok()
});
Powershell { path, profile }
}
#[cfg(windows)]
pub fn windows_powershell() -> Self {
Powershell {
path: which("powershell").filter(|_| !is_dumb()),
profile: None,
pub fn new() -> Option<Self> {
if terminal::is_dumb() {
return None;
}
}
#[cfg(windows)]
pub fn has_module(powershell: &Path, command: &str) -> bool {
Command::new(powershell)
.args([
"-NoProfile",
"-Command",
&format!("Get-Module -ListAvailable {command}"),
])
.output_checked_utf8()
.map(|result| !result.stdout.is_empty())
.unwrap_or(false)
let (path, is_pwsh) = which("pwsh")
.map(|p| (Some(p), true))
.or_else(|| which("powershell").map(|p| (Some(p), false)))
.unwrap_or((None, false));
path.map(|path| {
let mut ret = Self {
path,
profile: None,
is_pwsh,
};
ret.set_profile();
ret
})
}
pub fn profile(&self) -> Option<&PathBuf> {
self.profile.as_ref()
}
pub fn update_modules(&self, ctx: &ExecutionContext) -> Result<()> {
let powershell = require_option(self.path.as_ref(), String::from("Powershell is not installed"))?;
print_separator("Powershell Modules Update");
let mut cmd = vec!["Update-Module"];
if ctx.config().verbose() {
cmd.push("-Verbose")
}
if ctx.config().yes(Step::Powershell) {
cmd.push("-Force")
}
println!("Updating modules...");
ctx.run_type()
.execute(powershell)
// This probably doesn't need `shell_words::join`.
.args(["-NoProfile", "-Command", &cmd.join(" ")])
.status_checked()
fn set_profile(&mut self) {
let profile = self
.build_command_internal("Split-Path $PROFILE")
.output_checked_utf8()
.map(|output| output.stdout.trim().to_string())
.and_then(|s| PathBuf::from(s).require())
.ok();
debug!("Found PowerShell profile: {:?}", profile);
self.profile = profile;
}
#[cfg(windows)]
pub fn supports_windows_update(&self) -> bool {
self.path
.as_ref()
.map(|p| Self::has_module(p, "PSWindowsUpdate"))
.unwrap_or(false)
pub fn is_pwsh(&self) -> bool {
self.is_pwsh
}
#[cfg(windows)]
pub fn windows_update(&self, ctx: &ExecutionContext) -> Result<()> {
let powershell = require_option(self.path.as_ref(), String::from("Powershell is not installed"))?;
/// Builds an "internal" powershell command
pub fn build_command_internal(&self, cmd: &str) -> Command {
let mut command = Command::new(&self.path);
debug_assert!(self.supports_windows_update());
command.args(["-NoProfile", "-Command"]);
command.arg(cmd);
let mut command = if let Some(sudo) = ctx.sudo() {
let mut command = ctx.run_type().execute(sudo);
command.arg(powershell);
command
} else {
ctx.run_type().execute(powershell)
};
// If topgrade was run from pwsh, but we are trying to run powershell, then
// the inherited PSModulePath breaks module imports
if !self.is_pwsh {
command.env_remove("PSModulePath");
}
command
.args([
"-NoProfile",
"-Command",
&format!(
"Start-Process powershell -Verb runAs -ArgumentList 'Import-Module PSWindowsUpdate; Install-WindowsUpdate -MicrosoftUpdate {} -Verbose'",
if ctx.config().accept_all_windows_updates() {
"-AcceptAll"
} else {
""
}
),
])
.status_checked()
}
/// Builds a "primary" powershell command (uses dry-run if required):
/// {powershell} -NoProfile -Command {cmd}
pub fn build_command<'a>(
&self,
ctx: &'a ExecutionContext,
cmd: &str,
use_sudo: bool,
) -> Result<impl CommandExt + 'a> {
let mut command = if use_sudo {
let sudo = ctx.require_sudo()?;
sudo.execute(ctx, &self.path)?
} else {
ctx.execute(&self.path)
};
#[cfg(windows)]
{
// Check execution policy and return early if it's not set correctly
self.execution_policy_args_if_needed()?;
}
command.args(["-NoProfile", "-Command"]);
command.arg(cmd);
// If topgrade was run from pwsh, but we are trying to run powershell, then
// the inherited PSModulePath breaks module imports
if !self.is_pwsh {
command.env_remove("PSModulePath");
}
Ok(command)
}
#[cfg(windows)]
fn execution_policy_args_if_needed(&self) -> Result<()> {
if !self.is_execution_policy_set("RemoteSigned") {
Err(eyre!(
"PowerShell execution policy is too restrictive. \
Please run 'Set-ExecutionPolicy RemoteSigned -Scope CurrentUser' in PowerShell \
(or use Unrestricted/Bypass if you're sure about the security implications)"
))
} else {
Ok(())
}
}
#[cfg(windows)]
fn is_execution_policy_set(&self, policy: &str) -> bool {
// These policies are ordered from most restrictive to least restrictive
let valid_policies = ["Restricted", "AllSigned", "RemoteSigned", "Unrestricted", "Bypass"];
// Find the index of our target policy
let target_idx = valid_policies.iter().position(|&p| p == policy);
let current_policy = self
.build_command_internal("Get-ExecutionPolicy")
.output_checked_utf8()
.map(|output| output.stdout.trim().to_string());
debug!("Found PowerShell ExecutionPolicy: {:?}", current_policy);
current_policy.is_ok_and(|current_policy| {
// Find the index of the current policy
let current_idx = valid_policies.iter().position(|&p| p == current_policy);
// Check if current policy exists and is at least as permissive as the target
match (current_idx, target_idx) {
(Some(current), Some(target)) => current >= target,
_ => false,
}
})
}
#[cfg(windows)]
pub fn has_module(&self, module_name: &str) -> bool {
let cmd = format!("Get-Module -ListAvailable {}", module_name);
self.build_command_internal(&cmd)
.output_checked()
.map(|output| !output.stdout.trim_ascii().is_empty())
.unwrap_or(false)
}
}

View File

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

View File

@@ -4,13 +4,15 @@ use std::{fmt::Display, rc::Rc, str::FromStr};
use color_eyre::eyre::Result;
use regex::Regex;
use rust_i18n::t;
use strum::EnumString;
use tracing::{debug, error};
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::step::Step;
use crate::terminal::print_separator;
use crate::{error::SkipStep, utils, Step};
use crate::{error::SkipStep, utils};
#[derive(Debug, Copy, Clone, EnumString)]
#[strum(serialize_all = "lowercase")]
@@ -112,8 +114,7 @@ impl<'a> TemporaryPowerOn<'a> {
BoxStatus::Running => unreachable!(),
};
ctx.run_type()
.execute(vagrant)
ctx.execute(vagrant)
.args([subcommand, &vagrant_box.name])
.current_dir(vagrant_box.path.clone())
.status_checked()?;
@@ -125,7 +126,7 @@ impl<'a> TemporaryPowerOn<'a> {
}
}
impl<'a> Drop for TemporaryPowerOn<'a> {
impl Drop for TemporaryPowerOn<'_> {
fn drop(&mut self) {
let subcommand = if self.ctx.config().vagrant_always_suspend().unwrap_or(false) {
"suspend"
@@ -139,7 +140,6 @@ impl<'a> Drop for TemporaryPowerOn<'a> {
println!();
self.ctx
.run_type()
.execute(self.vagrant)
.args([subcommand, &self.vagrant_box.name])
.current_dir(self.vagrant_box.path.clone())
@@ -151,14 +151,14 @@ impl<'a> Drop for TemporaryPowerOn<'a> {
pub fn collect_boxes(ctx: &ExecutionContext) -> Result<Vec<VagrantBox>> {
let directories = utils::require_option(
ctx.config().vagrant_directories(),
String::from("No Vagrant directories were specified in the configuration file"),
String::from(t!("No Vagrant directories were specified in the configuration file")),
)?;
let vagrant = Vagrant {
path: utils::require("vagrant")?,
};
print_separator("Vagrant");
println!("Collecting Vagrant boxes");
println!("{}", t!("Collecting Vagrant boxes"));
let mut result = Vec::new();
@@ -179,25 +179,28 @@ pub fn topgrade_vagrant_box(ctx: &ExecutionContext, vagrant_box: &VagrantBox) ->
path: utils::require("vagrant")?,
};
let seperator = format!("Vagrant ({})", vagrant_box.smart_name());
let separator = format!("Vagrant ({})", vagrant_box.smart_name());
let mut _poweron = None;
if !vagrant_box.initial_status.powered_on() {
if !(ctx.config().vagrant_power_on().unwrap_or(true)) {
return Err(SkipStep(format!("Skipping powered off box {vagrant_box}")).into());
return Err(SkipStep(format!(
"{}",
t!("Skipping powered off box {vagrant_box}", vagrant_box = vagrant_box)
))
.into());
} else {
print_separator(seperator);
print_separator(separator);
_poweron = Some(vagrant.temporary_power_on(vagrant_box, ctx)?);
}
} else {
print_separator(seperator);
print_separator(separator);
}
let mut command = format!("env TOPGRADE_PREFIX={} topgrade", vagrant_box.smart_name());
if ctx.config().yes(Step::Vagrant) {
command.push_str(" -y");
}
ctx.run_type()
.execute(&vagrant.path)
ctx.execute(&vagrant.path)
.current_dir(&vagrant_box.path)
.args(["ssh", "-c", &command])
.status_checked()
@@ -205,7 +208,7 @@ pub fn topgrade_vagrant_box(ctx: &ExecutionContext, vagrant_box: &VagrantBox) ->
pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> {
let vagrant = utils::require("vagrant")?;
print_separator("Vagrant boxes");
print_separator(t!("Vagrant boxes"));
let outdated = Command::new(&vagrant)
.args(["box", "outdated", "--global"])
@@ -217,7 +220,6 @@ pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> {
for ele in re.captures_iter(&outdated.stdout) {
found = true;
let _ = ctx
.run_type()
.execute(&vagrant)
.args(["box", "update", "--box"])
.arg(ele.get(1).unwrap().as_str())
@@ -227,12 +229,9 @@ pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> {
}
if !found {
println!("No outdated boxes")
println!("{}", t!("No outdated boxes"));
} else {
ctx.run_type()
.execute(&vagrant)
.args(["box", "prune"])
.status_checked()?;
ctx.execute(&vagrant).args(["box", "prune"]).status_checked()?;
}
Ok(())

View File

@@ -2,34 +2,53 @@ 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 color_eyre::eyre::eyre;
use etcetera::base_strategy::BaseStrategy;
use crate::command::CommandExt;
use crate::config::TmuxConfig;
use crate::config::TmuxSessionMode;
use crate::terminal::print_separator;
use crate::HOME_DIR;
use crate::{HOME_DIR, XDG_DIRS};
use crate::{
execution_context::ExecutionContext,
utils::{which, PathExt},
utils::{PathExt, which},
};
use rust_i18n::t;
#[cfg(unix)]
use std::os::unix::process::CommandExt as _;
// update_plugins path is relative to the TPM path
const UPDATE_PLUGINS: &str = "bin/update_plugins";
// Default TPM path relative to the TMux config directory
const TPM_PATH: &str = "plugins/tpm";
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"),
// Use `$TMUX_PLUGIN_MANAGER_PATH` if set,
Ok(var) => PathBuf::from(var).join(UPDATE_PLUGINS),
Err(_) => {
// otherwise, use the default XDG location `~/.config/tmux`
#[cfg(unix)]
let xdg_path = XDG_DIRS.config_dir().join("tmux").join(TPM_PATH).join(UPDATE_PLUGINS);
#[cfg(windows)]
let xdg_path = HOME_DIR.join(".config/tmux").join(TPM_PATH).join(UPDATE_PLUGINS);
if xdg_path.exists() {
xdg_path
} else {
// or fallback on the standard default location `~/.tmux`.
HOME_DIR.join(".tmux").join(TPM_PATH).join(UPDATE_PLUGINS)
}
}
}
.require()?;
print_separator("tmux plugins");
ctx.run_type().execute(tpm).arg("all").status_checked()
ctx.execute(tpm).arg("all").status_checked()
}
struct Tmux {
@@ -125,13 +144,13 @@ impl Tmux {
.output_checked_utf8()?
.stdout
.lines()
.map(|l| l.parse())
.map(str::parse)
.collect::<Result<Vec<usize>, _>>()
.context("Failed to compute tmux windows")
}
}
pub fn run_in_tmux(args: Vec<String>) -> Result<()> {
pub fn run_in_tmux(config: TmuxConfig) -> Result<()> {
let command = {
let mut command = vec![
String::from("env"),
@@ -144,39 +163,50 @@ pub fn run_in_tmux(args: Vec<String>) -> Result<()> {
shell_words::join(command)
};
let tmux = Tmux::new(args);
let tmux = Tmux::new(config.args);
// Find an unused session and run `topgrade` in it with the current command's arguments.
let session_name = "topgrade";
let window_name = "topgrade";
let session = tmux.new_unique_session(session_name, window_name, &command)?;
// Only attach to the newly-created session if we're not currently in a tmux session.
if env::var("TMUX").is_err() {
let err = tmux.build().args(["attach-session", "-t", &session]).exec();
Err(eyre!("{err}")).context("Failed to `execvp(3)` tmux")
} else {
println!("Topgrade launched in a new tmux session");
Ok(())
}
let is_inside_tmux = env::var("TMUX").is_ok();
let err = match config.session_mode {
TmuxSessionMode::AttachIfNotInSession => {
if is_inside_tmux {
// Only attach to the newly-created session if we're not currently in a tmux session.
println!("{}", t!("Topgrade launched in a new tmux session"));
return Ok(());
} else {
tmux.build().args(["attach-session", "-t", &session]).exec()
}
}
TmuxSessionMode::AttachAlways => {
if is_inside_tmux {
tmux.build().args(["switch-client", "-t", &session]).exec()
} else {
tmux.build().args(["attach-session", "-t", &session]).exec()
}
}
};
Err(eyre!("{err}")).context("Failed to `execvp(3)` tmux")
}
pub fn run_command(ctx: &ExecutionContext, window_name: &str, command: &str) -> Result<()> {
let tmux = Tmux::new(ctx.config().tmux_arguments()?);
let tmux = Tmux::new(ctx.config().tmux_config()?.args);
match ctx.get_tmux_session() {
Some(session_name) => {
let indices = tmux.window_indices(&session_name)?;
let last_window = indices
.iter()
.last()
.ok_or_else(|| eyre!("tmux session {session_name} has no windows"))?;
tmux.new_window(&session_name, &format!("{last_window}"), command)?;
}
None => {
let name = tmux.new_unique_session("topgrade", window_name, command)?;
ctx.set_tmux_session(name);
}
if let Some(session_name) = ctx.get_tmux_session() {
let indices = tmux.window_indices(&session_name)?;
let last_window = indices
.iter()
.last()
.ok_or_else(|| eyre!("tmux session {session_name} has no windows"))?;
tmux.new_window(&session_name, &format!("{last_window}"), command)?;
} else {
let name = tmux.new_unique_session("topgrade", window_name, command)?;
ctx.set_tmux_session(name);
}
Ok(())
}

View File

@@ -1,7 +1,7 @@
use color_eyre::eyre::Result;
use crate::command::CommandExt;
use crate::config::Step;
use crate::step::Step;
use crate::terminal::print_separator;
use crate::{execution_context::ExecutionContext, utils::require};
use std::path::Path;
@@ -59,7 +59,7 @@ pub fn run_toolbx(ctx: &ExecutionContext) -> Result<()> {
args.push("--yes");
}
ctx.run_type().execute(&toolbx).args(&args).status_checked()?;
ctx.execute(&toolbx).args(&args).status_checked()?;
}
Ok(())

View File

@@ -1,6 +1,6 @@
use crate::HOME_DIR;
use crate::command::CommandExt;
use crate::error::{SkipStep, TopgradeError};
use crate::HOME_DIR;
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
@@ -8,8 +8,9 @@ use crate::executor::{Executor, ExecutorOutput};
use crate::terminal::print_separator;
use crate::{
execution_context::ExecutionContext,
utils::{require, PathExt},
utils::{PathExt, require},
};
use rust_i18n::t;
use std::path::PathBuf;
use std::{
io::{self, Write},
@@ -28,15 +29,32 @@ pub fn vimrc() -> Result<PathBuf> {
fn nvimrc() -> Result<PathBuf> {
#[cfg(unix)]
let base_dir = crate::XDG_DIRS.config_dir();
let bases: Vec<PathBuf> = vec![crate::XDG_DIRS.config_dir()];
#[cfg(windows)]
let base_dir = crate::WINDOWS_DIRS.cache_dir();
let mut bases: Vec<PathBuf> = vec![crate::WINDOWS_DIRS.cache_dir()];
base_dir
.join("nvim/init.vim")
.require()
.or_else(|_| base_dir.join("nvim/init.lua").require())
#[cfg(windows)]
{
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|path| path.is_absolute())
{
bases.insert(0, xdg);
}
}
for base_dir in bases {
if let Ok(p) = base_dir
.join("nvim/init.vim")
.require()
.or_else(|_| base_dir.join("nvim/init.lua").require())
{
return Ok(p);
}
}
Err(SkipStep(format!("{}", t!("No Neovim config found"))).into())
}
fn upgrade_script() -> Result<tempfile::NamedTempFile> {
@@ -64,7 +82,7 @@ fn upgrade(command: &mut Executor, ctx: &ExecutionContext) -> Result<()> {
if !status.success() {
return Err(TopgradeError::ProcessFailed(command.get_program(), status).into());
} else {
println!("Plugins upgraded")
println!("{}", t!("Plugins upgraded"));
}
}
@@ -77,25 +95,21 @@ pub fn upgrade_ultimate_vimrc(ctx: &ExecutionContext) -> Result<()> {
let python = require("python3")?;
let update_plugins = config_dir.join("update_plugins.py").require()?;
print_separator("The Ultimate vimrc");
print_separator(t!("The Ultimate vimrc"));
ctx.run_type()
.execute(&git)
ctx.execute(&git)
.current_dir(&config_dir)
.args(["reset", "--hard"])
.status_checked()?;
ctx.run_type()
.execute(&git)
ctx.execute(&git)
.current_dir(&config_dir)
.args(["clean", "-d", "--force"])
.status_checked()?;
ctx.run_type()
.execute(&git)
ctx.execute(&git)
.current_dir(&config_dir)
.args(["pull", "--rebase"])
.status_checked()?;
ctx.run_type()
.execute(python)
ctx.execute(python)
.current_dir(config_dir)
.arg(update_plugins)
.status_checked()?;
@@ -108,15 +122,14 @@ pub fn upgrade_vim(ctx: &ExecutionContext) -> Result<()> {
let output = Command::new(&vim).arg("--version").output_checked_utf8()?;
if !output.stdout.starts_with("VIM") {
return Err(SkipStep(String::from("vim binary might be actually nvim")).into());
return Err(SkipStep(t!("vim binary might be actually nvim").to_string()).into());
}
let vimrc = vimrc()?;
print_separator("Vim");
upgrade(
ctx.run_type()
.execute(&vim)
ctx.execute(&vim)
.args(["-u"])
.arg(vimrc)
.args(["-U", "NONE", "-V1", "-nNesS"])
@@ -131,8 +144,7 @@ pub fn upgrade_neovim(ctx: &ExecutionContext) -> Result<()> {
print_separator("Neovim");
upgrade(
ctx.run_type()
.execute(nvim)
ctx.execute(nvim)
.args(["-u"])
.arg(nvimrc)
.args(["--headless", "-V1", "-nS"])
@@ -146,5 +158,5 @@ pub fn run_voom(ctx: &ExecutionContext) -> Result<()> {
print_separator("voom");
ctx.run_type().execute(voom).arg("update").status_checked()
ctx.execute(voom).arg("update").status_checked()
}

View File

@@ -6,13 +6,13 @@ use color_eyre::eyre::Result;
use tracing::debug;
use walkdir::WalkDir;
use crate::HOME_DIR;
use crate::XDG_DIRS;
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 crate::utils::{PathExt, require};
use etcetera::base_strategy::BaseStrategy;
pub fn run_zr(ctx: &ExecutionContext) -> Result<()> {
@@ -23,16 +23,11 @@ pub fn run_zr(ctx: &ExecutionContext) -> Result<()> {
print_separator("zr");
let cmd = format!("source {} && zr --update", zshrc().display());
ctx.run_type()
.execute(zsh)
.args(["-l", "-c", cmd.as_str()])
.status_checked()
ctx.execute(zsh).args(["-l", "-c", cmd.as_str()]).status_checked()
}
fn zdotdir() -> PathBuf {
env::var("ZDOTDIR")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.clone())
env::var("ZDOTDIR").map_or_else(|_| HOME_DIR.clone(), PathBuf::from)
}
pub fn zshrc() -> PathBuf {
@@ -46,8 +41,7 @@ pub fn run_antidote(ctx: &ExecutionContext) -> Result<()> {
print_separator("antidote");
ctx.run_type()
.execute(zsh)
ctx.execute(zsh)
.arg("-c")
.arg(format!("source {} && antidote update", antidote.display()))
.status_checked()
@@ -59,41 +53,33 @@ pub fn run_antibody(ctx: &ExecutionContext) -> Result<()> {
print_separator("antibody");
ctx.run_type().execute(antibody).arg("update").status_checked()
ctx.execute(antibody).arg("update").status_checked()
}
pub fn run_antigen(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?;
let zshrc = zshrc().require()?;
env::var("ADOTDIR")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join("antigen.zsh"))
.map_or_else(|_| HOME_DIR.join("antigen.zsh"), PathBuf::from)
.require()?;
print_separator("antigen");
let cmd = format!("source {} && (antigen selfupdate ; antigen update)", zshrc.display());
ctx.run_type()
.execute(zsh)
.args(["-l", "-c", cmd.as_str()])
.status_checked()
ctx.execute(zsh).args(["-l", "-c", cmd.as_str()]).status_checked()
}
pub fn run_zgenom(ctx: &ExecutionContext) -> Result<()> {
let zsh = require("zsh")?;
let zshrc = zshrc().require()?;
env::var("ZGEN_SOURCE")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".zgenom"))
.map_or_else(|_| HOME_DIR.join(".zgenom"), PathBuf::from)
.require()?;
print_separator("zgenom");
let cmd = format!("source {} && zgenom selfupdate && zgenom update", zshrc.display());
ctx.run_type()
.execute(zsh)
.args(["-l", "-c", cmd.as_str()])
.status_checked()
ctx.execute(zsh).args(["-l", "-c", cmd.as_str()]).status_checked()
}
pub fn run_zplug(ctx: &ExecutionContext) -> Result<()> {
@@ -101,16 +87,12 @@ pub fn run_zplug(ctx: &ExecutionContext) -> Result<()> {
zshrc().require()?;
env::var("ZPLUG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".zplug"))
.map_or_else(|_| HOME_DIR.join(".zplug"), PathBuf::from)
.require()?;
print_separator("zplug");
ctx.run_type()
.execute(zsh)
.args(["-i", "-c", "zplug update"])
.status_checked()
ctx.execute(zsh).args(["-i", "-c", "zplug update"]).status_checked()
}
pub fn run_zinit(ctx: &ExecutionContext) -> Result<()> {
@@ -118,17 +100,13 @@ pub fn run_zinit(ctx: &ExecutionContext) -> Result<()> {
let zshrc = zshrc().require()?;
env::var("ZINIT_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| XDG_DIRS.data_dir().join("zinit"))
.map_or_else(|_| XDG_DIRS.data_dir().join("zinit"), PathBuf::from)
.require()?;
print_separator("zinit");
let cmd = format!("source {} && zinit self-update && zinit update --all", zshrc.display());
ctx.run_type()
.execute(zsh)
.args(["-i", "-c", cmd.as_str()])
.status_checked()
ctx.execute(zsh).args(["-i", "-c", cmd.as_str()]).status_checked()
}
pub fn run_zi(ctx: &ExecutionContext) -> Result<()> {
@@ -140,7 +118,7 @@ pub fn run_zi(ctx: &ExecutionContext) -> Result<()> {
print_separator("zi");
let cmd = format!("source {} && zi self-update && zi update --all", zshrc.display());
ctx.run_type().execute(zsh).args(["-i", "-c", &cmd]).status_checked()
ctx.execute(zsh).args(["-i", "-c", &cmd]).status_checked()
}
pub fn run_zim(ctx: &ExecutionContext) -> Result<()> {
@@ -153,14 +131,12 @@ pub fn run_zim(ctx: &ExecutionContext) -> Result<()> {
.output_checked_utf8()
.map(|o| o.stdout)
})
.map(PathBuf::from)
.unwrap_or_else(|_| HOME_DIR.join(".zim"))
.map_or_else(|_| HOME_DIR.join(".zim"), PathBuf::from)
.require()?;
print_separator("zim");
ctx.run_type()
.execute(zsh)
ctx.execute(zsh)
.args(["-i", "-c", "zimfw upgrade && zimfw update"])
.status_checked()
}
@@ -186,7 +162,7 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
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);
unsafe { env::set_var("ZSH", env_zsh) };
}
}
@@ -210,8 +186,7 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
.unwrap_or_else(|e| {
let default_path = oh_my_zsh.join("custom");
debug!(
"Running zsh returned {}. Using default path: {}",
e,
"Running zsh returned {e}. Using default path: {}",
default_path.display()
);
default_path
@@ -227,9 +202,8 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
}
custom_repos.remove(&oh_my_zsh);
ctx.run_type()
.execute("zsh")
.arg(&oh_my_zsh.join("tools/upgrade.sh"))
ctx.execute("zsh")
.arg(oh_my_zsh.join("tools/upgrade.sh"))
// oh-my-zsh returns 80 when it is already updated and no changes pulled
// in this update.
// See this comment: https://github.com/r-darwish/topgrade/issues/569#issuecomment-736756731

View File

@@ -2,12 +2,23 @@ use std::ffi::OsStr;
use std::path::Path;
use std::path::PathBuf;
#[cfg(windows)]
use color_eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
#[cfg(windows)]
use color_eyre::eyre::eyre;
use rust_i18n::t;
use serde::Deserialize;
use strum::AsRefStr;
use strum::Display;
use thiserror::Error;
#[cfg(windows)]
use tracing::{debug, warn};
#[cfg(windows)]
use windows::Win32::Foundation::ERROR_FILE_NOT_FOUND;
use crate::command::CommandExt;
use crate::error::UnsupportedSudo;
use crate::execution_context::ExecutionContext;
use crate::executor::Executor;
use crate::terminal::print_separator;
@@ -16,26 +27,241 @@ use crate::utils::which;
#[derive(Clone, Debug)]
pub struct Sudo {
/// The path to the `sudo` binary.
path: PathBuf,
path: Option<PathBuf>,
/// The type of program being used as `sudo`.
kind: SudoKind,
}
#[derive(Error, Debug)]
pub enum SudoCreateError {
CannotFindBinary,
#[cfg(windows)]
WinSudoDisabled,
#[cfg(windows)]
WinSudoNewWindowMode,
}
impl std::fmt::Display for SudoCreateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SudoCreateError::CannotFindBinary => {
write!(f, "{}", t!("Cannot find sudo binary"))
}
#[cfg(windows)]
SudoCreateError::WinSudoDisabled => {
write!(f, "{}", t!("Found Windows Sudo, but it is disabled"))
}
#[cfg(windows)]
SudoCreateError::WinSudoNewWindowMode => {
write!(
f,
"{}",
t!("Found Windows Sudo, but it is using 'In a new window' mode")
)
}
}
}
}
#[derive(Clone, Debug, Default)]
pub enum SudoPreserveEnv<'a> {
/// Preserve all environment variables.
All,
/// Preserve only the specified environment variables.
Some(&'a [&'a str]),
/// Preserve no environment variables.
#[default]
None,
}
/// Generic sudo options, translated into flags to pass to `sudo`.
/// NOTE: Depending on the sudo kind, OS and system config, some options might be specified by
/// default or unsupported.
#[derive(Clone, Debug, Default)]
pub struct SudoExecuteOpts<'a> {
/// Run the command inside a login shell.
pub login_shell: bool,
/// Preserve environment variables across the sudo call.
pub preserve_env: SudoPreserveEnv<'a>,
/// Set the HOME environment variable to the target user's home directory.
pub set_home: bool,
/// Run the command as a user other than the root user.
pub user: Option<&'a str>,
}
impl<'a> SudoExecuteOpts<'a> {
pub fn new() -> Self {
Self::default()
}
/// Run the command inside a login shell.
#[allow(unused)]
pub fn login_shell(mut self) -> Self {
self.login_shell = true;
self
}
/// Preserve all environment variables across the sudo call.
#[allow(unused)]
pub fn preserve_env(mut self) -> Self {
self.preserve_env = SudoPreserveEnv::All;
self
}
/// Preserve only the specified environment variables across the sudo call.
#[allow(unused)]
pub fn preserve_env_list(mut self, vars: &'a [&'a str]) -> Self {
self.preserve_env = SudoPreserveEnv::Some(vars);
self
}
/// Set the HOME environment variable to the target user's home directory.
#[allow(unused)]
pub fn set_home(mut self) -> Self {
self.set_home = true;
self
}
/// Run the command as a user other than the root user.
#[allow(unused)]
pub fn user(mut self, user: &'a str) -> Self {
self.user = Some(user);
self
}
}
#[cfg(not(windows))]
const DETECT_ORDER: [SudoKind; 5] = [
SudoKind::Doas,
SudoKind::Sudo,
SudoKind::Pkexec,
SudoKind::Run0,
SudoKind::Please,
];
// NOTE: keep WinSudo last, allows short-circuit error return in Sudo::detect() to work
#[cfg(windows)]
const DETECT_ORDER: [SudoKind; 2] = [SudoKind::Gsudo, SudoKind::WinSudo];
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 })
pub fn detect() -> Result<Self, SudoCreateError> {
use SudoCreateError::*;
for kind in DETECT_ORDER {
match Self::new(kind) {
Ok(sudo) => return Ok(sudo),
Err(CannotFindBinary) => continue,
#[cfg(windows)]
Err(e @ (WinSudoDisabled | WinSudoNewWindowMode)) => {
// we can return directly here since WinSudo is detected last
return Err(e);
}
}
}
Err(CannotFindBinary)
}
/// 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 })
pub fn new(kind: SudoKind) -> Result<Self, SudoCreateError> {
// no actual binary for null sudo
if let SudoKind::Null = kind {
return Ok(Self { path: None, kind });
}
match kind.which() {
Some(path) => {
let sudo = Self { path: Some(path), kind };
#[cfg(windows)]
if let SudoKind::WinSudo = kind {
// Windows Sudo might be disabled, causing it to error on use.
//
// It checks two registry keys to determine its mode:
// a "policy" (HLKM\SOFTWARE\Policies\Microsoft\Windows\Sudo\Enabled)
// and a "setting" (HLKM\SOFTWARE\Microsoft\Windows\CurrentVersion\Sudo\Enabled).
//
// Both keys are u32's, with these meanings:
// 0 - Disabled
// 1 - ForceNewWindow
// 2 - DisableInput
// 3 - Normal
//
// Setting the sudo option in Settings changes the setting key, the policy key
// sets an upper limit on the setting key: mode = min(policy, setting).
// The default for the policy key is 3 (all modes allowed), and the default for
// the setting key is 0 (disabled).
//
// See https://github.com/microsoft/sudo/blob/9f50d79704a9d4d468bc59f725993714762981ca/sudo/src/helpers.rs#L442
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
enum SudoMode {
Disabled = 0,
ForceNewWindow = 1,
DisableInput = 2,
Normal = 3,
}
impl TryFrom<u32> for SudoMode {
type Error = eyre::Error;
fn try_from(value: u32) -> Result<Self> {
match value {
0 => Ok(SudoMode::Disabled),
1 => Ok(SudoMode::ForceNewWindow),
2 => Ok(SudoMode::DisableInput),
3 => Ok(SudoMode::Normal),
_ => Err(eyre!("invalid integer SudoMode: {value}")),
}
}
}
fn get_mode(key: &str, on_missing: SudoMode) -> SudoMode {
match windows_registry::LOCAL_MACHINE
.open(key)
.and_then(|k| k.get_u32("Enabled"))
{
Ok(v) => v.min(3).try_into().unwrap(),
Err(e) if e.code() == ERROR_FILE_NOT_FOUND.to_hresult() => on_missing,
Err(e) => {
// warn, but treat as normal (using sudo should error)
warn!(r"Error reading registry key HKLM\{key}\Enabled: {e}");
SudoMode::Normal
}
}
}
// default to normal if key missing
let policy_mode = get_mode(r"SOFTWARE\Policies\Microsoft\Windows\Sudo", SudoMode::Normal);
debug!("Windows Sudo policy mode: {policy_mode:?}");
// default to disabled if key missing
let setting_mode = get_mode(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Sudo", SudoMode::Disabled);
debug!("Windows Sudo setting mode: {setting_mode:?}");
let sudo_mode = policy_mode.min(setting_mode);
debug!("Windows Sudo mode: {sudo_mode:?}");
if sudo_mode == SudoMode::Disabled {
return Err(SudoCreateError::WinSudoDisabled);
} else if sudo_mode == SudoMode::ForceNewWindow {
return Err(SudoCreateError::WinSudoNewWindowMode);
}
// Normal mode is best, but DisableInput doesn't seem to cause issues
}
Ok(sudo)
}
None => Err(SudoCreateError::CannotFindBinary),
}
}
/// Gets the path to the `sudo` binary. Do not use this to execute `sudo` directly - either use
/// [`Sudo::elevate`], or if you need to specify arguments to `sudo`, use [`Sudo::elevate_opts`].
/// This way, sudo options can be specified generically and the actual arguments customized
/// depending on the sudo kind.
#[allow(unused)]
pub fn path(&self) -> Option<&Path> {
self.path.as_deref()
}
/// Elevate permissions with `sudo`.
@@ -45,8 +271,15 @@ impl Sudo {
///
/// See: https://github.com/topgrade-rs/topgrade/issues/205
pub fn elevate(&self, ctx: &ExecutionContext) -> Result<()> {
// skip if using null sudo
if let SudoKind::Null = self.kind {
return Ok(());
}
print_separator("Sudo");
let mut cmd = ctx.run_type().execute(self);
// self.path is only None for null sudo, which we've handled above
let mut cmd = ctx.execute(self.path.as_deref().unwrap());
match self.kind {
SudoKind::Doas => {
// `doas` doesn't have anything like `sudo -v` to cache credentials,
@@ -64,10 +297,19 @@ impl Sudo {
// command. Not all security policies support cached credentials.
cmd.arg("-v");
}
SudoKind::WinSudo => {
// Windows `sudo` doesn't cache credentials, so we just execute a
// dummy command - the easiest on Windows is `rem` in cmd.
// See: https://learn.microsoft.com/en-us/windows/advanced-settings/sudo/
cmd.args(["cmd.exe", "/c", "rem"]);
}
SudoKind::Gsudo => {
// Shows current user, cache and console status.
// `gsudo` doesn't have anything like `sudo -v` to cache credentials,
// so we just execute a dummy command - the easiest on Windows is
// `rem` in cmd. `-d` tells it to run the command directly, without
// going through a shell (which could be powershell) first.
// See: https://gerardog.github.io/gsudo/docs/usage
cmd.arg("status");
cmd.args(["-d", "cmd.exe", "/c", "rem"]);
}
SudoKind::Pkexec => {
// I don't think this does anything; `pkexec` usually asks for
@@ -79,47 +321,246 @@ impl Sudo {
// See: https://linux.die.net/man/1/pkexec
cmd.arg("echo");
}
SudoKind::Run0 => {
// `run0` uses polkit for authentication
// and thus has the same issues as `pkexec`.
//
// See: https://www.freedesktop.org/software/systemd/man/devel/run0.html
cmd.arg("echo");
}
SudoKind::Please => {
// From `man please`
// -w, --warm
// Warm the access token and exit.
cmd.arg("-w");
}
SudoKind::Null => unreachable!(),
}
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);
pub fn execute<S: AsRef<OsStr>>(&self, ctx: &ExecutionContext, command: S) -> Result<Executor> {
self.execute_opts(ctx, command, SudoExecuteOpts::new())
}
if let SudoKind::Sudo = self.kind {
cmd.arg("--preserve-env=DIFFPROG");
/// Execute a command with `sudo`, with custom options.
pub fn execute_opts<S: AsRef<OsStr>>(
&self,
ctx: &ExecutionContext,
command: S,
opts: SudoExecuteOpts,
) -> Result<Executor> {
// null sudo is very different, do separately
if let SudoKind::Null = self.kind {
if opts.login_shell {
// TODO: emulate running in a login shell with su/runuser
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "login_shell",
}
.into());
}
if opts.user.is_some() {
// TODO: emulate running as a different user with su/runuser
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "user",
}
.into());
}
// NOTE: we ignore preserve_env and set_home, using
// no sudo effectively preserves these by default
// run command directly
return Ok(ctx.execute(command));
}
if interactive {
cmd.arg("-i");
// self.path is only None for null sudo, which we've handled above
let mut cmd = ctx.execute(self.path.as_ref().unwrap());
if opts.login_shell {
match self.kind {
SudoKind::Sudo => {
cmd.arg("-i");
}
SudoKind::Gsudo => {
// By default, gsudo runs all commands inside a shell. If login_shell
// is *not* specified, we add `-d` to run outside of a shell - see below.
}
SudoKind::Doas | SudoKind::WinSudo | SudoKind::Pkexec | SudoKind::Run0 | SudoKind::Please => {
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "login_shell",
}
.into());
}
SudoKind::Null => unreachable!(),
}
} else if let SudoKind::Gsudo = self.kind {
// The `-d` (direct) flag disables shell detection, running the command directly
// rather than through the current shell.
// Additionally, if the current shell is pwsh >= 7.3.0, then not including this
// gives errors if the command to run has spaces in it: see
// https://github.com/gerardog/gsudo/issues/297
cmd.arg("-d");
}
match opts.preserve_env {
SudoPreserveEnv::All => match self.kind {
SudoKind::Sudo => {
cmd.arg("-E");
}
SudoKind::Gsudo => {
cmd.arg("--copyEV");
}
SudoKind::Doas | SudoKind::WinSudo | SudoKind::Pkexec | SudoKind::Run0 | SudoKind::Please => {
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "preserve_env",
}
.into());
}
SudoKind::Null => unreachable!(),
},
SudoPreserveEnv::Some(vars) => match self.kind {
SudoKind::Sudo => {
cmd.arg(format!("--preserve-env={}", vars.join(",")));
}
SudoKind::Run0 => {
for env in vars {
cmd.arg(format!("--setenv={}", env));
}
}
SudoKind::Please => {
cmd.arg("-a");
cmd.arg(vars.join(","));
}
SudoKind::Doas | SudoKind::WinSudo | SudoKind::Gsudo | SudoKind::Pkexec => {
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "preserve_env_list",
}
.into());
}
SudoKind::Null => unreachable!(),
},
SudoPreserveEnv::None => {}
}
if opts.set_home {
match self.kind {
SudoKind::Sudo => {
cmd.arg("-H");
}
SudoKind::Doas
| SudoKind::WinSudo
| SudoKind::Gsudo
| SudoKind::Pkexec
| SudoKind::Run0
| SudoKind::Please => {
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "set_home",
}
.into());
}
SudoKind::Null => unreachable!(),
}
}
if let Some(user) = opts.user {
match self.kind {
SudoKind::Sudo => {
cmd.args(["-u", user]);
}
SudoKind::Doas | SudoKind::Gsudo | SudoKind::Run0 | SudoKind::Please => {
cmd.args(["-u", user]);
}
SudoKind::Pkexec => {
cmd.args(["--user", user]);
}
SudoKind::WinSudo => {
// Windows sudo is the only one that doesn't have a `-u` flag
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "user",
}
.into());
}
SudoKind::Null => unreachable!(),
}
}
cmd.arg(command);
cmd
Ok(cmd)
}
}
#[derive(Clone, Copy, Debug, Deserialize, AsRefStr)]
// On unix we use `SudoKind::Sudo`, and on windows `SudoKind::WinSudo`.
// We always define both though, so that we don't have to put
// #[cfg(...)] everywhere.
#[derive(Clone, Copy, Debug, Display, Deserialize)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum SudoKind {
Doas,
// On unix, "sudo" in the config file means Sudo
#[cfg(not(windows))]
Sudo,
// and WinSudo is skipped, making it unused.
#[cfg(not(windows))]
#[expect(unused, reason = "WinSudo is windows-only")]
#[serde(skip)]
WinSudo,
// On unix, Sudo is skipped and unused
#[cfg(windows)]
#[expect(unused, reason = "Sudo is unix-only")]
#[serde(skip)]
Sudo,
// and "sudo" in the config file means WinSudo.
#[cfg(windows)]
#[serde(rename = "sudo")]
WinSudo,
Doas,
Gsudo,
Pkexec,
Run0,
Please,
/// A "no-op" sudo, used when topgrade itself is running as root
Null,
}
impl AsRef<OsStr> for Sudo {
fn as_ref(&self) -> &OsStr {
self.path.as_ref()
impl SudoKind {
/// Get the name of the "sudo" binary.
///
/// For `SudoKind::WinSudo`, returns the full hardcoded path
/// instead to ensure we find Windows Sudo rather than gsudo
/// masquerading as sudo.
///
/// Only returns `None` for `SudoKind::Null`.
fn binary_name(self) -> Option<&'static str> {
match self {
SudoKind::Doas => Some("doas"),
SudoKind::Sudo => Some("sudo"),
SudoKind::WinSudo => Some(r"C:\Windows\System32\sudo.exe"),
SudoKind::Gsudo => Some("gsudo"),
SudoKind::Pkexec => Some("pkexec"),
SudoKind::Run0 => Some("run0"),
SudoKind::Please => Some("please"),
SudoKind::Null => None,
}
}
/// Find the full path to the "sudo" binary, if it exists on the system.
fn which(self) -> Option<PathBuf> {
match self.binary_name() {
Some(name) => which(name),
None => None,
}
}
}

View File

@@ -2,25 +2,23 @@ use std::cmp::{max, min};
use std::env;
use std::io::{self, Write};
use std::process::Command;
use std::sync::Mutex;
use std::sync::{LazyLock, Mutex};
use std::time::Duration;
use chrono::{Local, Timelike};
use color_eyre::eyre;
use color_eyre::eyre::Context;
use console::{style, Key, Term};
use lazy_static::lazy_static;
use console::{Key, Term, style};
use notify_rust::{Notification, Timeout};
use rust_i18n::t;
use tracing::{debug, error};
#[cfg(windows)]
use which_crate::which;
use crate::command::CommandExt;
use crate::report::StepResult;
use crate::runner::StepResult;
lazy_static! {
static ref TERMINAL: Mutex<Terminal> = Mutex::new(Terminal::new());
}
static TERMINAL: LazyLock<Mutex<Terminal>> = LazyLock::new(|| Mutex::new(Terminal::new()));
#[cfg(unix)]
pub fn shell() -> String {
@@ -51,9 +49,7 @@ impl Terminal {
Self {
width: term.size_checked().map(|(_, w)| w),
term,
prefix: env::var("TOPGRADE_PREFIX")
.map(|prefix| format!("({prefix}) "))
.unwrap_or_else(|_| String::new()),
prefix: env::var("TOPGRADE_PREFIX").map_or_else(|_| String::new(), |prefix| format!("({prefix}) ")),
set_title: true,
display_time: true,
desktop_notification: false,
@@ -61,15 +57,15 @@ impl Terminal {
}
fn set_desktop_notifications(&mut self, desktop_notifications: bool) {
self.desktop_notification = desktop_notifications
self.desktop_notification = desktop_notifications;
}
fn set_title(&mut self, set_title: bool) {
self.set_title = set_title
self.set_title = set_title;
}
fn display_time(&mut self, display_time: bool) {
self.display_time = display_time
self.display_time = display_time;
}
fn notify_desktop<P: AsRef<str>>(&self, message: P, timeout: Option<Duration>) {
@@ -144,7 +140,7 @@ impl Terminal {
self.term
.write_fmt(format_args!(
"{} {}",
style(format!("{key} failed:")).red().bold(),
style(format!("{}", t!("{key} failed:", key = key))).red().bold(),
message
))
.ok();
@@ -174,10 +170,15 @@ impl Terminal {
"{}: {}\n",
key,
match result {
StepResult::Success => format!("{}", style("OK").bold().green()),
StepResult::Failure => format!("{}", style("FAILED").bold().red()),
StepResult::Ignored => format!("{}", style("IGNORED").bold().yellow()),
StepResult::Skipped(reason) => format!("{}: {}", style("SKIPPED").bold().blue(), reason),
StepResult::Success => format!("{}", style(t!("OK")).bold().green()),
StepResult::Failure => format!("{}", style(t!("FAILED")).bold().red()),
StepResult::Ignored => format!("{}", style(t!("IGNORED")).bold().yellow()),
StepResult::SkippedMissingSudo => format!(
"{}: {}",
style(t!("SKIPPED")).bold().yellow(),
t!("Could not find sudo")
),
StepResult::Skipped(reason) => format!("{}: {}", style(t!("SKIPPED")).bold().blue(), reason),
}
))
.ok();
@@ -188,7 +189,7 @@ impl Terminal {
self.term
.write_fmt(format_args!(
"{}",
style(format!("{question} (y)es/(N)o",)).yellow().bold()
style(format!("{question} {}", t!("(Y)es/(N)o"))).yellow().bold()
))
.ok();
@@ -200,21 +201,22 @@ impl Terminal {
}
}
}
#[allow(unused_variables)]
fn should_retry(&mut self, interrupted: bool, step_name: &str) -> eyre::Result<bool> {
fn should_retry(&mut self, step_name: &str) -> eyre::Result<ShouldRetry> {
if self.width.is_none() {
return Ok(false);
return Ok(ShouldRetry::No);
}
if self.set_title {
self.term.set_title("Topgrade - Awaiting user");
self.term.set_title(format!("Topgrade - {}", t!("Awaiting user")));
}
if self.desktop_notification {
self.notify_desktop(format!("{step_name} failed"), None);
self.notify_desktop(format!("{}", t!("{step_name} failed", step_name = step_name)), None);
}
let prompt_inner = style(format!("{}Retry? (y)es/(N)o/(s)hell/(q)uit", self.prefix))
let prompt_inner = style(format!("{}{}", self.prefix, t!("Retry? (y)es/(N)o/(s)hell/(q)uit")))
.yellow()
.bold();
@@ -222,22 +224,25 @@ impl Terminal {
let answer = loop {
match self.term.read_key() {
Ok(Key::Char('y')) | Ok(Key::Char('Y')) => break Ok(true),
Ok(Key::Char('s')) | Ok(Key::Char('S')) => {
println!("\n\nDropping you to shell. Fix what you need and then exit the shell.\n");
Ok(Key::Char('y' | 'Y')) => break Ok(ShouldRetry::Yes),
Ok(Key::Char('s' | 'S')) => {
println!(
"\n\n{}\n",
t!("Dropping you to shell. Fix what you need and then exit the shell.")
);
if let Err(err) = run_shell().context("Failed to run shell") {
self.term.write_fmt(format_args!("{err:?}\n{prompt_inner}")).ok();
} else {
break Ok(true);
break Ok(ShouldRetry::Yes);
}
}
Ok(Key::Char('n')) | Ok(Key::Char('N')) | Ok(Key::Enter) => break Ok(false),
Ok(Key::Char('n' | 'N') | Key::Enter) => break Ok(ShouldRetry::No),
Err(e) => {
error!("Error reading from terminal: {}", e);
break Ok(false);
break Ok(ShouldRetry::No);
}
Ok(Key::Char('q')) | Ok(Key::Char('Q')) => {
return Err(io::Error::from(io::ErrorKind::Interrupted)).context("Quit from user input")
Ok(Key::Char('q' | 'Q')) => {
break Ok(ShouldRetry::Quit);
}
_ => (),
}
@@ -253,37 +258,44 @@ impl Terminal {
}
}
#[derive(Clone, Copy)]
pub enum ShouldRetry {
Yes,
No,
Quit,
}
impl Default for Terminal {
fn default() -> Self {
Self::new()
}
}
pub fn should_retry(interrupted: bool, step_name: &str) -> eyre::Result<bool> {
TERMINAL.lock().unwrap().should_retry(interrupted, step_name)
pub fn should_retry(step_name: &str) -> eyre::Result<ShouldRetry> {
TERMINAL.lock().unwrap().should_retry(step_name)
}
pub fn print_separator<P: AsRef<str>>(message: P) {
TERMINAL.lock().unwrap().print_separator(message)
TERMINAL.lock().unwrap().print_separator(message);
}
#[allow(dead_code)]
pub fn print_error<P: AsRef<str>, Q: AsRef<str>>(key: Q, message: P) {
TERMINAL.lock().unwrap().print_error(key, message)
TERMINAL.lock().unwrap().print_error(key, message);
}
#[allow(dead_code)]
pub fn print_warning<P: AsRef<str>>(message: P) {
TERMINAL.lock().unwrap().print_warning(message)
TERMINAL.lock().unwrap().print_warning(message);
}
#[allow(dead_code)]
pub fn print_info<P: AsRef<str>>(message: P) {
TERMINAL.lock().unwrap().print_info(message)
TERMINAL.lock().unwrap().print_info(message);
}
pub fn print_result<P: AsRef<str>>(key: P, result: &StepResult) {
TERMINAL.lock().unwrap().print_result(key, result)
TERMINAL.lock().unwrap().print_result(key, result);
}
/// Tells whether the terminal is dumb.
@@ -312,7 +324,7 @@ pub fn prompt_yesno(question: &str) -> Result<bool, io::Error> {
}
pub fn notify_desktop<P: AsRef<str>>(message: P, timeout: Option<Duration>) {
TERMINAL.lock().unwrap().notify_desktop(message, timeout)
TERMINAL.lock().unwrap().notify_desktop(message, timeout);
}
pub fn display_time(display_time: bool) {

View File

@@ -5,13 +5,14 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use color_eyre::eyre::Result;
use rust_i18n::t;
use tracing::{debug, error};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::reload::{Handle, Layer};
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{fmt, Registry};
use tracing_subscriber::{registry, EnvFilter};
use tracing_subscriber::{EnvFilter, registry};
use tracing_subscriber::{Registry, fmt};
use crate::command::CommandExt;
use crate::config::DEFAULT_LOG_LEVEL;
@@ -51,7 +52,11 @@ where
debug!("Path {:?} exists", self.as_ref());
Ok(self)
} else {
Err(SkipStep(format!("Path {:?} doesn't exist", self.as_ref())).into())
Err(SkipStep(format!(
"{}",
t!("Path {path} doesn't exist", path = format!("{:?}", self.as_ref()))
))
.into())
}
}
}
@@ -81,7 +86,7 @@ pub fn editor() -> Vec<String> {
env::var("EDITOR")
.unwrap_or_else(|_| String::from(if cfg!(windows) { "notepad" } else { "vi" }))
.split_whitespace()
.map(|s| s.to_owned())
.map(std::borrow::ToOwned::to_owned)
.collect()
}
@@ -92,9 +97,14 @@ pub fn require<T: AsRef<OsStr> + Debug>(binary_name: T) -> Result<PathBuf> {
Ok(path)
}
Err(e) => match e {
which_crate::Error::CannotFindBinaryPath => {
Err(SkipStep(format!("Cannot find {:?} in PATH", &binary_name)).into())
}
which_crate::Error::CannotFindBinaryPath => Err(SkipStep(format!(
"{}",
t!(
"Cannot find {binary_name} in PATH",
binary_name = format!("{:?}", &binary_name)
)
))
.into()),
_ => {
panic!("Detecting {:?} failed: {}", &binary_name, e);
}
@@ -102,6 +112,29 @@ pub fn require<T: AsRef<OsStr> + Debug>(binary_name: T) -> Result<PathBuf> {
}
}
pub fn require_one<T: AsRef<OsStr> + Debug>(binary_names: impl IntoIterator<Item = T>) -> Result<PathBuf> {
let mut failed_bins = Vec::new();
for bin in binary_names {
match require(&bin) {
Ok(path) => return Ok(path),
Err(_) => failed_bins.push(bin),
}
}
Err(SkipStep(format!(
"{}",
t!(
"Cannot find any of {binary_names} in PATH",
binary_names = failed_bins
.iter()
.map(|bin| format!("{:?}", bin))
.collect::<Vec<_>>()
.join(", ")
)
))
.into())
}
#[allow(dead_code)]
pub fn require_option<T>(option: Option<T>, cause: String) -> Result<T> {
if let Some(value) = option {
@@ -118,24 +151,40 @@ pub fn string_prepend_str(string: &mut String, s: &str) {
*string = new_string;
}
#[cfg(target_family = "unix")]
#[cfg(unix)]
pub fn hostname() -> Result<String> {
match nix::unistd::gethostname() {
Ok(os_str) => Ok(os_str
.into_string()
.map_err(|_| SkipStep("Failed to get a UTF-8 encoded hostname".into()))?),
.map_err(|_| SkipStep(t!("Failed to get a UTF-8 encoded hostname").into()))?),
Err(e) => Err(e.into()),
}
}
#[cfg(target_family = "windows")]
#[cfg(windows)]
pub fn hostname() -> Result<String> {
Command::new("hostname")
.output_checked_utf8()
.map_err(|err| SkipStep(format!("Failed to get hostname: {err}")).into())
.map_err(|err| SkipStep(t!("Failed to get hostname: {err}", err = err).to_string()).into())
.map(|output| output.stdout.trim().to_owned())
}
#[cfg(unix)]
pub fn is_elevated() -> bool {
let euid = nix::unistd::Uid::effective();
debug!("Running with euid: {euid}");
euid.is_root()
}
#[cfg(windows)]
pub fn is_elevated() -> bool {
let elevated = is_elevated::is_elevated();
if elevated {
debug!("Detected elevated process");
}
elevated
}
pub mod merge_strategies {
use merge::Merge;
@@ -146,7 +195,7 @@ pub mod merge_strategies {
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));
let _ = left.replace(right_vec);
}
} else {
*left = right;
@@ -169,7 +218,7 @@ pub mod merge_strategies {
where
T: Merge,
{
if let Some(ref mut left_inner) = left {
if let Some(left_inner) = left {
if let Some(right_inner) = right {
left_inner.merge(right_inner);
}
@@ -179,7 +228,7 @@ pub mod merge_strategies {
}
pub fn commands_merge_opt(left: &mut Option<Commands>, right: Option<Commands>) {
if let Some(ref mut left_inner) = left {
if let Some(left_inner) = left {
if let Some(right_inner) = right {
left_inner.extend(right_inner);
}
@@ -189,15 +238,11 @@ pub mod merge_strategies {
}
}
// Skip causes
// TODO: Put them in a better place when we have more of them
pub const REQUIRE_SUDO: &str = "Require sudo or counterpart but not found, skip";
/// 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
/// is installed as well as a `python3` shim. Shim is invocable, but when you
/// execute it, the Microsoft App Store will be launched instead of a Python
/// shell.
///
@@ -218,11 +263,11 @@ pub fn check_is_python_2_or_shim(python: PathBuf) -> Result<PathBuf> {
.parse::<u32>()
.expect("Major version should be a valid number");
if major_version == 2 {
return Err(SkipStep(format!("{} is a Python 2, skip.", python.display())).into());
return Err(SkipStep(t!("{python} is a Python 2, skip.", python = python.display()).to_string()).into());
}
} else {
// No version number, is a shim
return Err(SkipStep(format!("{} is a Python shim, skip.", python.display())).into());
return Err(SkipStep(t!("{python} is a Python shim, skip.", python = python.display()).to_string()).into());
}
Ok(python)
@@ -270,3 +315,15 @@ pub fn install_color_eyre() -> Result<()> {
.display_location_section(true)
.install()
}
/// Macro to construct an error message for when the output of a command is unexpected.
#[macro_export]
macro_rules! output_changed_message {
($command:expr, $message:expr) => {
format!(
"The output of `{}` changed: {}. This is not your fault, this is an issue in Topgrade. Please open an issue at: https://github.com/topgrade-rs/topgrade/issues/new?template=bug_report.md",
$command,
$message,
)
};
}

10
translate.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
## Translate the given string into $langs using translate-shell, outputting to the yaml structure expected for locales/app.yml
langs="en lt es fr zh_CN zh_TW de"
printf "\"%s\":\n" "$@"
for lang in $langs; do
result=$(trans -brief -no-auto -s en -t "${lang/_/-/}" "$@")
printf " %s: \"%s\"\n" "$lang" "$result"
done