├── .cspell.json ├── .github ├── dependabot.yml └── workflows │ ├── dependabot-auto-merge.yml │ ├── docs.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── .taplo.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── logo.png ├── build.rs ├── crates └── pacaptr-macros │ ├── Cargo.toml │ └── src │ ├── compat_table.rs │ ├── lib.rs │ └── test_dsl.rs ├── docs └── CONTRIBUTING.md ├── rustfmt.toml ├── src ├── cmd.rs ├── config.rs ├── error.rs ├── exec.rs ├── lib.rs ├── main.rs ├── pm.rs ├── pm │ ├── apk.rs │ ├── apt.rs │ ├── brew.rs │ ├── choco.rs │ ├── conda.rs │ ├── dnf.rs │ ├── emerge.rs │ ├── pip.rs │ ├── pkcon.rs │ ├── port.rs │ ├── scoop.rs │ ├── tlmgr.rs │ ├── unknown.rs │ ├── winget.rs │ ├── xbps.rs │ └── zypper.rs ├── print.rs └── print │ ├── prompt.rs │ └── style.rs └── tests ├── apk.rs ├── apt.rs ├── brew.rs ├── choco.rs ├── common.rs ├── conda.rs ├── dnf.rs ├── emerge.rs ├── pip.rs ├── pkcon.rs ├── port.rs ├── scoop.rs ├── winget.rs ├── xbps.rs └── zypper.rs /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en", 4 | "words": [ 5 | "aarch", 6 | "algos", 7 | "binutils", 8 | "Bohnenkamper", 9 | "bools", 10 | "chmod", 11 | "choco", 12 | "chocolateys", 13 | "clippy", 14 | "concatcp", 15 | "conda", 16 | "confy", 17 | "consts", 18 | "deinstall", 19 | "devel", 20 | "dialoguer", 21 | "distro", 22 | "dtolnay", 23 | "eclean", 24 | "equery", 25 | "Exherbo", 26 | "formatcp", 27 | "goarch", 28 | "gsudo", 29 | "impls", 30 | "indoc", 31 | "iproute", 32 | "itertools", 33 | "libzypp", 34 | "litrs", 35 | "macos", 36 | "mkdir", 37 | "mockpm", 38 | "nocache", 39 | "noconfirm", 40 | "nuget", 41 | "nupkg", 42 | "olegtarasov", 43 | "pacaptr", 44 | "phinx", 45 | "pkcon", 46 | "Pkgng", 47 | "pkgver", 48 | "printf", 49 | "procursus", 50 | "proto", 51 | "qfile", 52 | "qlist", 53 | "qsearch", 54 | "rdepends", 55 | "refreshenv", 56 | "repoquery", 57 | "rmtree", 58 | "rustfmt", 59 | "saxutils", 60 | "sccc", 61 | "SDKROOT", 62 | "strat", 63 | "struct", 64 | "structopt", 65 | "subcmd", 66 | "svenstaro", 67 | "Swatinem", 68 | "Swupd", 69 | "sympy", 70 | "sysupgrade", 71 | "tasksel", 72 | "Tazpkg", 73 | "thiserror", 74 | "Tlmgr", 75 | "unmerge", 76 | "Unmerging", 77 | "unistd", 78 | "untrusted", 79 | "verif", 80 | "xcrun", 81 | "xshell", 82 | "xtask", 83 | "Zypper" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "cargo" 14 | directory: "/" 15 | schedule: 16 | interval: "monthly" 17 | groups: 18 | minors: 19 | update-types: 20 | - "minor" 21 | - "patch" 22 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: dependabot-auto-merge 2 | 3 | on: 4 | # https://github.com/dependabot/dependabot-core/issues/3253#issuecomment-852541544 5 | pull_request_target: 6 | 7 | jobs: 8 | auto-merge: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | # Hack: https://github.com/ahmadnassri/action-dependabot-auto-merge/issues/58#issuecomment-981520187 14 | token: ${{ secrets.TAP_GITHUB_TOKEN }} 15 | ref: ${{ github.event.pull_request.head.sha }} 16 | 17 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 18 | with: 19 | # https://docs.github.com/en/enterprise-server@3.6/code-security/dependabot/working-with-dependabot/managing-pull-requests-for-dependency-updates 20 | command: squash and merge 21 | target: patch 22 | github-token: ${{ secrets.TAP_GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | env: 19 | CARGO_TERM_COLOR: always 20 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 21 | 22 | jobs: 23 | build: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: Swatinem/rust-cache@v2 28 | - uses: dtolnay/rust-toolchain@nightly 29 | - name: Create 30 | run: cargo doc --all-features --document-private-items --no-deps 31 | # https://dev.to/deciduously/prepare-your-rust-api-docs-for-github-pages-2n5i 32 | - name: Patch `index.html` 33 | run: | 34 | echo '' > ./target/doc/index.html 35 | # https://github.com/actions/upload-pages-artifact#example-permissions-fix-for-linux 36 | - name: Fix permissions 37 | run: | 38 | chmod -c -R +rX "./target/doc" | while read line; do 39 | echo "::warning title=Invalid file permissions automatically fixed::$line" 40 | done 41 | - name: Upload artifact 42 | if: github.event_name == 'push' 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: ./target/doc 46 | 47 | deploy: 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | runs-on: ubuntu-latest 52 | if: github.event_name == 'push' 53 | needs: build 54 | steps: 55 | - name: Deploy to GitHub Pages 56 | id: deployment 57 | uses: actions/deploy-pages@v4 58 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | # pull_request: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | - "*" 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 14 | # https://users.rust-lang.org/t/cross-compiling-how-to-statically-link-glibc/83907/2 15 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-gnu-gcc 16 | 17 | jobs: 18 | create-release: 19 | name: Create GitHub release 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 24 | 25 | - name: Create release 26 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 27 | uses: softprops/action-gh-release@v2 28 | with: 29 | prerelease: ${{ contains(github.ref, '-') }} 30 | 31 | build-release: 32 | name: Build release binaries for ${{ matrix.target }} 33 | needs: [create-release] 34 | runs-on: ${{ matrix.os }} 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | include: 39 | - os: windows-latest 40 | target: x86_64-pc-windows-msvc 41 | - os: windows-latest 42 | target: aarch64-pc-windows-msvc 43 | - os: macos-latest 44 | target: x86_64-apple-darwin 45 | - os: macos-latest 46 | target: aarch64-apple-darwin 47 | - os: ubuntu-latest 48 | target: x86_64-unknown-linux-musl 49 | - os: ubuntu-latest 50 | target: aarch64-unknown-linux-musl 51 | steps: 52 | - uses: actions/checkout@v4 53 | 54 | - name: Setup extra build tools 55 | if: matrix.target == 'aarch64-unknown-linux-musl' 56 | run: | 57 | sudo apt-get update 58 | sudo apt-get install -y gcc-aarch64-linux-gnu 59 | 60 | - name: Setup Rust 61 | uses: dtolnay/rust-toolchain@stable 62 | with: 63 | targets: ${{ matrix.target }} 64 | 65 | - name: Build 66 | run: | 67 | cargo build --verbose --bin=pacaptr --release --locked --target=${{ matrix.target }} 68 | 69 | # https://github.com/vercel/turbo/blob/ea934d13038361c24a1f71cad3b490d6c0936f37/.github/workflows/turborepo-release.yml#L268-L272 70 | - name: Upload artifacts 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: pacaptr-${{ matrix.target }} 74 | path: target/${{ matrix.target }}/release/pacaptr* 75 | retention-days: 1 76 | 77 | publish: 78 | name: Publish via GoReleaser 79 | needs: [build-release] 80 | runs-on: ubuntu-latest 81 | container: 82 | # NOTE: It is also possible to install `choco` directly: 83 | # https://github.com/JetBrains/qodana-cli/blob/29134e654dc4878e0587f02338c5101bce327560/.github/workflows/release.yml#L19-L27 84 | image: chocolatey/choco:latest-linux 85 | steps: 86 | - name: Setup build essential 87 | run: | 88 | apt-get update && apt-get install -y git curl build-essential 89 | 90 | - uses: actions/checkout@v4 91 | 92 | - name: Setup Golang 93 | uses: actions/setup-go@v5 94 | with: 95 | go-version: stable 96 | 97 | # https://github.com/vercel/turbo/blob/ea934d13038361c24a1f71cad3b490d6c0936f37/.github/workflows/turborepo-release.yml#L306-L309 98 | - name: Download artifacts 99 | uses: actions/download-artifact@v4 100 | with: 101 | path: target/gh-artifacts 102 | 103 | # Here we use `mv` to map Rust targets to Golang ones. 104 | # https://github.com/vercel/turbo/blob/ea934d13038361c24a1f71cad3b490d6c0936f37/.github/workflows/turborepo-release.yml#L313-L318 105 | - name: Modify and inspect artifacts 106 | shell: bash 107 | run: | 108 | echo $PWD 109 | chown -R $(id -u):$(id -g) $PWD 110 | chmod -R 744 target/gh-artifacts 111 | ls -laR target/gh-artifacts 112 | mv -f target/gh-artifacts/pacaptr-x86_64-pc-windows-msvc target/gh-artifacts/pacaptr_windows_amd64 113 | mv -f target/gh-artifacts/pacaptr-aarch64-pc-windows-msvc target/gh-artifacts/pacaptr_windows_arm64 114 | mv -f target/gh-artifacts/pacaptr-x86_64-apple-darwin target/gh-artifacts/pacaptr_darwin_amd64 115 | mv -f target/gh-artifacts/pacaptr-aarch64-apple-darwin target/gh-artifacts/pacaptr_darwin_arm64 116 | mv -f target/gh-artifacts/pacaptr-x86_64-unknown-linux-musl target/gh-artifacts/pacaptr_linux_amd64 117 | mv -f target/gh-artifacts/pacaptr-aarch64-unknown-linux-musl target/gh-artifacts/pacaptr_linux_arm64 118 | echo '=======' 119 | ls -laR target/gh-artifacts 120 | 121 | # https://goreleaser.com/ci/actions/?h=github+act#usage 122 | - name: Publish via GoReleaser 123 | uses: goreleaser/goreleaser-action@v6 124 | with: 125 | distribution: goreleaser 126 | version: latest 127 | args: release --clean --verbose ${{ github.event_name == 'push' && contains(github.ref, 'refs/tags/') && ' ' || '--snapshot --skip=publish' }} 128 | env: 129 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 130 | TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} 131 | CHOCO_API_KEY: ${{ secrets.CHOCO_API_KEY }} 132 | 133 | # https://github.com/goreleaser/goreleaser/blob/2a3009757a8996cdcf2a77deb0e5fa413d1f2660/internal/pipe/chocolatey/chocolatey.go#L158 134 | - name: Inspect generated artifacts 135 | run: | 136 | ls -laR dist/ 137 | cat dist/pacaptr.choco/pacaptr.nuspec 138 | 139 | # https://github.com/goreleaser/goreleaser/blob/2a3009757a8996cdcf2a77deb0e5fa413d1f2660/internal/pipe/chocolatey/chocolatey.go#L201-L208 140 | # https://stackoverflow.com/a/75835172 141 | - name: Publish app on Chocolatey 142 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 143 | run: | 144 | choco push dist/*.nupkg --source https://push.chocolatey.org --api-key ${{ secrets.CHOCO_API_KEY }} --verbose ${{ contains(github.ref, '-') && '--noop' || ' ' }} 145 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 12 | # https://users.rust-lang.org/t/cross-compiling-how-to-statically-link-glibc/83907/2 13 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-gnu-gcc 14 | 15 | jobs: 16 | skip-check: 17 | continue-on-error: false 18 | runs-on: ubuntu-latest 19 | outputs: 20 | should_skip: ${{ steps.skip_check.outputs.should_skip }} 21 | steps: 22 | - id: skip_check 23 | uses: fkirc/skip-duplicate-actions@v5 24 | with: 25 | concurrent_skipping: same_content_newer 26 | do_not_skip: '["pull_request"]' 27 | 28 | lint: 29 | name: lint (${{ matrix.os }}) 30 | needs: skip-check 31 | if: ${{ needs.skip-check.outputs.should_skip != 'true' }} 32 | runs-on: ${{ matrix.os }} 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | include: 37 | - os: windows-latest 38 | - os: macos-latest 39 | - os: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Setup Rust 43 | if: ${{ steps.cache_build.outputs.cache-hit != 'true' }} 44 | uses: dtolnay/rust-toolchain@stable 45 | - name: Setup `cargo-binstall` and `taplo` 46 | uses: taiki-e/install-action@v2 47 | with: 48 | tool: taplo-cli 49 | - name: Check TOML format 50 | if: ${{ contains(matrix.os, 'ubuntu') }} 51 | run: | 52 | taplo fmt 53 | git diff --exit-code 54 | - name: Check Rust format 55 | if: ${{ contains(matrix.os, 'ubuntu') }} 56 | run: | 57 | cargo fmt --all --check 58 | - name: Lint Rust 59 | run: | 60 | cargo clippy --all-targets --all-features 61 | 62 | choco-test: 63 | runs-on: windows-latest 64 | needs: skip-check 65 | if: ${{ needs.skip-check.outputs.should_skip != 'true' }} 66 | steps: 67 | - uses: actions/checkout@v4 68 | - uses: dtolnay/rust-toolchain@stable 69 | with: 70 | targets: x86_64-pc-windows-msvc 71 | - name: Build and run tests 72 | env: 73 | CARGO_BUILD_TARGET: x86_64-pc-windows-msvc 74 | run: | 75 | cargo build --verbose 76 | cargo test --features=test tests 77 | cargo test --features=test choco -- --test-threads=1 78 | cargo test --features=test choco -- --ignored --test-threads=1 79 | 80 | scoop-winget-test: 81 | runs-on: windows-latest 82 | needs: skip-check 83 | if: ${{ needs.skip-check.outputs.should_skip != 'true' }} 84 | steps: 85 | - uses: actions/checkout@v4 86 | - uses: dtolnay/rust-toolchain@stable 87 | with: 88 | targets: x86_64-pc-windows-msvc 89 | - name: Install scoop 90 | shell: powershell 91 | run: | 92 | Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force 93 | iwr -useb 'https://raw.githubusercontent.com/scoopinstaller/install/master/install.ps1' -outfile 'install.ps1' 94 | .\install.ps1 -RunAsAdmin 95 | (Resolve-Path ~\scoop\shims).Path >> $Env:GITHUB_PATH 96 | - name: Verify scoop installation 97 | run: | 98 | Get-Command scoop 99 | powershell scoop help 100 | # Ironically, to install winget we need to install scoop first :D 101 | # See: https://github.com/microsoft/winget-cli/issues/1328#issuecomment-1208640211 102 | - name: Install winget 103 | shell: powershell 104 | run: scoop install winget 105 | - name: Verify winget installation 106 | run: | 107 | Get-Command winget 108 | winget --info 109 | - name: Build and run tests 110 | env: 111 | CARGO_BUILD_TARGET: x86_64-pc-windows-msvc 112 | run: | 113 | cargo build --verbose 114 | cargo test --features=test tests 115 | 116 | cargo test --features=test scoop 117 | cargo test --features=test winget 118 | 119 | cargo test --features=test scoop -- --ignored 120 | cargo test --features=test winget -- --ignored 121 | 122 | brew-test: 123 | runs-on: macos-latest 124 | needs: skip-check 125 | if: ${{ needs.skip-check.outputs.should_skip != 'true' }} 126 | steps: 127 | - uses: actions/checkout@v4 128 | - uses: dtolnay/rust-toolchain@stable 129 | with: 130 | targets: aarch64-apple-darwin 131 | - name: Build and run tests 132 | env: 133 | CARGO_BUILD_TARGET: aarch64-apple-darwin 134 | run: | 135 | cargo build --verbose 136 | cargo test --features=test tests 137 | cargo test --features=test brew 138 | cargo test --features=test brew -- --ignored 139 | 140 | port-test: 141 | runs-on: macos-latest 142 | needs: skip-check 143 | if: ${{ needs.skip-check.outputs.should_skip != 'true' }} 144 | steps: 145 | - uses: actions/checkout@v4 146 | - name: Get OS build 147 | run: | 148 | sw_vers > macos_build.txt 149 | cat macos_build.txt 150 | # https://github.com/actions/cache/issues/629#issuecomment-1189184648 151 | - name: Create gtar wrapper 152 | run: | 153 | mkdir target 154 | cat << 'EOF' > "target/gtar" 155 | #!/bin/bash 156 | set -x 157 | exec sudo /opt/homebrew/bin/gtar.orig "$@" 158 | EOF 159 | - name: Install gtar wrapper 160 | run: | 161 | sudo mv /opt/homebrew/bin/gtar /opt/homebrew/bin/gtar.orig 162 | sudo mv target/gtar /opt/homebrew/bin/gtar 163 | sudo chmod +x /opt/homebrew/bin/gtar 164 | /opt/homebrew/bin/gtar --usage 165 | - name: Cache MacPorts 166 | id: cache-macports 167 | uses: actions/cache@v4 168 | with: 169 | path: /opt/local/ 170 | key: ${{ runner.os }}-macports-${{ hashFiles('macos_build.txt') }} 171 | - name: Restore MacPorts PATH 172 | if: steps.cache-macports.outputs.cache-hit == 'true' 173 | run: echo "/opt/local/bin" >> "$GITHUB_PATH" 174 | - name: Install MacPorts 175 | if: steps.cache-macports.outputs.cache-hit != 'true' 176 | run: | 177 | curl -LO https://raw.githubusercontent.com/GiovanniBussi/macports-ci/master/macports-ci 178 | source ./macports-ci install 179 | sudo port install wget 180 | port installed 181 | - uses: dtolnay/rust-toolchain@stable 182 | with: 183 | targets: aarch64-apple-darwin 184 | - name: Build and run tests 185 | env: 186 | CARGO_BUILD_TARGET: aarch64-apple-darwin 187 | run: | 188 | cargo build --verbose 189 | cargo test --features=test tests 190 | cargo test --features=test port 191 | cargo test --features=test port -- --ignored 192 | 193 | apt-test: 194 | runs-on: ubuntu-latest 195 | needs: skip-check 196 | if: ${{ needs.skip-check.outputs.should_skip != 'true' }} 197 | steps: 198 | - uses: actions/checkout@v4 199 | - uses: dtolnay/rust-toolchain@stable 200 | with: 201 | targets: x86_64-unknown-linux-musl 202 | - name: Build and run tests 203 | env: 204 | CARGO_BUILD_TARGET: x86_64-unknown-linux-musl 205 | run: | 206 | cargo build --verbose 207 | cargo test --features=test tests 208 | cargo test --features=test apt 209 | cargo test --features=test apt -- --ignored 210 | 211 | dnf-test: 212 | runs-on: ubuntu-latest 213 | needs: skip-check 214 | if: ${{ needs.skip-check.outputs.should_skip != 'true' }} 215 | container: 216 | image: fedora:latest 217 | steps: 218 | - uses: actions/checkout@v4 219 | - name: Setup extra build tools 220 | run: dnf install -y make automake gcc gcc-c++ kernel-devel 221 | - uses: dtolnay/rust-toolchain@stable 222 | with: 223 | targets: x86_64-unknown-linux-musl 224 | - name: Build and run tests 225 | env: 226 | CARGO_BUILD_TARGET: x86_64-unknown-linux-musl 227 | run: | 228 | cargo build --verbose 229 | cargo test --features=test tests 230 | cargo test --features=test dnf 231 | cargo test --features=test dnf -- --ignored 232 | 233 | emerge-test: 234 | runs-on: ubuntu-latest 235 | needs: skip-check 236 | if: ${{ needs.skip-check.outputs.should_skip != 'true' }} 237 | container: 238 | image: gentoo/stage3 239 | steps: 240 | - uses: actions/checkout@v4 241 | - name: Setup extra build tools 242 | run: | 243 | # `pacaptr -Ss` might fail without this line. 244 | emerge --sync || true 245 | emerge curl 246 | - uses: dtolnay/rust-toolchain@stable 247 | with: 248 | targets: x86_64-unknown-linux-musl 249 | - name: Build and run tests 250 | env: 251 | CARGO_BUILD_TARGET: x86_64-unknown-linux-musl 252 | run: | 253 | cargo build --verbose 254 | cargo test --features=test tests 255 | cargo test --features=test emerge 256 | cargo test --features=test emerge -- --ignored 257 | 258 | xbps-test: 259 | runs-on: ubuntu-latest 260 | needs: skip-check 261 | if: ${{ needs.skip-check.outputs.should_skip != 'true' }} 262 | container: 263 | image: ghcr.io/void-linux/void-glibc-full:latest 264 | steps: 265 | - name: Setup extra build tools 266 | run: | 267 | xbps-install -y -Su || (xbps-install -y -u xbps && xbps-install -y -Su) 268 | xbps-install -y base-devel curl bash 269 | - uses: actions/checkout@v4 270 | - uses: dtolnay/rust-toolchain@stable 271 | with: 272 | targets: x86_64-unknown-linux-musl 273 | - name: Build and run tests 274 | env: 275 | CARGO_BUILD_TARGET: x86_64-unknown-linux-musl 276 | run: | 277 | cargo build --verbose 278 | cargo test --features=test tests 279 | cargo test --features=test xbps 280 | cargo test --features=test xbps -- --ignored 281 | 282 | zypper-test: 283 | runs-on: ubuntu-latest 284 | needs: skip-check 285 | if: ${{ needs.skip-check.outputs.should_skip != 'true' }} 286 | container: 287 | image: registry.opensuse.org/opensuse/bci/rust:latest 288 | steps: 289 | - name: Setup extra build tools 290 | run: zypper install -y tar gzip curl gcc bash 291 | - uses: actions/checkout@v4 292 | - uses: dtolnay/rust-toolchain@stable 293 | with: 294 | targets: x86_64-unknown-linux-musl 295 | - name: Build and run tests 296 | env: 297 | CARGO_BUILD_TARGET: x86_64-unknown-linux-musl 298 | run: | 299 | cargo build --verbose 300 | cargo test --features=test tests 301 | cargo test --features=test zypper -- --test-threads=1 302 | cargo test --features=test zypper -- --ignored --test-threads=1 303 | 304 | apk-test: 305 | runs-on: ubuntu-latest 306 | needs: skip-check 307 | if: ${{ needs.skip-check.outputs.should_skip != 'true' }} 308 | container: 309 | image: rust:alpine 310 | steps: 311 | - name: Setup extra build tools 312 | run: | 313 | apk add -U build-base tar bash 314 | - uses: actions/checkout@v4 315 | - uses: dtolnay/rust-toolchain@stable 316 | with: 317 | targets: x86_64-unknown-linux-musl 318 | - name: Build and run tests 319 | env: 320 | RUSTFLAGS: "-C target-feature=-crt-static" 321 | CARGO_BUILD_TARGET: x86_64-unknown-linux-musl 322 | run: | 323 | cargo build --verbose 324 | cargo test --features=test tests 325 | cargo test --features=test apk 326 | cargo test --features=test apk -- --ignored 327 | 328 | pkcon-pip-conda-test: 329 | runs-on: ubuntu-latest 330 | needs: skip-check 331 | if: ${{ needs.skip-check.outputs.should_skip != 'true' }} 332 | steps: 333 | - uses: actions/checkout@v4 334 | - name: Setup extra build tools 335 | run: | 336 | sudo apt-get update 337 | sudo apt-get install -y packagekit packagekit-tools 338 | - uses: dtolnay/rust-toolchain@stable 339 | with: 340 | targets: x86_64-unknown-linux-musl 341 | - name: Build and run tests 342 | env: 343 | CARGO_BUILD_TARGET: x86_64-unknown-linux-musl 344 | run: | 345 | cargo build --verbose 346 | 347 | cargo test --features=test pkcon 348 | cargo test --features=test pip 349 | cargo test --features=test conda 350 | 351 | cargo test --features=test pkcon -- --ignored 352 | cargo test --features=test pip -- --ignored 353 | cargo test --features=test conda -- --ignored 354 | 355 | # https://github.com/PyO3/pyo3/blob/42601f3af94242b017402b763a495798a92da8f8/.github/workflows/ci.yml#L452-L472 356 | conclusion: 357 | needs: 358 | - lint 359 | - choco-test 360 | - scoop-winget-test 361 | - brew-test 362 | - port-test 363 | - apt-test 364 | - dnf-test 365 | - emerge-test 366 | - xbps-test 367 | - zypper-test 368 | - apk-test 369 | - pkcon-pip-conda-test 370 | if: always() 371 | runs-on: ubuntu-latest 372 | steps: 373 | - name: Result 374 | run: | 375 | jq -C <<< "${needs}" 376 | # Check if all needs were successful or skipped. 377 | "$(jq -r 'all(.result as $result | (["success", "skipped"] | contains([$result])))' <<< "${needs}")" 378 | env: 379 | needs: ${{ toJson(needs) }} 380 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ** macOS ** 2 | .DS_Store 3 | 4 | # ** Golang ** 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | *.code-workspace 21 | 22 | # ** Rust ** 23 | # Generated by Cargo 24 | # will have compiled files and executables 25 | /target/ 26 | 27 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 28 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 29 | # Cargo.lock 30 | 31 | # These are backup files generated by rustfmt 32 | **/*.rs.bk 33 | 34 | # ** IDE ** 35 | .vscode/ 36 | 37 | # ** Dist ** 38 | # Choco files that should get ignored 39 | ignore.* 40 | 41 | # Generated by GoReleaser 42 | dist/ 43 | 44 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Check the documentation at https://goreleaser.com 2 | 3 | # Adapted from https://github.com/LGUG2Z/komorebi/blob/e240bc770619fa7c1f311b8a376551f2dde8a2d7/.goreleaser.yml 4 | version: 2 5 | project_name: pacaptr 6 | 7 | before: 8 | hooks: 9 | - bash -c 'echo "package main; func main() { panic(0xdeadbeef) }" > dummy.go' 10 | 11 | builds: 12 | - id: pacaptr 13 | binary: pacaptr 14 | main: dummy.go 15 | goos: 16 | - linux 17 | - windows 18 | - darwin 19 | goarch: 20 | - arm64 21 | - amd64 22 | hooks: 23 | # Actually override the release binary. 24 | post: bash -c 'mv -f target/gh-artifacts/{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}/{{ .Name }} {{ .Path }}' 25 | 26 | universal_binaries: 27 | - replace: true 28 | 29 | archives: 30 | - format: tar.gz 31 | name_template: >- 32 | {{ .ProjectName }}- 33 | {{- .Os }}- 34 | {{- if eq .Arch "all" }}universal2 35 | {{- else if eq .Arch "386" }}i386 36 | {{- else }}{{ .Arch }}{{ end }} 37 | {{- if .Arm }}v{{ .Arm }}{{ end }} 38 | # Use zip for windows archives 39 | format_overrides: 40 | - goos: windows 41 | format: zip 42 | 43 | checksum: 44 | name_template: "checksums.txt" 45 | 46 | release: 47 | prerelease: auto 48 | 49 | changelog: 50 | sort: asc 51 | filters: 52 | exclude: 53 | - "^test" 54 | - "^chore" 55 | 56 | brews: 57 | # https://goreleaser.com/customization/homebrew/ 58 | - homepage: https://github.com/rami3l/pacaptr 59 | description: Pacman-like syntax wrapper for many package managers. 60 | license: GPL-3.0-only 61 | 62 | directory: Formula 63 | commit_msg_template: "feat(formula): add `{{ .ProjectName }}` {{ .Tag }}" 64 | 65 | custom_block: | 66 | head "https://github.com/rami3l/pacaptr.git" 67 | 68 | head do 69 | depends_on "rust" => :build 70 | end 71 | 72 | install: | 73 | if build.head? then 74 | system "cargo", "install", *std_cargo_args 75 | else 76 | bin.install "pacaptr" 77 | end 78 | 79 | test: | 80 | system "#{bin}/pacaptr --help" 81 | 82 | skip_upload: auto 83 | 84 | # https://github.com/goreleaser/goreleaser/blob/a0f0d01a8143913cde72ebc1248abef089ae9b27/.goreleaser.yaml#L211 85 | repository: 86 | owner: rami3l 87 | name: homebrew-tap 88 | branch: "{{.ProjectName}}-{{.Version}}" 89 | token: "{{ .Env.TAP_GITHUB_TOKEN }}" 90 | pull_request: 91 | enabled: true 92 | base: 93 | owner: rami3l 94 | name: homebrew-tap 95 | branch: master 96 | 97 | chocolateys: 98 | - name: pacaptr 99 | package_source_url: https://github.com/rami3l/pacaptr 100 | owners: Rami3L 101 | 102 | # == SOFTWARE SPECIFIC SECTION == 103 | title: pacaptr (Install) 104 | authors: Rami3L 105 | project_url: https://github.com/rami3l/pacaptr 106 | copyright: 2020 Rami3L 107 | license_url: https://opensource.org/license/gpl-3-0 108 | require_license_acceptance: false 109 | project_source_url: https://github.com/rami3l/pacaptr 110 | bug_tracker_url: https://github.com/rami3l/pacaptr/issues 111 | tags: pacaptr pacman 112 | summary: Pacman-like syntax wrapper for many package managers. 113 | release_notes: "https://github.com/rami3l/pacaptr/releases/tag/v{{ .Version }}" 114 | description: | 115 | # pacaptr 116 | 117 | `pacaptr` is a Rust port of [icy/pacapt], a wrapper for many package managers with pacman-style command syntax. 118 | 119 | Run `pacaptr -Syu` on the OS of your choice! 120 | 121 | # == PUBLISH SPECIFIC SECTION == 122 | source_repo: "https://push.chocolatey.org/" 123 | api_key: "{{ .Env.CHOCO_API_KEY }}" 124 | # Publishing is handled in `.github\workflows\publish.yml`. 125 | skip_publish: true 126 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | include = ["**/*.toml"] 2 | 3 | [[rule]] 4 | include = ["**/Cargo.toml"] 5 | keys = [ 6 | "build-dependencies", 7 | "dependencies", 8 | "dev-dependencies", 9 | "workspace.dependencies", 10 | "workspace.lints.clippy", 11 | "workspace.lints.rust", 12 | "workspace.lints.rustdoc", 13 | ] 14 | 15 | [rule.formatting] 16 | reorder_keys = true 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 2 | 3 | [workspace] 4 | resolver = "2" 5 | members = [".", "crates/*"] 6 | 7 | [workspace.package] 8 | version = "0.23.0" 9 | license = "GPL-3.0" 10 | edition = "2024" 11 | 12 | [workspace.dependencies] 13 | itertools = "0.14.0" 14 | once_cell = "1.21.3" 15 | regex = { version = "1.11.0", default-features = false, features = [ 16 | "std", 17 | "perf", 18 | "unicode-case", 19 | "unicode-perl", 20 | ] } 21 | 22 | # https://github.com/crate-ci/cargo-release/blob/master/docs/reference.md#configuration 23 | [workspace.metadata.release] 24 | allow-branch = ["master"] 25 | pre-release-commit-message = "dist: cut a new release" 26 | # https://github.com/crate-ci/cargo-release/issues/333 27 | tag = false 28 | 29 | [workspace.lints.rust] 30 | missing_copy_implementations = "warn" 31 | missing_debug_implementations = "warn" 32 | trivial_numeric_casts = "warn" 33 | unsafe_code = "forbid" 34 | unused_allocation = "warn" 35 | 36 | [workspace.lints.clippy] 37 | dbg_macro = "warn" 38 | nursery = "warn" 39 | pedantic = "warn" 40 | todo = "warn" 41 | 42 | [workspace.lints.rustdoc] 43 | broken_intra_doc_links = "warn" 44 | 45 | [package] 46 | name = "pacaptr" 47 | version.workspace = true 48 | license.workspace = true 49 | edition.workspace = true 50 | homepage = "https://github.com/rami3l/pacaptr" 51 | repository = "https://github.com/rami3l/pacaptr" 52 | description = "Pacman-like syntax wrapper for many package managers." 53 | readme = "README.md" 54 | 55 | keywords = ["package-management"] 56 | categories = ["command-line-utilities"] 57 | 58 | include = ["LICENSE", "Cargo.toml", "src/**/*.rs", "build.rs"] 59 | 60 | [package.metadata.docs.rs] 61 | all-features = true 62 | 63 | [package.metadata.release] 64 | # https://github.com/crate-ci/cargo-release/issues/333 65 | tag = true 66 | tag-message = "" 67 | 68 | [package.metadata.binstall] 69 | bin-dir = "{ bin }{ binary-ext }" 70 | 71 | [package.metadata.binstall.overrides] 72 | x86_64-apple-darwin.pkg-url = "{ repo }/releases/download/v{ version }/{ name }-darwin-universal2{ archive-suffix }" 73 | aarch64-apple-darwin.pkg-url = "{ repo }/releases/download/v{ version }/{ name }-darwin-universal2{ archive-suffix }" 74 | x86_64-pc-windows-msvc = { pkg-url = "{ repo }/releases/download/v{ version }/{ name }-windows-amd64{ archive-suffix }", pkg-fmt = "zip" } 75 | aarch64-pc-windows-msvc = { pkg-url = "{ repo }/releases/download/v{ version }/{ name }-windows-arm64{ archive-suffix }", pkg-fmt = "zip" } 76 | x86_64-unknown-linux-musl.pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-amd64{ archive-suffix }" 77 | aarch64-unknown-linux-musl.pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-arm64{ archive-suffix }" 78 | 79 | [package.metadata.deb] 80 | copyright = "2020, Rami3L" 81 | maintainer = "Rami3L " 82 | # license-file = ["LICENSE", "4"] 83 | assets = [ 84 | [ 85 | "target/release/pacaptr", 86 | "usr/bin/", 87 | "755", 88 | ], 89 | [ 90 | "README.md", 91 | "usr/share/doc/pacaptr/README", 92 | "644", 93 | ], 94 | ] 95 | depends = "$auto" 96 | extended-description = "Pacman-like syntax wrapper for many package managers." 97 | priority = "optional" 98 | section = "utility" 99 | 100 | [build-dependencies] 101 | built = { version = "0.8.0", features = ["git2"] } 102 | 103 | [dev-dependencies] 104 | xshell = "0.2.6" 105 | 106 | [dependencies] 107 | async-trait = "0.1.88" 108 | bytes = "1.10.1" 109 | clap = { version = "4.5.39", features = ["cargo", "derive"] } 110 | console = "0.15.11" 111 | ctrlc = { version = "3.4.7", features = ["termination"] } 112 | dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } 113 | dirs-next = "2.0.0" 114 | figment = { version = "0.10.19", features = ["env", "toml"] } 115 | futures = { version = "0.3.30", default-features = false, features = ["std"] } 116 | indoc = "2.0.6" 117 | itertools = { workspace = true } 118 | macro_rules_attribute = "0.2.2" 119 | pacaptr-macros = { path = "crates/pacaptr-macros", version = "0.23.0" } 120 | paste = "1.0.15" 121 | regex = { workspace = true } 122 | serde = { version = "1.0.219", features = ["derive"] } 123 | tap = "1.0.1" 124 | thiserror = "2.0.12" 125 | thiserror-ext = "0.3.0" 126 | tokio = { version = "1.45.1", features = [ 127 | "io-std", 128 | "io-util", 129 | "macros", 130 | "process", 131 | "rt-multi-thread", 132 | "sync", 133 | ] } 134 | tokio-stream = "0.1.15" 135 | tokio-util = { version = "0.7.15", features = ["codec", "compat"] } 136 | tt-call = "1.0.9" 137 | which = "7.0.3" 138 | 139 | [target.'cfg(windows)'.dependencies] 140 | is_elevated = "0.1.2" 141 | 142 | [target.'cfg(unix)'.dependencies] 143 | nix = { version = "0.30.1", default-features = false, features = ["user"] } 144 | 145 | [features] 146 | test = ["pacaptr-macros/test"] 147 | 148 | [profile.release] 149 | codegen-units = 1 150 | debug = 0 151 | lto = true 152 | opt-level = "z" 153 | panic = "abort" 154 | strip = "symbols" 155 | 156 | [lints] 157 | workspace = true 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # pacaptr 4 | 5 | [![pacaptr](https://socialify.git.ci/rami3l/pacaptr/image?description=1&font=Inter&logo=https%3A%2F%2Fgithub.com%2Frami3l%2Fpacaptr%2Fblob%2Fmaster%2Fassets%2Flogo.png%3Fraw%3Dtrue&name=1&owner=1&pattern=Solid&theme=Light)](https://crates.io/crates/pacaptr) 6 | 7 | 10 | 11 | 14 | 15 | [![Crates.io](https://img.shields.io/crates/v/pacaptr?style=flat-square)](https://crates.io/crates/pacaptr) 16 | [![docs.rs](https://img.shields.io/docsrs/pacaptr?style=flat-square)](https://docs.rs/pacaptr) 17 | [![Private APIs](https://img.shields.io/badge/docs-private--apis-lightgrey?style=flat-square)](https://rami3l.github.io/pacaptr) 18 | [![License](https://img.shields.io/github/license/rami3l/pacaptr?style=flat-square)](LICENSE) 19 | 20 | `pac·apt·r`, or the _PACman AdaPTeR_, is a wrapper for many package managers that allows you to use pacman commands with them. 21 | 22 | Just set `pacman` as the alias of `pacaptr` on your non-Arch OS, and then you can run `pacman -Syu` wherever you like! 23 | 24 | ```smalltalk 25 | > pacaptr -S neofetch 26 | Pending `brew reinstall neofetch` 27 | Proceed with the previous command? · Yes 28 | Running `brew reinstall neofetch` 29 | ==> Downloading https://homebrew.bintray.com/bottles/neofetch-7.1.0 30 | ########################################################### 100.0% 31 | ==> Reinstalling neofetch 32 | ==> Pouring neofetch-7.1.0.big_sur.bottle.tar.gz 33 | 🍺 /usr/local/Cellar/neofetch/7.1.0: 6 files, 351.7KB 34 | ``` 35 | 36 | 42 | 43 | --- 44 | 45 | ## Why `pacaptr`? 46 | 47 | Coming from `Arch Linux` to `macOS`, I really like the idea of having an automated version of [Pacman Rosetta] for making common package managing tasks less of a travail thanks to the concise `pacman` syntax. 48 | 49 | That's why I decided to take inspiration from the existing `sh`-based [icy/pacapt] to make a new CLI tool in Rust for better portability (especially for Windows and macOS) and easier maintenance. 50 | 51 | ## Supported Package Managers 52 | 53 | `pacaptr` currently supports the following package managers (in order of precedence): 54 | 55 | - Windows 56 | - [`scoop`](#for-scoop) 57 | - [`choco`](#for-choco) 58 | - `winget` 59 | - macOS 60 | - [`brew`](#for-brew) 61 | - `port` 62 | - `apt` (through [Procursus]) 63 | - Linux 64 | - `apt` 65 | - `apk` 66 | - `dnf` 67 | - `emerge` 68 | - `xbps` 69 | - `zypper` 70 | - External: These are only available with the [`pacaptr --using `](#--using---pm) syntax. 71 | - `brew` 72 | - `conda` 73 | - [`pip`](#for-pip)/[`pip3`](#for-pip) 74 | - `pkcon` 75 | - `tlmgr` 76 | 77 | As for now, the precedence is still (unfortunately) hard-coded. For example, if both `scoop` and `choco` are installed, `scoop` will be the default. You can, however, edit the default package manager in your [config](#configuration). 78 | 79 | Please refer to the [compatibility table] for more details on which operations are supported. 80 | 81 | ## Installation 82 | 83 | 84 | > **Note** 85 | > [We need your help](https://github.com/rami3l/pacaptr/issues/5) to achieve binary distribution of `pacaptr` on more platforms! 86 | 87 | ### Brew 88 | 89 | [![Tap Updated](https://img.shields.io/github/last-commit/rami3l/homebrew-tap/master?style=flat-square&label=tap%20updated)](https://github.com/rami3l/homebrew-tap) 90 | 91 | ```bash 92 | brew install rami3l/tap/pacaptr 93 | ``` 94 | 95 | ### Scoop 96 | 97 | [![Scoop Version](https://img.shields.io/scoop/v/pacaptr?bucket=extras&style=flat-square)](https://scoop.sh/#/apps?q=pacaptr&o=true) 98 | 99 | ```powershell 100 | scoop bucket add extras 101 | scoop install pacaptr 102 | ``` 103 | 104 | ### Choco 105 | 106 | [![Chocolatey Version](https://img.shields.io/chocolatey/v/pacaptr?style=flat-square)](https://community.chocolatey.org/packages/pacaptr) 107 | [![Chocolatey Downloads](https://img.shields.io/chocolatey/dt/pacaptr?style=flat-square)](https://community.chocolatey.org/packages/pacaptr) 108 | 109 | ```powershell 110 | choco install pacaptr 111 | ``` 112 | 113 | ### Cargo 114 | 115 | [![Cargo Version](https://img.shields.io/crates/v/pacaptr?style=flat-square)](https://crates.io/crates/pacaptr) 116 | [![Cargo Downloads](https://img.shields.io/crates/d/pacaptr?style=flat-square)](https://crates.io/crates/pacaptr) 117 | 118 | If you have installed [`cargo-binstall`], the fastest way of installing `pacaptr` via `cargo` is by running: 119 | 120 | ```bash 121 | cargo binstall pacaptr 122 | ``` 123 | 124 | To build and install the release version from crates.io: 125 | 126 | ```bash 127 | cargo install pacaptr 128 | ``` 129 | 130 | To build and install the `master` version from GitHub: 131 | 132 | ```bash 133 | cargo install pacaptr --git https://github.com/rami3l/pacaptr.git 134 | ``` 135 | 136 | For those who are interested, it is also possible to build and install from your local repo: 137 | 138 | ```bash 139 | git clone https://github.com/rami3l/pacaptr.git && cd pacaptr 140 | cargo install --path . 141 | # The output path is usually `$HOME/.cargo/bin/pacaptr`. 142 | ``` 143 | 144 | To uninstall: 145 | 146 | ```bash 147 | cargo uninstall pacaptr 148 | ``` 149 | 150 | For `Alpine Linux` users, `cargo build` might not work. Please try the following instead: 151 | 152 | ```bash 153 | RUSTFLAGS="-C target-feature=-crt-static" cargo build 154 | ``` 155 | 156 | ### Packaging for Debian 157 | 158 | ```bash 159 | cargo install cargo-deb 160 | cargo deb 161 | ``` 162 | 163 | ## Configuration 164 | 165 | The config file path is defined with the following precedence: 166 | 167 | - `$PACAPTR_CONFIG`, if it is set; 168 | - `$XDG_CONFIG_HOME/pacaptr/pacaptr.toml`, if `$XDG_CONFIG_HOME` is set; 169 | - `$HOME/.config/pacaptr/pacaptr.toml`. 170 | 171 | I decided not to trash user's `$HOME` without their permission, so: 172 | 173 | - If the user hasn't yet specified any path to look at, we will look for the config file in the default path. 174 | 175 | - If the config file is not present anyway, a default one will be loaded with `Default::default`, and no files will be written. 176 | 177 | - Any config item can be overridden by the corresponding `PACAPTR_*` environment variable. For example, `PACAPTR_NEEDED=false` is prioritized over `needed = true` in `pacaptr.toml`. 178 | 179 |
Example 180 | 181 | ```toml 182 | # This enforces the use of `install` instead of 183 | # `reinstall` in `pacaptr -S` 184 | needed = true 185 | 186 | # Explicitly set the default package manager 187 | default_pm = "choco" 188 | 189 | # dry_run = false 190 | # no_confirm = false 191 | # no_cache = false 192 | ``` 193 | 194 |
195 | 196 | ## Tips 197 | 198 | ### Universal 199 | 200 | #### `--using`, `--pm` 201 | 202 | Use this flag to explicitly specify the underlying package manager to be invoked. 203 | 204 | ```bash 205 | # Here we force the use of `choco`, 206 | # so the following output is platform-independent: 207 | pacaptr --using choco -Su --dryrun 208 | # Canceled: choco upgrade all 209 | ``` 210 | 211 | This can be useful when you are running Linux and you want to use `linuxbrew`, for example. In that case, you can `--using brew`. 212 | 213 | #### Automatic `sudo` invocation 214 | 215 | If you are not `root` and you wish to do something requiring `sudo`, `pacaptr` will do it for you by invoking `sudo -S`. 216 | 217 | This feature is currently available for `apk`, `apt`, `dnf`, `emerge`, `pkcon`, `port`, `xbps` and `zypper`. 218 | 219 | #### Extra flags support 220 | 221 | The flags after a `--` will be passed directly to the underlying package manager: 222 | 223 | ```bash 224 | pacaptr -h 225 | # USAGE: 226 | # pacaptr [FLAGS] [KEYWORDS]... [-- ...] 227 | 228 | pacaptr -S curl docker --dryrun -- --proxy=localhost:1234 229 | # Canceled: foo install curl --proxy=localhost:1234 230 | # Canceled: foo install docker --proxy=localhost:1234 231 | ``` 232 | 233 | Here `foo` is the name of your package manager. 234 | (The actual output is platform-specific, which largely depends on if `foo` can actually read the flags given.) 235 | 236 | #### `--dryrun`, `--dry-run` 237 | 238 | Use this flag to just print out the command to be executed 239 | (sometimes with a --dry-run flag to activate the package manager's dryrun option). 240 | 241 | `Pending` means that the command execution has been blocked by a prompt; `Canceled` means it has been canceled in a dry run; `Running` means that it has started running. 242 | 243 | Some query commands might still be run, but anything "big" should have been stopped from running, e.g. installation. 244 | For instance: 245 | 246 | ```bash 247 | # Nothing will be installed, 248 | # as `brew install curl` won't run: 249 | pacaptr -S curl --dryrun 250 | # Canceled: brew install curl 251 | 252 | # Nothing will be deleted here, 253 | # but `brew cleanup --dry-run` is actually running: 254 | pacaptr -Sc --dryrun 255 | # Running: brew cleanup --dry-run 256 | # .. (showing the files to be removed) 257 | 258 | # To remove the forementioned files, 259 | # run the command above again without `--dryrun`: 260 | pacaptr -Sc 261 | # Running: brew cleanup 262 | # .. (cleaning up) 263 | ``` 264 | 265 | #### `--yes`, `--noconfirm`, `--no-confirm` 266 | 267 | Use this flag to trigger the corresponding flag of your package manager (if possible) in order to answer "yes" to every incoming question. 268 | 269 | This option is useful when you don't want to be asked during installation, for example, but it can also be dangerous if you don't know what you're doing! 270 | 271 | #### `--nocache`, `--no-cache` 272 | 273 | Use this flag to remove cache after package installation. 274 | 275 | This option is useful when you want to reduce `Docker` image size, for example. 276 | 277 | ### Platform-Specific Tips 278 | 279 | #### For `brew` 280 | 281 | - Please note that `cask` is for `macOS` only. 282 | 283 | - Be careful when a formula and a cask share the same name, e.g. `docker`. 284 | 285 | ```bash 286 | pacaptr -Si docker | rg cask 287 | # => Warning: Treating docker as a formula. For the cask, use homebrew/cask/docker 288 | 289 | # Install the formula `docker` 290 | pacaptr -S docker 291 | 292 | # Install the cask `docker` 293 | pacaptr -S homebrew/cask/docker 294 | 295 | # Make homebrew treat all keywords as casks 296 | pacaptr -S docker -- --cask 297 | ``` 298 | 299 | #### For `scoop` 300 | 301 | - `pacaptr` launches a [`pwsh`](https://powershellexplained.com/2017-12-29-Powershell-what-is-pwsh/) subprocess to run `scoop`, or a `powershell` one if `pwsh` is not found in `$PATH`. Please make sure that you have set the right execution policy in the corresponding shell: 302 | 303 | ```pwsh 304 | Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser 305 | ``` 306 | 307 | #### For `choco` 308 | 309 | - Don't forget to run in an elevated shell! You can do this easily with tools like [gsudo]. 310 | 311 | #### For `pip` 312 | 313 | - Use `pacaptr --using pip3` if you want to run the `pip3` command. 314 | 315 | ### Feel Like Contributing? 316 | 317 | Sounds nice! Please let me take you to the [contributing guidelines](docs/CONTRIBUTING.md) :) 318 | 319 | [`cargo-binstall`]: https://github.com/cargo-bins/cargo-binstall 320 | [compatibility table]: https://rami3l.github.io/pacaptr/pacaptr/#compatibility-table 321 | [gsudo]: https://github.com/gerardog/gsudo 322 | [icy/pacapt]: https://github.com/icy/pacapt 323 | [pacman rosetta]: https://wiki.archlinux.org/index.php/Pacman/Rosetta 324 | [procursus]: https://github.com/ProcursusTeam/Procursus 325 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rami3l/pacaptr/c5d80e2d763966d3d719cb5d82c4a68183b05283/assets/logo.png -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | built::write_built_file().expect("failed to acquire build-time information"); 3 | } 4 | -------------------------------------------------------------------------------- /crates/pacaptr-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pacaptr-macros" 3 | version.workspace = true 4 | license.workspace = true 5 | edition.workspace = true 6 | 7 | homepage = "https://github.com/rami3l/pacaptr/tree/master/crates/pacaptr-macros" 8 | repository = "https://github.com/rami3l/pacaptr/tree/master/crates/pacaptr-macros" 9 | description = "Implementation of several macros used in pacaptr." 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | anyhow = "1.0.98" 16 | itertools = { workspace = true } 17 | litrs = { version = "0.4.1", optional = true } 18 | once_cell = { workspace = true } 19 | proc-macro2 = "1.0.95" 20 | quote = { version = "1.0.40", optional = true } 21 | regex = { workspace = true } 22 | syn = "2.0.101" 23 | tabled = "0.19.0" 24 | 25 | [features] 26 | test = ["dep:litrs", "dep:quote"] 27 | 28 | [lints] 29 | workspace = true 30 | -------------------------------------------------------------------------------- /crates/pacaptr-macros/src/compat_table.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, collections::BTreeMap, ffi::OsString, fmt::Debug, fs, path::Path, str::FromStr, 3 | sync::LazyLock, 4 | }; 5 | 6 | use anyhow::Context; 7 | use itertools::{Itertools, chain}; 8 | use proc_macro2::{Span, TokenStream}; 9 | use regex::Regex; 10 | use syn::{Error, Result}; 11 | use tabled::{Table, Tabled, settings::Style as TableStyle}; 12 | 13 | const PM_IMPL_DIR: &str = "src/pm/"; 14 | 15 | // We have to specify the length there (the elision is blocked by https://github.com/rust-lang/rfcs/pull/2545). 16 | // TODO: Fix this when the issue is resolved. 17 | const METHODS: [&str; 31] = [ 18 | "q", "qc", "qe", "qi", "qii", "qk", "ql", "qm", "qo", "qp", "qs", "qu", "r", "rn", "rns", "rs", 19 | "rss", "s", "sc", "scc", "sccc", "sg", "si", "sii", "sl", "ss", "su", "suy", "sw", "sy", "u", 20 | ]; 21 | 22 | /// Checks the implementation status of `pacman` commands in a specific file 23 | /// (eg. `homebrew.rs`). 24 | fn check_methods(file: &Path) -> anyhow::Result> { 25 | let bytes = fs::read(file)?; 26 | let contents = String::from_utf8(bytes)?; 27 | 28 | METHODS 29 | .iter() 30 | .map(|&method| { 31 | // A function definition (rg. `rs`) is written as follows: 32 | // `(async) fn rs(..) {..}` 33 | let found = Regex::new(&format!(r"fn\s+{method}\s*\("))?.is_match(&contents); 34 | Ok((method.to_owned(), found)) 35 | }) 36 | .try_collect() 37 | } 38 | 39 | struct CompatRow { 40 | fields: Vec, 41 | } 42 | 43 | impl Tabled for CompatRow { 44 | const LENGTH: usize = 1 + METHODS.len(); 45 | 46 | fn fields(&self) -> Vec> { 47 | self.fields.iter().map(|s| Cow::Owned(s.clone())).collect() 48 | } 49 | 50 | fn headers() -> Vec> { 51 | // `["Module", "q", "qc", "qe", ..]` 52 | static HEADERS: LazyLock>> = 53 | LazyLock::new(|| chain!(["Module"], METHODS).map_into().collect()); 54 | HEADERS.clone() 55 | } 56 | } 57 | 58 | fn make_table() -> anyhow::Result { 59 | let paths: Vec = fs::read_dir(PM_IMPL_DIR) 60 | .context("failed while reading PM_IMPL_DIR")? 61 | .map(|entry| entry.context("error while reading path")) 62 | .try_collect()?; 63 | 64 | let excluded_names = ["mod.rs", "unknown.rs"]; 65 | let impls: BTreeMap> = paths 66 | .iter() 67 | .filter(|entry| !excluded_names.iter().any(|&ex| ex == entry.file_name())) 68 | .map(|entry| check_methods(&entry.path()).map(|impl_| (entry.file_name(), impl_))) 69 | .try_collect()?; 70 | 71 | let make_row = |name, data| { 72 | let fields = chain!([name], data).map_into().collect_vec(); 73 | CompatRow { fields } 74 | }; 75 | 76 | let data: Vec<_> = impls 77 | .iter() 78 | .map(|(file, items)| { 79 | let data = METHODS.map(|method| { 80 | items 81 | .get(method) 82 | .expect("implementation details not registered") 83 | .then(|| "*") 84 | .unwrap_or("") 85 | }); 86 | file.to_str() 87 | .context("failed to convert `file: OsString` to `&str`") 88 | .map(|file| make_row(file, data)) 89 | }) 90 | .try_collect()?; 91 | 92 | let mut table = Table::new(data); 93 | Ok(format!( 94 | "\n\n\n{}\n\n\n", 95 | table.with(TableStyle::markdown()) 96 | )) 97 | } 98 | 99 | #[allow(clippy::module_name_repetitions)] 100 | pub fn compat_table_impl() -> Result { 101 | fn throw(e: &dyn Debug) -> Error { 102 | let msg = format!("{e:?}"); 103 | Error::new(Span::call_site(), msg) 104 | } 105 | 106 | let table = make_table().map_err(|e| throw(&e))?; 107 | let docstring = format!(r##"r#"{table}"#"##); 108 | Ok(TokenStream::from_str(&docstring)?) 109 | } 110 | -------------------------------------------------------------------------------- /crates/pacaptr-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod compat_table; 2 | #[cfg(feature = "test")] 3 | mod test_dsl; 4 | 5 | use anyhow::Result; 6 | use proc_macro::TokenStream; 7 | 8 | use crate::compat_table::compat_table_impl; 9 | #[cfg(feature = "test")] 10 | use crate::test_dsl::test_dsl_impl; 11 | 12 | /// A DSL (Domain-Specific Language) embedded in Rust, in order to simplify the 13 | /// form of smoke tests. 14 | /// 15 | /// This macro accepts the source of the Test DSL in a **string literal**. 16 | /// In this DSL, each line is called an `item`. We now support the following 17 | /// item types: 18 | /// - `in` item: Run command on `pacaptr`. 19 | /// - `in !` item: Run command with the system shell (`sh` on Unix,`powershell` 20 | /// on Windows). 21 | /// - `ou` item: Check the output of the **last** `in` or `in !` item above 22 | /// against a **regex** pattern. 23 | /// 24 | /// A comment in this DSL starts with a `#`. 25 | /// 26 | /// # Examples 27 | /// 28 | /// ```no_run 29 | /// #[test] 30 | /// #[ignore] 31 | /// fn apt_r_s() { 32 | /// test_dsl! { r##" 33 | /// # Refresh with `pacaptr -Sy`. 34 | /// in -Sy 35 | /// 36 | /// # Install `screen`. 37 | /// in -S screen --yes 38 | /// 39 | /// # Verify installation. 40 | /// in ! which screen 41 | /// ou ^/usr/bin/screen 42 | /// 43 | /// # Remove `screen` and verify the removal. 44 | /// in -R screen --yes 45 | /// in -Qi screen 46 | /// ou ^Status: deinstall 47 | /// "## } 48 | /// } 49 | /// ``` 50 | #[cfg(feature = "test")] 51 | #[proc_macro] 52 | pub fn test_dsl(input: TokenStream) -> TokenStream { 53 | use itertools::Itertools; 54 | use litrs::StringLit; 55 | use quote::quote; 56 | 57 | let input = input.into_iter().collect_vec(); 58 | if input.len() != 1 { 59 | let msg = format!( 60 | "argument must be a single string literal, but got {} tokens", 61 | input.len() 62 | ); 63 | return quote! { compile_error!(#msg) }.into(); 64 | } 65 | 66 | let string_lit = match StringLit::try_from(&input[0]) { 67 | // Error if the token is not a string literal 68 | Err(e) => return e.to_compile_error(), 69 | Ok(lit) => lit, 70 | }; 71 | 72 | res_token_stream(test_dsl_impl(string_lit.value())) 73 | } 74 | 75 | /// Generates the compatibility table as a docstring on the top of given input. 76 | #[proc_macro] 77 | pub fn compat_table(input: TokenStream) -> TokenStream { 78 | let res = 79 | compat_table_impl().map(|docstring| TokenStream::from_iter([docstring.into(), input])); 80 | res_token_stream(res) 81 | } 82 | 83 | fn res_token_stream(res: Result, syn::Error>) -> TokenStream { 84 | res.map_or_else(|e| e.to_compile_error().into(), Into::into) 85 | } 86 | -------------------------------------------------------------------------------- /crates/pacaptr-macros/src/test_dsl.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use proc_macro2::{Literal, Span, TokenStream}; 3 | use quote::quote; 4 | use syn::{Error, Result}; 5 | 6 | enum TestDslItem { 7 | In(Vec), 8 | InBang(Vec), 9 | Ou(String), 10 | } 11 | 12 | impl TestDslItem { 13 | fn try_from_line(ln: &str) -> Result { 14 | let in_bang = "in ! "; 15 | let in_ = "in "; 16 | let ou = "ou "; 17 | let tokenize = |s: &str| s.split_whitespace().map_into().collect(); 18 | #[allow(clippy::option_if_let_else)] 19 | if let Some(rest) = ln.strip_prefix(in_bang) { 20 | Ok(Self::InBang(tokenize(rest))) 21 | } else if let Some(rest) = ln.strip_prefix(in_) { 22 | Ok(Self::In(tokenize(rest))) 23 | } else if let Some(rest) = ln.strip_prefix(ou) { 24 | Ok(Self::Ou(rest.into())) 25 | } else { 26 | let msg = format!( 27 | "Item must start with one of the following: {}, found `{}`", 28 | [in_bang, in_, ou,] 29 | .iter() 30 | .map(|s| format!("`{}`", s.trim_end())) 31 | .join(", "), 32 | ln, 33 | ); 34 | Err(Error::new(Span::call_site(), msg)) 35 | } 36 | } 37 | 38 | fn build(&self) -> TokenStream { 39 | match self { 40 | Self::In(i) => { 41 | let i = i.iter().map(|s| Literal::string(s)).collect_vec(); 42 | quote! { .pacaptr(&[ #(#i),* ], &[]) } 43 | } 44 | Self::InBang(i) => { 45 | let i = i.iter().map(|s| Literal::string(s)).collect_vec(); 46 | quote! { .exec(&[ #(#i),* ], &[]) } 47 | } 48 | Self::Ou(o) => { 49 | let o = Literal::string(o); 50 | quote! { .output(&[ #o ]) } 51 | } 52 | } 53 | } 54 | } 55 | 56 | #[allow(clippy::module_name_repetitions)] 57 | pub fn test_dsl_impl(input: &str) -> Result { 58 | let items: Vec = input 59 | .lines() 60 | .map(|ln| ln.trim_start().trim_end()) 61 | // Filter out comments and empty lines. 62 | .filter(|ln| !(ln.is_empty() || ln.starts_with('#'))) 63 | .map(|ln| TestDslItem::try_from_line(ln).map(|item| item.build())) 64 | .try_collect()?; 65 | Ok(quote! { Test::new() #(#items)* .run()}) 66 | } 67 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `pacaptr` 2 | 3 | 4 | > **Warning** 5 | > This project is still slowly evolving, and the conventions and the APIs could be changed. 6 | > Some discussions concerning certain crucial design choices haven't been made yet. 7 | 8 | Welcome to `pacaptr`! 9 | 10 | ## Coding Conventions 11 | 12 | - Rust code: Use `cargo +nightly fmt` and stick with [`rustfmt.toml`](../rustfmt.toml). Follow `cargo clippy` lints if possible. 13 | - Commit message: See [Conventional Commits](https://conventionalcommits.org). 14 | 15 | ## API Docs 16 | 17 | The API docs is a good starting point if you want to dive a little deeper into this project. 18 | You can get it in one of the following ways: 19 | 20 | - See the precompiled version on [GitHub Pages](https://rami3l.github.io/pacaptr). 21 | - Compile from source: 22 | 23 | ```bash 24 | cargo doc --document-private-items --open 25 | ``` 26 | 27 | ## Making a New Release 28 | 29 | We currently make a new release by pushing a single new version tag to `master`, which will make the CI generate a new GitHub release together with the necessary artifacts. 30 | 31 | To make this automatic (and to push the new version to crates.io at the same time), it is recommended to use [`cargo-release`](https://github.com/crate-ci/cargo-release): 32 | 33 | - Perform a dry run to see if everything is OK[^patch]: 34 | 35 | ```bash 36 | cargo release --workspace patch 37 | ``` 38 | 39 | [^patch]: 40 | This example uses `patch` (0.0.1). 41 | Depending on the situation, `minor` (0.1) or `major` (1.0) might be used instead. 42 | 43 | - Add `-x` to actually publish the new version. 44 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2024" 2 | unstable_features = true 3 | 4 | format_macro_matchers = true 5 | format_macro_bodies = true 6 | group_imports = "StdExternalCrate" 7 | imports_granularity = "Crate" 8 | reorder_impl_items = true 9 | wrap_comments = true 10 | 11 | use_field_init_shorthand = true 12 | trailing_semicolon = true 13 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! APIs for reading [`pacaptr`](crate) configurations from the filesystem. 2 | //! 3 | //! I decided not to trash user's `$HOME` without their permission, so: 4 | //! - If the user hasn't yet specified any path to look at, we will look for the 5 | //! config file in the default path. 6 | //! - If the config file is not present anyway, a default one will be loaded 7 | //! with [`Default::default`], and no files will be written. 8 | //! - Any config item can be overridden by the corresponding `PACAPTR_*` 9 | //! environment variable. For example, `PACAPTR_NEEDED=false` is prioritized 10 | //! over `needed = true` in `pacaptr.toml`. 11 | 12 | use std::{env, path::PathBuf}; 13 | 14 | use figment::{ 15 | Figment, Provider, 16 | providers::{Env, Format, Toml}, 17 | util::bool_from_str_or_int, 18 | }; 19 | use serde::{Deserialize, Deserializer, Serialize}; 20 | use tap::prelude::*; 21 | 22 | /// The crate name. 23 | const CRATE_NAME: &str = clap::crate_name!(); 24 | 25 | /// The environment variable prefix for config item literals. 26 | const CONFIG_ITEM_ENV_PREFIX: &str = "PACAPTR_"; 27 | 28 | /// The environment variable name for custom config file path. 29 | const CONFIG_FILE_ENV: &str = "PACAPTR_CONFIG"; 30 | 31 | /// Configurations that may vary when running the package manager. 32 | #[must_use] 33 | #[derive(Clone, Default, Debug, Serialize, Deserialize)] 34 | #[allow(clippy::struct_excessive_bools)] 35 | pub struct Config { 36 | /// Perform a dry run. 37 | #[serde(default, deserialize_with = "bool_from_str_or_int")] 38 | pub dry_run: bool, 39 | 40 | /// Prevent reinstalling previously installed packages. 41 | #[serde(default, deserialize_with = "bool_from_str_or_int")] 42 | pub needed: bool, 43 | 44 | /// Answer yes to every question. 45 | #[serde(default, deserialize_with = "bool_from_str_or_int")] 46 | pub no_confirm: bool, 47 | 48 | /// Remove cache after installation. 49 | #[serde(default, deserialize_with = "bool_from_str_or_int")] 50 | pub no_cache: bool, 51 | 52 | /// Suppress log output. 53 | #[serde(default, deserialize_with = "option_bool_from_str_or_int")] 54 | pub quiet: Option, 55 | 56 | /// The default package manager to be invoked. 57 | pub default_pm: Option, 58 | } 59 | 60 | fn option_bool_from_str_or_int<'de, D: Deserializer<'de>>(de: D) -> Result, D::Error> { 61 | bool_from_str_or_int(de).map(Some) 62 | } 63 | 64 | impl Config { 65 | /// Returns the value of the `quiet` flag if it is present, 66 | /// otherwise returns whether the current `stdout` is **not** a TTY. 67 | #[must_use] 68 | pub fn quiet(&self) -> bool { 69 | self.quiet 70 | .unwrap_or_else(|| !console::Term::stdout().is_term()) 71 | } 72 | 73 | /// Performs a left-biased join of two `Config`s. 74 | pub fn join(&self, other: Self) -> Self { 75 | Self { 76 | dry_run: self.dry_run || other.dry_run, 77 | needed: self.needed || other.dry_run, 78 | no_confirm: self.no_confirm || other.no_confirm, 79 | no_cache: self.no_cache || other.no_cache, 80 | quiet: self.quiet.or(other.quiet), 81 | default_pm: self.default_pm.clone().or(other.default_pm), 82 | } 83 | } 84 | 85 | /// The default config file path is defined with the following precedence: 86 | /// 87 | /// - `$XDG_CONFIG_HOME/pacaptr/pacaptr.toml`, if `$XDG_CONFIG_HOME` is set; 88 | /// - `$HOME/.config/pacaptr/pacaptr.toml`. 89 | /// 90 | /// This aligns with `fish`'s behavior. 91 | /// See: 92 | fn default_path() -> Option { 93 | env::var_os("XDG_CONFIG_HOME") 94 | .map(PathBuf::from) 95 | .filter(|p| p.is_absolute()) 96 | .or_else(|| dirs_next::home_dir().map(|p| p.join(".config"))) 97 | .tap_some_mut(|p| { 98 | p.extend([CRATE_NAME, &format!("{CRATE_NAME}.toml")]); 99 | }) 100 | } 101 | 102 | /// Gets the custom config file path specified by the `PACAPTR_CONFIG` 103 | /// environment variable. 104 | fn custom_path() -> Option { 105 | env::var_os(CONFIG_FILE_ENV).map(PathBuf::from) 106 | } 107 | 108 | /// Returns the config [`Provider`] from the custom or default config file 109 | /// path. 110 | #[must_use] 111 | pub fn file_provider() -> impl Provider { 112 | Self::custom_path() 113 | .or_else(Self::default_path) 114 | .map_or_else(Figment::new, |f| Figment::from(Toml::file(f))) 115 | } 116 | 117 | /// Returns the environment config [`Provider`]. 118 | #[must_use] 119 | pub fn env_provider() -> impl Provider { 120 | Env::prefixed(CONFIG_ITEM_ENV_PREFIX) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Basic error definitions specific to this crate. 2 | 3 | use std::{ 4 | fmt::{self, Debug}, 5 | process::{ExitCode, Termination}, 6 | }; 7 | 8 | use thiserror::Error; 9 | use thiserror_ext::AsReport; 10 | use tokio::{io, task::JoinError}; 11 | 12 | use crate::{ 13 | exec::{Output, StatusCode}, 14 | print, 15 | }; 16 | 17 | /// A specialized [`Result`](std::result::Result) type used by 18 | /// [`pacaptr`](crate). 19 | pub type Result = std::result::Result; 20 | 21 | /// Error type for the [`pacaptr`](crate) library. 22 | #[derive(Debug, Error)] 23 | #[non_exhaustive] 24 | pub enum Error { 25 | /// Error when parsing CLI arguments. 26 | #[allow(missing_docs)] 27 | #[error("failed to parse arguments: {msg}")] 28 | ArgParseError { msg: String }, 29 | 30 | /// Error when handling a [`Config`](crate::config::Config). 31 | #[error("failed to parse config")] 32 | ConfigError(#[from] figment::Error), 33 | 34 | /// A [`Cmd`](crate::exec::Cmd) failed to finish. 35 | #[error("failed to get exit code of subprocess")] 36 | CmdJoinError(#[from] JoinError), 37 | 38 | /// A [`Cmd`](crate::exec::Cmd) failed to spawn. 39 | #[error("failed to spawn subprocess")] 40 | CmdSpawnError(#[source] io::Error), 41 | 42 | /// Error when trying to get a handle (e.g. `stdout`, `stderr`) out of a 43 | /// running [`Cmd`](crate::exec::Cmd). 44 | #[allow(missing_docs)] 45 | #[error("subprocess didn't have a handle to {handle}")] 46 | CmdNoHandleError { handle: String }, 47 | 48 | /// A [`Cmd`](crate::exec::Cmd) failed when waiting for it to finish. 49 | #[error("subprocess failed while running")] 50 | CmdWaitError(#[source] io::Error), 51 | 52 | /// A [`Cmd`](crate::exec::Cmd) exited with an error. 53 | #[allow(missing_docs)] 54 | #[error("subprocess exited with code {code}")] 55 | CmdStatusCodeError { code: StatusCode, output: Output }, 56 | 57 | /// A [`Cmd`](crate::exec::Cmd) was interrupted by a signal. 58 | #[error("subprocess interrupted by signal")] 59 | CmdInterruptedError, 60 | 61 | /// Error while converting a [`Vec`] to a [`String`]. 62 | #[error(transparent)] 63 | FromUtf8Error(#[from] std::string::FromUtf8Error), 64 | 65 | /// Error while rendering a dialog. 66 | #[error(transparent)] 67 | DialogError(#[from] dialoguer::Error), 68 | 69 | /// A non-specific [`io::Error`]. 70 | #[error(transparent)] 71 | IoError(#[from] io::Error), 72 | 73 | /// A [`Pm`](crate::pm::Pm) operation is not implemented. 74 | #[allow(missing_docs)] 75 | #[error("operation `{op}` is unimplemented for `{pm}`")] 76 | OperationUnimplementedError { op: String, pm: String }, 77 | 78 | /// Miscellaneous other error. 79 | #[error("{0}")] 80 | OtherError(String), 81 | } 82 | 83 | /// A simple [`enum@Error`] wrapper designed to be returned in the `main` 84 | /// function. It delegates its [`Debug`] implementation to the 85 | /// [`std::fmt::Display`] implementation of its underlying error. 86 | #[allow(clippy::module_name_repetitions)] 87 | pub struct MainError(Error); 88 | 89 | impl From for MainError { 90 | fn from(e: Error) -> Self { 91 | Self(e) 92 | } 93 | } 94 | 95 | impl Debug for MainError { 96 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 97 | // Erase the default "Error: " message header. 98 | write!(f, "\r")?; 99 | print::write_err(f, &*print::prompt::ERROR, self.0.as_report()) 100 | } 101 | } 102 | 103 | impl Termination for MainError { 104 | fn report(self) -> ExitCode { 105 | #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] 106 | match self.0 { 107 | Error::CmdStatusCodeError { code, .. } => code as u8, 108 | _ => 1, 109 | } 110 | .into() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `pacaptr` is a `pacman`-like syntax wrapper for many package managers. 2 | #![cfg_attr( 3 | doc, 4 | doc = indoc::indoc!{r##" 5 | # Compatibility Table 6 | 7 | Currently, `pacaptr` supports the following operations: 8 | "##} 9 | )] 10 | #![cfg_attr(doc, doc = pacaptr_macros::compat_table!())] 11 | #![cfg_attr( 12 | doc, 13 | doc = indoc::indoc!{r##" 14 | Note: Some flags are "translated" so are not shown in this table, eg. `-p` 15 | in `-Sp`. 16 | "##} 17 | )] 18 | #![warn(missing_docs)] 19 | #![cfg_attr(any(test, feature = "test"), allow(clippy::wildcard_imports))] 20 | 21 | pub mod config; 22 | pub mod error; 23 | pub mod exec; 24 | pub mod pm; 25 | pub mod print; 26 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cmd; 2 | 3 | mod _built { 4 | include!(concat!(env!("OUT_DIR"), "/built.rs")); 5 | } 6 | 7 | use clap::Parser; 8 | use pacaptr::error::MainError; 9 | 10 | use crate::cmd::Pacaptr; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<(), MainError> { 14 | Pacaptr::parse().dispatch().await?; 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /src/pm/apk.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use indoc::indoc; 7 | use tap::prelude::*; 8 | 9 | use super::{NoCacheStrategy, Pm, PmHelper, PromptStrategy, Strategy}; 10 | use crate::{config::Config, error::Result, exec::Cmd}; 11 | 12 | macro_rules! doc_self { 13 | () => { 14 | indoc! {r" 15 | The [Alpine Linux package management system](https://wiki.alpinelinux.org/wiki/Alpine_Linux_package_management). 16 | "} 17 | }; 18 | } 19 | use doc_self; 20 | 21 | #[doc = doc_self!()] 22 | #[derive(Debug)] 23 | pub struct Apk { 24 | cfg: Config, 25 | } 26 | 27 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 28 | prompt: PromptStrategy::CustomPrompt, 29 | ..Strategy::default() 30 | }); 31 | 32 | static STRAT_INSTALL: LazyLock = LazyLock::new(|| Strategy { 33 | prompt: PromptStrategy::CustomPrompt, 34 | no_cache: NoCacheStrategy::with_flags(["--no-cache"]), 35 | ..Strategy::default() 36 | }); 37 | 38 | impl Apk { 39 | #[must_use] 40 | #[allow(missing_docs)] 41 | pub const fn new(cfg: Config) -> Self { 42 | Self { cfg } 43 | } 44 | } 45 | 46 | #[async_trait] 47 | impl Pm for Apk { 48 | /// Gets the name of the package manager. 49 | fn name(&self) -> &'static str { 50 | "apk" 51 | } 52 | 53 | fn cfg(&self) -> &Config { 54 | &self.cfg 55 | } 56 | 57 | /// Q generates a list of installed packages. 58 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 59 | if kws.is_empty() { 60 | self.run(Cmd::new(["apk", "info"]).flags(flags)).await 61 | } else { 62 | self.qs(kws, flags).await 63 | } 64 | } 65 | 66 | /// Qi displays local package information: name, version, description, etc. 67 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 68 | self.si(kws, flags).await 69 | } 70 | 71 | /// Qii displays local packages which require X to be installed, aka local 72 | /// reverse dependencies. 73 | async fn qii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 74 | self.sii(kws, flags).await 75 | } 76 | 77 | /// Ql displays files provided by local package. 78 | async fn ql(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 79 | self.run(Cmd::new(["apk", "info", "-L"]).kws(kws).flags(flags)) 80 | .await 81 | } 82 | 83 | /// Qo queries the package which provides FILE. 84 | async fn qo(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 85 | Cmd::new(["apk", "info", "--who-owns"]) 86 | .kws(kws) 87 | .flags(flags) 88 | .pipe(|cmd| self.run(cmd)) 89 | .await 90 | } 91 | 92 | /// Qs searches locally installed package for names or descriptions. 93 | // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, 94 | // when including multiple search terms, only packages with descriptions 95 | // matching ALL of those terms are returned. 96 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 97 | self.search_regex(Cmd::new(["apk", "info", "-d"]).flags(flags), kws) 98 | .await 99 | } 100 | 101 | /// Qu lists packages which have an update available. 102 | //? Is that the right way to input '<'? 103 | async fn qu(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 104 | self.run(Cmd::new(["apk", "version", "-l", "<"]).flags(flags)) 105 | .await 106 | } 107 | 108 | /// R removes a single package, leaving all of its dependencies installed. 109 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 110 | Cmd::with_sudo(["apk", "del"]) 111 | .kws(kws) 112 | .flags(flags) 113 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 114 | .await 115 | } 116 | 117 | /// Rn removes a package and skips the generation of configuration backup 118 | /// files. 119 | async fn rn(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 120 | Cmd::with_sudo(["apk", "del", "--purge"]) 121 | .kws(kws) 122 | .flags(flags) 123 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 124 | .await 125 | } 126 | 127 | /// Rns removes a package and its dependencies which are not required by any 128 | /// other installed package, and skips the generation of configuration 129 | /// backup files. 130 | async fn rns(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 131 | Cmd::with_sudo(["apk", "del", "--purge", "-r"]) 132 | .kws(kws) 133 | .flags(flags) 134 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 135 | .await 136 | } 137 | 138 | /// Rs removes a package and its dependencies which are not required by any 139 | /// other installed package, and not explicitly installed by the user. 140 | async fn rs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 141 | self.r(kws, flags).await 142 | } 143 | 144 | /// S installs one or more packages by name. 145 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 146 | Cmd::with_sudo(["apk", "add"]) 147 | .kws(kws) 148 | .flags(flags) 149 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 150 | .await 151 | } 152 | 153 | /// Sc removes all the cached packages that are not currently installed, and 154 | /// the unused sync database. 155 | async fn sc(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 156 | Cmd::with_sudo(["apk", "cache", "-v", "clean"]) 157 | .flags(flags) 158 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 159 | .await 160 | } 161 | 162 | /// Scc removes all files from the cache. 163 | async fn scc(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 164 | Cmd::with_sudo(["rm", "-vrf", "/var/cache/apk/*"]) 165 | .flags(flags) 166 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 167 | .await 168 | } 169 | 170 | /// Si displays remote package information: name, version, description, etc. 171 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 172 | self.run(Cmd::new(["apk", "info", "-a"]).kws(kws).flags(flags)) 173 | .await 174 | } 175 | 176 | /// Sii displays packages which require X to be installed, aka reverse 177 | /// dependencies. 178 | async fn sii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 179 | self.run(Cmd::new(["apk", "info", "-r"]).kws(kws).flags(flags)) 180 | .await 181 | } 182 | 183 | /// Sl displays a list of all packages in all installation sources that are 184 | /// handled by the package management. 185 | async fn sl(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 186 | self.run(Cmd::new(["apk", "search"]).kws(kws).flags(flags)) 187 | .await 188 | } 189 | 190 | /// Ss searches for package(s) by searching the expression in name, 191 | /// description, short description. 192 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 193 | self.run(Cmd::new(["apk", "search", "-v"]).kws(kws).flags(flags)) 194 | .await 195 | } 196 | 197 | /// Su updates outdated packages. 198 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 199 | Cmd::with_sudo(if kws.is_empty() { 200 | &["apk", "upgrade"][..] 201 | } else { 202 | &["apk", "add", "-u"][..] 203 | }) 204 | .kws(kws) 205 | .flags(flags) 206 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 207 | .await 208 | } 209 | 210 | /// Suy refreshes the local package database, then updates outdated 211 | /// packages. 212 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 213 | Cmd::with_sudo(if kws.is_empty() { 214 | ["apk", "upgrade", "-U", "-a"] 215 | } else { 216 | ["apk", "add", "-U", "-u"] 217 | }) 218 | .kws(kws) 219 | .flags(flags) 220 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 221 | .await 222 | } 223 | 224 | /// Sw retrieves all packages from the server, but does not install/upgrade 225 | /// anything. 226 | async fn sw(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 227 | Cmd::new(["apk", "fetch"]) 228 | .kws(kws) 229 | .flags(flags) 230 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 231 | .await 232 | } 233 | 234 | /// Sy refreshes the local package database. 235 | async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 236 | self.run(Cmd::with_sudo(["apk", "update"]).kws(kws).flags(flags)) 237 | .await?; 238 | if !kws.is_empty() { 239 | self.s(kws, flags).await?; 240 | } 241 | Ok(()) 242 | } 243 | 244 | /// U upgrades or adds package(s) to the system and installs the required 245 | /// dependencies from sync repositories. 246 | async fn u(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 247 | Cmd::with_sudo(["apk", "add", "--allow-untrusted"]) 248 | .kws(kws) 249 | .flags(flags) 250 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 251 | .await 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/pm/apt.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use indoc::indoc; 7 | use tap::prelude::*; 8 | 9 | use super::{NoCacheStrategy, Pm, PmHelper, PromptStrategy, Strategy}; 10 | use crate::{config::Config, error::Result, exec::Cmd}; 11 | 12 | macro_rules! doc_self { 13 | () => { 14 | indoc! {" 15 | The [Advanced Package Tool](https://salsa.debian.org/apt-team/apt). 16 | "} 17 | }; 18 | } 19 | use doc_self; 20 | 21 | #[doc = doc_self!()] 22 | #[derive(Debug)] 23 | pub struct Apt { 24 | cfg: Config, 25 | } 26 | 27 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 28 | prompt: PromptStrategy::native_no_confirm(["--yes"]), 29 | ..Strategy::default() 30 | }); 31 | 32 | static STRAT_INSTALL: LazyLock = LazyLock::new(|| Strategy { 33 | prompt: PromptStrategy::native_no_confirm(["--yes"]), 34 | no_cache: NoCacheStrategy::Scc, 35 | ..Strategy::default() 36 | }); 37 | 38 | impl Apt { 39 | #[must_use] 40 | #[allow(missing_docs)] 41 | pub const fn new(cfg: Config) -> Self { 42 | Self { cfg } 43 | } 44 | 45 | /// Returns the command used to invoke [`Apt`], eg. `apt`, `pkg`. 46 | #[must_use] 47 | fn cmd(&self) -> &str { 48 | self.cfg 49 | .default_pm 50 | .as_deref() 51 | .expect("default package manager should have been assigned before initialization") 52 | } 53 | } 54 | 55 | #[async_trait] 56 | impl Pm for Apt { 57 | /// Gets the name of the package manager. 58 | fn name(&self) -> &'static str { 59 | "apt" 60 | } 61 | 62 | fn cfg(&self) -> &Config { 63 | &self.cfg 64 | } 65 | 66 | /// Q generates a list of installed packages. 67 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 68 | Cmd::new(["apt", "list", "--installed"]) 69 | .kws(kws) 70 | .flags(flags) 71 | .pipe(|cmd| self.run(cmd)) 72 | .await 73 | } 74 | 75 | /// Qc shows the changelog of a package. 76 | async fn qc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 77 | self.run(Cmd::new(["apt", "changelog"]).kws(kws).flags(flags)) 78 | .await 79 | } 80 | 81 | /// Qe lists packages installed explicitly (not as dependencies). 82 | async fn qe(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 83 | self.run(Cmd::new(["apt-mark", "showmanual"]).kws(kws).flags(flags)) 84 | .await 85 | } 86 | 87 | /// Qi displays local package information: name, version, description, etc. 88 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 89 | self.run(Cmd::new(["dpkg-query", "-s"]).kws(kws).flags(flags)) 90 | .await 91 | } 92 | 93 | /// Qii displays local packages which require X to be installed, aka local 94 | /// reverse dependencies. 95 | async fn qii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 96 | self.sii(kws, flags).await 97 | } 98 | 99 | /// Qo queries the package which provides FILE. 100 | async fn qo(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 101 | self.run(Cmd::new(["dpkg-query", "-S"]).kws(kws).flags(flags)) 102 | .await 103 | } 104 | 105 | /// Qp queries a package supplied through a file supplied on the command 106 | /// line rather than an entry in the package management database. 107 | async fn qp(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 108 | self.run(Cmd::new(["dpkg-deb", "-I"]).kws(kws).flags(flags)) 109 | .await 110 | } 111 | 112 | /// Qs searches locally installed package for names or descriptions. 113 | // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, 114 | // when including multiple search terms, only packages with descriptions 115 | // matching ALL of those terms are returned. 116 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 117 | Cmd::new(["dpkg-query", "-l"]) 118 | .flags(flags) 119 | .pipe(|cmd| self.search_regex_with_header(cmd, kws, 4)) 120 | .await 121 | } 122 | 123 | /// Qu lists packages which have an update available. 124 | async fn qu(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 125 | Cmd::with_sudo(["apt", "upgrade", "--trivial-only"]) 126 | .kws(kws) 127 | .flags(flags) 128 | .pipe(|cmd| self.run(cmd)) 129 | .await 130 | } 131 | 132 | /// R removes a single package, leaving all of its dependencies installed. 133 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 134 | Cmd::with_sudo(["apt", "remove"]) 135 | .kws(kws) 136 | .flags(flags) 137 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 138 | .await 139 | } 140 | 141 | /// Rn removes a package and skips the generation of configuration backup 142 | /// files. 143 | async fn rn(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 144 | Cmd::with_sudo(["apt", "purge"]) 145 | .kws(kws) 146 | .flags(flags) 147 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 148 | .await 149 | } 150 | 151 | /// Rns removes a package and its dependencies which are not required by any 152 | /// other installed package, and skips the generation of configuration 153 | /// backup files. 154 | async fn rns(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 155 | Cmd::with_sudo(["apt", "autoremove", "--purge"]) 156 | .kws(kws) 157 | .flags(flags) 158 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 159 | .await 160 | } 161 | 162 | /// Rs removes a package and its dependencies which are not required by any 163 | /// other installed package, and not explicitly installed by the user. 164 | async fn rs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 165 | Cmd::with_sudo(["apt", "autoremove"]) 166 | .kws(kws) 167 | .flags(flags) 168 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 169 | .await 170 | } 171 | 172 | /// S installs one or more packages by name. 173 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 174 | if self.cfg.needed { 175 | Cmd::with_sudo(&[self.cmd(), "install"][..]) 176 | } else { 177 | Cmd::with_sudo(&[self.cmd(), "install", "--reinstall"][..]) 178 | } 179 | .kws(kws) 180 | .flags(flags) 181 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 182 | .await 183 | } 184 | 185 | /// Sc removes all the cached packages that are not currently installed, and 186 | /// the unused sync database. 187 | async fn sc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 188 | Cmd::with_sudo(["apt", "clean"]) 189 | .kws(kws) 190 | .flags(flags) 191 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 192 | .await 193 | } 194 | 195 | /// Scc removes all files from the cache. 196 | async fn scc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 197 | Cmd::with_sudo(["apt", "autoclean"]) 198 | .kws(kws) 199 | .flags(flags) 200 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 201 | .await 202 | } 203 | 204 | /// Sg lists all packages belonging to the GROUP. 205 | async fn sg(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 206 | Cmd::new(if kws.is_empty() { 207 | ["tasksel", "--list-task"] 208 | } else { 209 | ["tasksel", "--task-packages"] 210 | }) 211 | .kws(kws) 212 | .flags(flags) 213 | .pipe(|cmd| self.run(cmd)) 214 | .await 215 | } 216 | 217 | /// Si displays remote package information: name, version, description, etc. 218 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 219 | self.run(Cmd::new(["apt", "show"]).kws(kws).flags(flags)) 220 | .await 221 | } 222 | 223 | /// Sii displays packages which require X to be installed, aka reverse 224 | /// dependencies. 225 | async fn sii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 226 | self.run(Cmd::new(["apt", "rdepends"]).kws(kws).flags(flags)) 227 | .await 228 | } 229 | 230 | /// Ss searches for package(s) by searching the expression in name, 231 | /// description, short description. 232 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 233 | self.run(Cmd::new([self.cmd(), "search"]).kws(kws).flags(flags)) 234 | .await 235 | } 236 | 237 | /// Su updates outdated packages. 238 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 239 | if kws.is_empty() { 240 | Cmd::with_sudo(["apt", "upgrade"]) 241 | .flags(flags) 242 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 243 | .await?; 244 | Cmd::with_sudo(["apt", "dist-upgrade"]) 245 | .flags(flags) 246 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 247 | .await 248 | } else { 249 | self.s(kws, flags).await 250 | } 251 | } 252 | 253 | /// Suy refreshes the local package database, then updates outdated 254 | /// packages. 255 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 256 | self.sy(&[], flags).await?; 257 | self.su(kws, flags).await 258 | } 259 | 260 | /// Sw retrieves all packages from the server, but does not install/upgrade 261 | /// anything. 262 | async fn sw(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 263 | Cmd::with_sudo([self.cmd(), "install", "--download-only"]) 264 | .kws(kws) 265 | .flags(flags) 266 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 267 | .await 268 | } 269 | 270 | /// Sy refreshes the local package database. 271 | async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 272 | self.run(Cmd::with_sudo([self.cmd(), "update"]).flags(flags)) 273 | .await?; 274 | if !kws.is_empty() { 275 | self.s(kws, flags).await?; 276 | } 277 | Ok(()) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/pm/brew.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use indoc::indoc; 7 | use tap::prelude::*; 8 | 9 | use super::{DryRunStrategy, NoCacheStrategy, Pm, PmHelper, PromptStrategy, Strategy}; 10 | use crate::{config::Config, error::Result, exec::Cmd}; 11 | 12 | macro_rules! doc_self { 13 | () => { 14 | indoc! {" 15 | The [Homebrew Package Manager](https://brew.sh/). 16 | "} 17 | }; 18 | } 19 | use doc_self; 20 | 21 | #[doc = doc_self!()] 22 | #[derive(Debug)] 23 | pub struct Brew { 24 | cfg: Config, 25 | } 26 | 27 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 28 | prompt: PromptStrategy::CustomPrompt, 29 | ..Strategy::default() 30 | }); 31 | 32 | static STRAT_INSTALL: LazyLock = LazyLock::new(|| Strategy { 33 | prompt: PromptStrategy::CustomPrompt, 34 | no_cache: NoCacheStrategy::Scc, 35 | ..Strategy::default() 36 | }); 37 | 38 | impl Brew { 39 | #[must_use] 40 | #[allow(missing_docs)] 41 | pub const fn new(cfg: Config) -> Self { 42 | Self { cfg } 43 | } 44 | } 45 | 46 | #[async_trait] 47 | impl Pm for Brew { 48 | /// Gets the name of the package manager. 49 | fn name(&self) -> &'static str { 50 | "brew" 51 | } 52 | 53 | fn cfg(&self) -> &Config { 54 | &self.cfg 55 | } 56 | 57 | /// Q generates a list of installed packages. 58 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 59 | if kws.is_empty() { 60 | self.run(Cmd::new(["brew", "list"]).flags(flags)).await 61 | } else { 62 | self.qs(kws, flags).await 63 | } 64 | } 65 | 66 | /// Qc shows the changelog of a package. 67 | async fn qc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 68 | self.run(Cmd::new(["brew", "log"]).kws(kws).flags(flags)) 69 | .await 70 | } 71 | 72 | /// Qi displays local package information: name, version, description, etc. 73 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 74 | self.si(kws, flags).await 75 | } 76 | 77 | /// Qii displays local packages which require X to be installed, aka local 78 | /// reverse dependencies. 79 | async fn qii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 80 | Cmd::new(["brew", "uses", "--installed"]) 81 | .kws(kws) 82 | .flags(flags) 83 | .pipe(|cmd| self.run(cmd)) 84 | .await 85 | } 86 | 87 | /// Ql displays files provided by local package. 88 | async fn ql(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 89 | // TODO: it seems that the output of `brew list python` in fish has a mechanism 90 | // against duplication: /usr/local/Cellar/python/3.6.0/Frameworks/ 91 | // Python.framework/ (1234 files) 92 | self.run(Cmd::new(["brew", "list"]).kws(kws).flags(flags)) 93 | .await 94 | } 95 | 96 | /// Qs searches locally installed package for names or descriptions. 97 | // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, 98 | // when including multiple search terms, only packages with descriptions 99 | // matching ALL of those terms are returned. 100 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 101 | // ! `brew list` lists all formulae and casks only when using tty. 102 | self.search_regex(Cmd::new(["brew", "list", "--formula"]).flags(flags), kws) 103 | .await?; 104 | if cfg!(target_os = "macos") { 105 | self.search_regex(Cmd::new(["brew", "list", "--cask"]).flags(flags), kws) 106 | .await?; 107 | } 108 | 109 | Ok(()) 110 | } 111 | 112 | /// Qu lists packages which have an update available. 113 | async fn qu(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 114 | self.run(Cmd::new(["brew", "outdated"]).kws(kws).flags(flags)) 115 | .await 116 | } 117 | 118 | /// R removes a single package, leaving all of its dependencies installed. 119 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 120 | Cmd::new(["brew", "uninstall"]) 121 | .kws(kws) 122 | .flags(flags) 123 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 124 | .await 125 | } 126 | 127 | /// Rn removes a package and skips the generation of configuration backup 128 | /// files. 129 | async fn rn(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 130 | Cmd::new(["brew", "uninstall", "--zap", "-f"]) 131 | .kws(kws) 132 | .flags(flags) 133 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 134 | .await 135 | } 136 | 137 | /// Rns removes a package and its dependencies which are not required by any 138 | /// other installed package, and skips the generation of configuration 139 | /// backup files. 140 | async fn rns(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 141 | self.rn(kws, flags).await?; 142 | Cmd::new(["brew", "autoremove"]) 143 | .flags(flags) 144 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 145 | .await 146 | } 147 | 148 | /// Rs removes a package and its dependencies which are not required by any 149 | /// other installed package, and not explicitly installed by the user. 150 | async fn rs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 151 | self.r(kws, flags).await?; 152 | Cmd::new(["brew", "autoremove"]) 153 | .flags(flags) 154 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 155 | .await 156 | } 157 | 158 | /// S installs one or more packages by name. 159 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 160 | Cmd::new(if self.cfg.needed { 161 | ["brew", "install"] 162 | } else { 163 | // If the package is not installed, `brew reinstall` behaves just like `brew 164 | // install`, so `brew reinstall` matches perfectly the behavior of 165 | // `pacman -S`. 166 | ["brew", "reinstall"] 167 | }) 168 | .kws(kws) 169 | .flags(flags) 170 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 171 | .await 172 | } 173 | 174 | /// Sc removes all the cached packages that are not currently installed, and 175 | /// the unused sync database. 176 | async fn sc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 177 | let strat = Strategy { 178 | dry_run: DryRunStrategy::with_flags(["--dry-run"]), 179 | prompt: PromptStrategy::CustomPrompt, 180 | ..Strategy::default() 181 | }; 182 | Cmd::new(["brew", "cleanup"]) 183 | .kws(kws) 184 | .flags(flags) 185 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &strat)) 186 | .await 187 | } 188 | 189 | /// Scc removes all files from the cache. 190 | async fn scc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 191 | let strat = Strategy { 192 | dry_run: DryRunStrategy::with_flags(["--dry-run"]), 193 | prompt: PromptStrategy::CustomPrompt, 194 | ..Strategy::default() 195 | }; 196 | Cmd::new(["brew", "cleanup", "-s"]) 197 | .kws(kws) 198 | .flags(flags) 199 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &strat)) 200 | .await 201 | } 202 | 203 | /// Sccc performs a deeper cleaning of the cache than `Scc` (if applicable). 204 | async fn sccc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 205 | let strat = Strategy { 206 | dry_run: DryRunStrategy::with_flags(["--dry-run"]), 207 | prompt: PromptStrategy::CustomPrompt, 208 | ..Strategy::default() 209 | }; 210 | Cmd::new(["brew", "cleanup", "--prune=all"]) 211 | .kws(kws) 212 | .flags(flags) 213 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &strat)) 214 | .await 215 | } 216 | 217 | /// Si displays remote package information: name, version, description, etc. 218 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 219 | self.run(Cmd::new(["brew", "info"]).kws(kws).flags(flags)) 220 | .await 221 | } 222 | 223 | /// Sii displays packages which require X to be installed, aka reverse 224 | /// dependencies. 225 | async fn sii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 226 | Cmd::new(["brew", "uses", "--eval-all"]) 227 | .kws(kws) 228 | .flags(flags) 229 | .pipe(|cmd| self.run(cmd)) 230 | .await 231 | } 232 | 233 | /// Ss searches for package(s) by searching the expression in name, 234 | /// description, short description. 235 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 236 | self.run(Cmd::new(["brew", "search"]).kws(kws).flags(flags)) 237 | .await 238 | } 239 | 240 | /// Su updates outdated packages. 241 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 242 | Cmd::new(["brew", "upgrade"]) 243 | .kws(kws) 244 | .flags(flags) 245 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 246 | .await 247 | } 248 | 249 | /// Suy refreshes the local package database, then updates outdated 250 | /// packages. 251 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 252 | self.sy(&[], flags).await?; 253 | self.su(kws, flags).await 254 | } 255 | 256 | /// Sw retrieves all packages from the server, but does not install/upgrade 257 | /// anything. 258 | async fn sw(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 259 | Cmd::new(["brew", "fetch"]) 260 | .kws(kws) 261 | .flags(flags) 262 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 263 | .await 264 | } 265 | 266 | /// Sy refreshes the local package database. 267 | async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 268 | self.run(Cmd::new(["brew", "update"]).flags(flags)).await?; 269 | if !kws.is_empty() { 270 | self.s(kws, flags).await?; 271 | } 272 | Ok(()) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/pm/choco.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use indoc::indoc; 7 | use tap::prelude::*; 8 | 9 | use super::{DryRunStrategy, Pm, PmHelper, PromptStrategy, Strategy}; 10 | use crate::{config::Config, error::Result, exec::Cmd}; 11 | 12 | macro_rules! doc_self { 13 | () => { 14 | indoc! {" 15 | The [Chocolatey Package Manager](https://chocolatey.org/). 16 | "} 17 | }; 18 | } 19 | use doc_self; 20 | 21 | #[doc = doc_self!()] 22 | #[derive(Debug)] 23 | pub struct Choco { 24 | cfg: Config, 25 | } 26 | 27 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 28 | prompt: PromptStrategy::native_no_confirm(["--yes"]), 29 | dry_run: DryRunStrategy::with_flags(["--what-if"]), 30 | ..Strategy::default() 31 | }); 32 | 33 | static STRAT_CHECK_DRY: LazyLock = LazyLock::new(|| Strategy { 34 | dry_run: DryRunStrategy::with_flags(["--what-if"]), 35 | ..Strategy::default() 36 | }); 37 | 38 | impl Choco { 39 | #[must_use] 40 | #[allow(missing_docs)] 41 | pub const fn new(cfg: Config) -> Self { 42 | Self { cfg } 43 | } 44 | 45 | async fn check_dry(&self, cmd: Cmd) -> Result<()> { 46 | self.run_with(cmd, self.default_mode(), &STRAT_CHECK_DRY) 47 | .await 48 | } 49 | } 50 | 51 | // Windows is so special! It's better not to "sudo" automatically. 52 | #[async_trait] 53 | impl Pm for Choco { 54 | /// Gets the name of the package manager. 55 | fn name(&self) -> &'static str { 56 | "choco" 57 | } 58 | 59 | fn cfg(&self) -> &Config { 60 | &self.cfg 61 | } 62 | 63 | /// Q generates a list of installed packages. 64 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 65 | Cmd::new(["choco", "list"]) 66 | .kws(kws) 67 | .flags(flags) 68 | .pipe(|cmd| self.check_dry(cmd)) 69 | .await 70 | } 71 | 72 | /// Qi displays local package information: name, version, description, etc. 73 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 74 | self.si(kws, flags).await 75 | } 76 | 77 | /// Qu lists packages which have an update available. 78 | async fn qu(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 79 | self.check_dry(Cmd::new(["choco", "outdated"]).kws(kws).flags(flags)) 80 | .await 81 | } 82 | 83 | /// R removes a single package, leaving all of its dependencies installed. 84 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 85 | Cmd::new(["choco", "uninstall"]) 86 | .kws(kws) 87 | .flags(flags) 88 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 89 | .await 90 | } 91 | 92 | /// Rss removes a package and its dependencies which are not required by any 93 | /// other installed package. 94 | async fn rss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 95 | Cmd::new(["choco", "uninstall", "--removedependencies"]) 96 | .kws(kws) 97 | .flags(flags) 98 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 99 | .await 100 | } 101 | 102 | /// S installs one or more packages by name. 103 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 104 | Cmd::new(if self.cfg.needed { 105 | &["choco", "install"][..] 106 | } else { 107 | &["choco", "install", "--force"][..] 108 | }) 109 | .kws(kws) 110 | .flags(flags) 111 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 112 | .await 113 | } 114 | 115 | /// Si displays remote package information: name, version, description, etc. 116 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 117 | self.check_dry(Cmd::new(["choco", "info"]).kws(kws).flags(flags)) 118 | .await 119 | } 120 | 121 | /// Ss searches for package(s) by searching the expression in name, 122 | /// description, short description. 123 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 124 | self.check_dry(Cmd::new(["choco", "search"]).kws(kws).flags(flags)) 125 | .await 126 | } 127 | 128 | /// Su updates outdated packages. 129 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 130 | Cmd::new(if kws.is_empty() { 131 | &["choco", "upgrade", "all"][..] 132 | } else { 133 | &["choco", "upgrade"][..] 134 | }) 135 | .kws(kws) 136 | .flags(flags) 137 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 138 | .await 139 | } 140 | 141 | /// Suy refreshes the local package database, then updates outdated 142 | /// packages. 143 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 144 | self.su(kws, flags).await 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/pm/conda.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use futures::prelude::*; 7 | use indoc::indoc; 8 | use tap::prelude::*; 9 | 10 | use super::{Pm, PmHelper, PromptStrategy, Strategy}; 11 | use crate::{config::Config, error::Result, exec::Cmd}; 12 | 13 | macro_rules! doc_self { 14 | () => { 15 | indoc! {" 16 | The [Conda Package Manager](https://conda.io/). 17 | "} 18 | }; 19 | } 20 | use doc_self; 21 | 22 | #[doc = doc_self!()] 23 | #[derive(Debug)] 24 | pub struct Conda { 25 | cfg: Config, 26 | } 27 | 28 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 29 | prompt: PromptStrategy::native_no_confirm(["-y"]), 30 | ..Strategy::default() 31 | }); 32 | 33 | impl Conda { 34 | #[must_use] 35 | #[allow(missing_docs)] 36 | pub const fn new(cfg: Config) -> Self { 37 | Self { cfg } 38 | } 39 | } 40 | 41 | #[async_trait] 42 | impl Pm for Conda { 43 | /// Gets the name of the package manager. 44 | fn name(&self) -> &'static str { 45 | "conda" 46 | } 47 | 48 | fn cfg(&self) -> &Config { 49 | &self.cfg 50 | } 51 | 52 | /// Q generates a list of installed packages. 53 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 54 | if kws.is_empty() { 55 | self.run(Cmd::new(["conda", "list"]).flags(flags)).await 56 | } else { 57 | self.qs(kws, flags).await 58 | } 59 | } 60 | 61 | /// Qo queries the package which provides FILE. 62 | async fn qo(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 63 | Cmd::new(["conda", "package", "--which"]) 64 | .kws(kws) 65 | .flags(flags) 66 | .pipe(|cmd| self.run(cmd)) 67 | .await 68 | } 69 | 70 | /// Qs searches locally installed package for names or descriptions. 71 | // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, 72 | // when including multiple search terms, only packages with descriptions 73 | // matching ALL of those terms are returned. 74 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 75 | self.search_regex(Cmd::new(["conda", "list"]).flags(flags), kws) 76 | .await 77 | } 78 | 79 | /// R removes a single package, leaving all of its dependencies installed. 80 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 81 | Cmd::new(["conda", "remove"]) 82 | .kws(kws) 83 | .flags(flags) 84 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 85 | .await 86 | } 87 | 88 | /// S installs one or more packages by name. 89 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 90 | Cmd::new(["conda", "install"]) 91 | .kws(kws) 92 | .flags(flags) 93 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 94 | .await 95 | } 96 | 97 | /// Sc removes all the cached packages that are not currently installed, and 98 | /// the unused sync database. 99 | async fn sc(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 100 | Cmd::new(["conda", "clean", "--all"]) 101 | .flags(flags) 102 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 103 | .await 104 | } 105 | 106 | /// Si displays remote package information: name, version, description, etc. 107 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 108 | Cmd::new(["conda", "search", "--info"]) 109 | .kws(kws) 110 | .flags(flags) 111 | .pipe(|cmd| self.run(cmd)) 112 | .await 113 | } 114 | 115 | /// Ss searches for package(s) by searching the expression in name, 116 | /// description, short description. 117 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 118 | stream::iter(kws) 119 | .map(|s| Ok(format!("*{s}*"))) 120 | .try_for_each(|kw| self.run(Cmd::new(["conda", "search"]).kws([kw]).flags(flags))) 121 | .await 122 | } 123 | 124 | /// Su updates outdated packages. 125 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 126 | Cmd::new(["conda", "update", "--all"]) 127 | .kws(kws) 128 | .flags(flags) 129 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 130 | .await 131 | } 132 | 133 | /// Suy refreshes the local package database, then updates outdated 134 | /// packages. 135 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 136 | self.su(kws, flags).await 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/pm/dnf.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use indoc::indoc; 7 | use tap::prelude::*; 8 | 9 | use super::{NoCacheStrategy, Pm, PmHelper, PromptStrategy, Strategy}; 10 | use crate::{config::Config, error::Result, exec::Cmd}; 11 | 12 | macro_rules! doc_self { 13 | () => { 14 | indoc! {" 15 | The [Dandified YUM](https://github.com/rpm-software-management/dnf). 16 | "} 17 | }; 18 | } 19 | use doc_self; 20 | 21 | #[doc = doc_self!()] 22 | #[derive(Debug)] 23 | pub struct Dnf { 24 | cfg: Config, 25 | } 26 | 27 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 28 | prompt: PromptStrategy::native_no_confirm(["-y"]), 29 | ..Strategy::default() 30 | }); 31 | 32 | static STRAT_PROMPT_CUSTOM: LazyLock = LazyLock::new(|| Strategy { 33 | prompt: PromptStrategy::CustomPrompt, 34 | ..Strategy::default() 35 | }); 36 | 37 | static STRAT_INSTALL: LazyLock = LazyLock::new(|| Strategy { 38 | prompt: PromptStrategy::native_no_confirm(["-y"]), 39 | no_cache: NoCacheStrategy::Sccc, 40 | ..Strategy::default() 41 | }); 42 | 43 | impl Dnf { 44 | #[must_use] 45 | #[allow(missing_docs)] 46 | pub const fn new(cfg: Config) -> Self { 47 | Self { cfg } 48 | } 49 | } 50 | 51 | #[async_trait] 52 | impl Pm for Dnf { 53 | /// Gets the name of the package manager. 54 | fn name(&self) -> &'static str { 55 | "dnf" 56 | } 57 | 58 | fn cfg(&self) -> &Config { 59 | &self.cfg 60 | } 61 | 62 | /// Q generates a list of installed packages. 63 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 64 | if kws.is_empty() { 65 | self.run(Cmd::new(["rpm", "-qa", "--qf", "%{NAME} %{VERSION}\\n"]).flags(flags)) 66 | .await 67 | } else { 68 | self.qs(kws, flags).await 69 | } 70 | } 71 | 72 | /// Qc shows the changelog of a package. 73 | async fn qc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 74 | Cmd::new(["rpm", "-q", "--changelog"]) 75 | .kws(kws) 76 | .flags(flags) 77 | .pipe(|cmd| self.run(cmd)) 78 | .await 79 | } 80 | 81 | /// Qe lists packages installed explicitly (not as dependencies). 82 | async fn qe(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 83 | Cmd::new(["dnf", "repoquery", "--userinstalled"]) 84 | .kws(kws) 85 | .flags(flags) 86 | .pipe(|cmd| self.run(cmd)) 87 | .await 88 | } 89 | 90 | /// Qi displays local package information: name, version, description, etc. 91 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 92 | Cmd::new(["dnf", "info", "--installed"]) 93 | .kws(kws) 94 | .flags(flags) 95 | .pipe(|cmd| self.run(cmd)) 96 | .await 97 | } 98 | 99 | /// Qii displays local packages which require X to be installed, aka local 100 | /// reverse dependencies. 101 | async fn qii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 102 | Cmd::new(["dnf", "repoquery", "--installed", "--whatdepends"]) 103 | .kws(kws) 104 | .flags(flags) 105 | .pipe(|cmd| self.run(cmd)) 106 | .await 107 | } 108 | 109 | /// Ql displays files provided by local package. 110 | async fn ql(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 111 | self.run(Cmd::new(["rpm", "-ql"]).kws(kws).flags(flags)) 112 | .await 113 | } 114 | 115 | /// Qm lists packages that are installed but are not available in any 116 | /// installation source (anymore). 117 | async fn qm(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 118 | self.run(Cmd::new(["dnf", "list", "--extras"]).flags(flags)) 119 | .await 120 | } 121 | 122 | /// Qo queries the package which provides FILE. 123 | async fn qo(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 124 | self.run(Cmd::new(["rpm", "-qf"]).kws(kws).flags(flags)) 125 | .await 126 | } 127 | 128 | /// Qp queries a package supplied through a file supplied on the command 129 | /// line rather than an entry in the package management database. 130 | async fn qp(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 131 | self.run(Cmd::new(["rpm", "-qip"]).kws(kws).flags(flags)) 132 | .await 133 | } 134 | 135 | /// Qs searches locally installed package for names or descriptions. 136 | // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, 137 | // when including multiple search terms, only packages with descriptions 138 | // matching ALL of those terms are returned. 139 | // TODO: Is this right? 140 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 141 | self.search_regex(Cmd::new(["rpm", "-qa"]).flags(flags), kws) 142 | .await 143 | } 144 | 145 | /// Qu lists packages which have an update available. 146 | async fn qu(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 147 | self.run(Cmd::new(["dnf", "list", "updates"]).kws(kws).flags(flags)) 148 | .await 149 | } 150 | 151 | /// R removes a single package, leaving all of its dependencies installed. 152 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 153 | Cmd::with_sudo(["dnf", "remove"]) 154 | .kws(kws) 155 | .flags(flags) 156 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 157 | .await 158 | } 159 | 160 | /// S installs one or more packages by name. 161 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 162 | Cmd::with_sudo(["dnf", "install"]) 163 | .kws(kws) 164 | .flags(flags) 165 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 166 | .await 167 | } 168 | 169 | /// Sc removes all the cached packages that are not currently installed, and 170 | /// the unused sync database. 171 | async fn sc(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 172 | Cmd::new(["dnf", "clean", "expire-cache"]) 173 | .flags(flags) 174 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT_CUSTOM)) 175 | .await 176 | } 177 | 178 | /// Scc removes all files from the cache. 179 | async fn scc(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 180 | Cmd::new(["dnf", "clean", "packages"]) 181 | .flags(flags) 182 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT_CUSTOM)) 183 | .await 184 | } 185 | 186 | /// Sccc performs a deeper cleaning of the cache than `Scc` (if applicable). 187 | async fn sccc(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 188 | Cmd::new(["dnf", "clean", "all"]) 189 | .flags(flags) 190 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT_CUSTOM)) 191 | .await 192 | } 193 | 194 | /// Si displays remote package information: name, version, description, etc. 195 | 196 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 197 | self.run(Cmd::new(["dnf", "info"]).kws(kws).flags(flags)) 198 | .await 199 | } 200 | 201 | /// Sii displays packages which require X to be installed, aka reverse 202 | /// dependencies. 203 | async fn sii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 204 | Cmd::new(["dnf", "repoquery", "--whatdepends"]) 205 | .kws(kws) 206 | .flags(flags) 207 | .pipe(|cmd| self.run(cmd)) 208 | .await 209 | } 210 | 211 | /// Sg lists all packages belonging to the GROUP. 212 | async fn sg(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 213 | Cmd::new(if kws.is_empty() { 214 | ["dnf", "group", "list"] 215 | } else { 216 | ["dnf", "group", "info"] 217 | }) 218 | .kws(kws) 219 | .flags(flags) 220 | .pipe(|cmd| self.run(cmd)) 221 | .await 222 | } 223 | 224 | /// Sl displays a list of all packages in all installation sources that are 225 | /// handled by the package management. 226 | async fn sl(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 227 | Cmd::new(["dnf", "list", "--available"]) 228 | .kws(kws) 229 | .flags(flags) 230 | .pipe(|cmd| self.run(cmd)) 231 | .await 232 | } 233 | 234 | /// Ss searches for package(s) by searching the expression in name, 235 | /// description, short description. 236 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 237 | self.run(Cmd::new(["dnf", "search"]).kws(kws).flags(flags)) 238 | .await 239 | } 240 | 241 | /// Su updates outdated packages. 242 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 243 | Cmd::with_sudo(["dnf", "upgrade"]) 244 | .kws(kws) 245 | .flags(flags) 246 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 247 | .await 248 | } 249 | 250 | /// Suy refreshes the local package database, then updates outdated 251 | /// packages. 252 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 253 | self.su(kws, flags).await 254 | } 255 | 256 | /// Sw retrieves all packages from the server, but does not install/upgrade 257 | /// anything. 258 | async fn sw(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 259 | Cmd::with_sudo(["dnf", "install", "--downloadonly"]) 260 | .kws(kws) 261 | .flags(flags) 262 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 263 | .await 264 | } 265 | 266 | /// Sy refreshes the local package database. 267 | async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 268 | self.sc(&[], flags).await?; 269 | self.run(Cmd::new(["dnf", "check-update"]).flags(flags)) 270 | .await?; 271 | if !kws.is_empty() { 272 | self.s(kws, flags).await?; 273 | } 274 | Ok(()) 275 | } 276 | 277 | /// U upgrades or adds package(s) to the system and installs the required 278 | /// dependencies from sync repositories. 279 | async fn u(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 280 | self.s(kws, flags).await 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/pm/emerge.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use indoc::indoc; 7 | use itertools::Itertools; 8 | use tap::prelude::*; 9 | 10 | use super::{NoCacheStrategy, Pm, PmHelper, PromptStrategy, Strategy}; 11 | use crate::{config::Config, error::Result, exec::Cmd}; 12 | 13 | macro_rules! doc_self { 14 | () => { 15 | indoc! {" 16 | The [Portage Package Manager](https://wiki.gentoo.org/wiki/Portage). 17 | "} 18 | }; 19 | } 20 | use doc_self; 21 | 22 | #[doc = doc_self!()] 23 | #[derive(Debug)] 24 | pub struct Emerge { 25 | cfg: Config, 26 | } 27 | 28 | static STRAT_ASK: LazyLock = LazyLock::new(|| Strategy { 29 | prompt: PromptStrategy::native_confirm(["--ask"]), 30 | ..Strategy::default() 31 | }); 32 | 33 | static STRAT_INTERACTIVE: LazyLock = LazyLock::new(|| Strategy { 34 | prompt: PromptStrategy::native_confirm(["--interactive"]), 35 | ..Strategy::default() 36 | }); 37 | 38 | static STRAT_INSTALL: LazyLock = LazyLock::new(|| Strategy { 39 | prompt: PromptStrategy::native_confirm(["--ask"]), 40 | no_cache: NoCacheStrategy::Scc, 41 | ..Strategy::default() 42 | }); 43 | 44 | impl Emerge { 45 | #[must_use] 46 | #[allow(missing_docs)] 47 | pub const fn new(cfg: Config) -> Self { 48 | Self { cfg } 49 | } 50 | } 51 | 52 | #[async_trait] 53 | impl Pm for Emerge { 54 | /// Gets the name of the package manager. 55 | fn name(&self) -> &'static str { 56 | "emerge" 57 | } 58 | 59 | fn cfg(&self) -> &Config { 60 | &self.cfg 61 | } 62 | 63 | /// Q generates a list of installed packages. 64 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 65 | self.qs(kws, flags).await 66 | } 67 | 68 | /// Qi displays local package information: name, version, description, etc. 69 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 70 | self.si(kws, flags).await 71 | } 72 | 73 | /// Ql displays files provided by local package. 74 | async fn ql(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 75 | self.run(Cmd::new(["qlist"]).kws(kws).flags(flags)).await 76 | } 77 | 78 | /// Qo queries the package which provides FILE. 79 | async fn qo(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 80 | self.run(Cmd::new(["qfile"]).kws(kws).flags(flags)).await 81 | } 82 | 83 | /// Qs searches locally installed package for names or descriptions. 84 | // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, 85 | // when including multiple search terms, only packages with descriptions 86 | // matching ALL of those terms are returned. 87 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 88 | self.run(Cmd::new(["qlist", "-I"]).kws(kws).flags(flags)) 89 | .await 90 | } 91 | 92 | /// Qu lists packages which have an update available. 93 | async fn qu(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 94 | self.run(Cmd::new(["emerge", "-uDNp", "@world"]).flags(flags)) 95 | .await 96 | } 97 | 98 | /// R removes a single package, leaving all of its dependencies installed. 99 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 100 | Cmd::with_sudo(["emerge", "--unmerge"]) 101 | .kws(kws) 102 | .flags(flags) 103 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_ASK)) 104 | .await 105 | } 106 | 107 | /// Rs removes a package and its dependencies which are not required by any 108 | /// other installed package, and not explicitly installed by the user. 109 | async fn rs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 110 | Cmd::with_sudo(["emerge", "--depclean"]) 111 | .kws(kws) 112 | .flags(flags) 113 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_ASK)) 114 | .await 115 | } 116 | 117 | /// S installs one or more packages by name. 118 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 119 | Cmd::with_sudo(["emerge"]) 120 | .kws(kws) 121 | .flags(flags) 122 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 123 | .await 124 | } 125 | 126 | /// Sc removes all the cached packages that are not currently installed, and 127 | /// the unused sync database. 128 | async fn sc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 129 | Cmd::with_sudo(["eclean-dist"]) 130 | .kws(kws) 131 | .flags(flags) 132 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INTERACTIVE)) 133 | .await 134 | } 135 | 136 | /// Scc removes all files from the cache. 137 | async fn scc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 138 | self.sc(kws, flags).await 139 | } 140 | 141 | /// Si displays remote package information: name, version, description, etc. 142 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 143 | let kws = kws.iter().map(|kw| format!("^{kw}$")).collect_vec(); 144 | self.run(Cmd::new(["emerge", "-s"]).kws(kws).flags(flags)) 145 | .await 146 | } 147 | 148 | /// Ss searches for package(s) by searching the expression in name, 149 | /// description, short description. 150 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 151 | self.run(Cmd::new(["qsearch"]).kws(kws).flags(flags)).await 152 | } 153 | 154 | /// Su updates outdated packages. 155 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 156 | Cmd::with_sudo(["emerge", "-uDN"]) 157 | .kws(if kws.is_empty() { &["@world"][..] } else { kws }) 158 | .flags(flags) 159 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 160 | .await 161 | } 162 | 163 | /// Suy refreshes the local package database, then updates outdated 164 | /// packages. 165 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 166 | self.sy(&[], flags).await?; 167 | self.su(kws, flags).await 168 | } 169 | 170 | /// Sy refreshes the local package database. 171 | async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 172 | self.run(Cmd::with_sudo(["emerge", "--sync"]).flags(flags)) 173 | .await?; 174 | if !kws.is_empty() { 175 | self.s(kws, flags).await?; 176 | } 177 | Ok(()) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/pm/pip.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use indoc::indoc; 7 | use tap::prelude::*; 8 | 9 | use super::{Pm, PmHelper, PromptStrategy, Strategy}; 10 | use crate::{ 11 | config::Config, 12 | error::{Error, Result}, 13 | exec::Cmd, 14 | }; 15 | 16 | macro_rules! doc_self { 17 | () => { 18 | indoc! {" 19 | The [Python Package Installer](https://pip.pypa.io/). 20 | "} 21 | }; 22 | } 23 | use doc_self; 24 | 25 | #[doc = doc_self!()] 26 | #[derive(Debug)] 27 | pub struct Pip { 28 | cfg: Config, 29 | } 30 | 31 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 32 | prompt: PromptStrategy::CustomPrompt, 33 | ..Strategy::default() 34 | }); 35 | 36 | static STRAT_UNINSTALL: LazyLock = LazyLock::new(|| Strategy { 37 | prompt: PromptStrategy::native_no_confirm(["-y"]), 38 | ..Strategy::default() 39 | }); 40 | 41 | impl Pip { 42 | #[must_use] 43 | #[allow(missing_docs)] 44 | pub const fn new(cfg: Config) -> Self { 45 | Self { cfg } 46 | } 47 | 48 | /// Returns the command used to invoke [`Pip`], eg. `pip`, `pip3`. 49 | #[must_use] 50 | fn cmd(&self) -> &str { 51 | self.cfg 52 | .default_pm 53 | .as_deref() 54 | .expect("default package manager should have been assigned before initialization") 55 | } 56 | } 57 | 58 | #[async_trait] 59 | impl Pm for Pip { 60 | /// Gets the name of the package manager. 61 | fn name(&self) -> &'static str { 62 | "pip" 63 | } 64 | 65 | fn cfg(&self) -> &Config { 66 | &self.cfg 67 | } 68 | 69 | /// Q generates a list of installed packages. 70 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 71 | if kws.is_empty() { 72 | self.run(Cmd::new([self.cmd(), "list"]).flags(flags)).await 73 | } else { 74 | self.qs(kws, flags).await 75 | } 76 | } 77 | 78 | /// Qi displays local package information: name, version, description, etc. 79 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 80 | self.run(Cmd::new([self.cmd(), "show"]).kws(kws).flags(flags)) 81 | .await 82 | } 83 | 84 | /// Qs searches locally installed package for names or descriptions. 85 | // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, 86 | // when including multiple search terms, only packages with descriptions 87 | // matching ALL of those terms are returned. 88 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 89 | self.search_regex(Cmd::new([self.cmd(), "list"]).flags(flags), kws) 90 | .await 91 | } 92 | 93 | /// Qu lists packages which have an update available. 94 | async fn qu(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 95 | Cmd::new([self.cmd(), "list", "--outdated"]) 96 | .kws(kws) 97 | .flags(flags) 98 | .pipe(|cmd| self.run(cmd)) 99 | .await 100 | } 101 | 102 | /// R removes a single package, leaving all of its dependencies installed. 103 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 104 | Cmd::new([self.cmd(), "uninstall"]) 105 | .kws(kws) 106 | .flags(flags) 107 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_UNINSTALL)) 108 | .await 109 | } 110 | 111 | /// S installs one or more packages by name. 112 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 113 | Cmd::new([self.cmd(), "install"]) 114 | .kws(kws) 115 | .flags(flags) 116 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 117 | .await 118 | } 119 | 120 | /// Sc removes all the cached packages that are not currently installed, and 121 | /// the unused sync database. 122 | async fn sc(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 123 | self.run(Cmd::new([self.cmd(), "cache", "purge"]).flags(flags)) 124 | .await 125 | } 126 | 127 | /// Su updates outdated packages. 128 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 129 | if kws.is_empty() { 130 | return Err(Error::OperationUnimplementedError { 131 | op: "su".into(), 132 | pm: self.name().into(), 133 | }); 134 | } 135 | Cmd::new([self.cmd(), "install", "--upgrade"]) 136 | .kws(kws) 137 | .flags(flags) 138 | .pipe(|cmd| self.run(cmd)) 139 | .await 140 | } 141 | 142 | /// Sw retrieves all packages from the server, but does not install/upgrade 143 | /// anything. 144 | async fn sw(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 145 | Cmd::new([self.cmd(), "download"]) 146 | .kws(kws) 147 | .flags(flags) 148 | .pipe(|cmd| self.run(cmd)) 149 | .await 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/pm/pkcon.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use futures::prelude::*; 7 | use indoc::indoc; 8 | use tap::prelude::*; 9 | 10 | use super::{Pm, PmHelper, PromptStrategy, Strategy}; 11 | use crate::{config::Config, error::Result, exec::Cmd}; 12 | 13 | macro_rules! doc_self { 14 | () => { 15 | indoc! {" 16 | The [PackageKit Console Client](https://www.freedesktop.org/software/PackageKit). 17 | "} 18 | }; 19 | } 20 | use doc_self; 21 | 22 | #[doc = doc_self!()] 23 | #[derive(Debug)] 24 | pub struct Pkcon { 25 | cfg: Config, 26 | } 27 | 28 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 29 | prompt: PromptStrategy::native_no_confirm(["-y"]), 30 | ..Strategy::default() 31 | }); 32 | 33 | impl Pkcon { 34 | #[must_use] 35 | #[allow(missing_docs)] 36 | pub const fn new(cfg: Config) -> Self { 37 | Self { cfg } 38 | } 39 | } 40 | 41 | #[async_trait] 42 | impl Pm for Pkcon { 43 | /// Gets the name of the package manager. 44 | fn name(&self) -> &'static str { 45 | "pkcon" 46 | } 47 | 48 | fn cfg(&self) -> &Config { 49 | &self.cfg 50 | } 51 | 52 | /// Q generates a list of installed packages. 53 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 54 | if kws.is_empty() { 55 | Cmd::new(["pkcon", "get-packages", "--filter", "installed"]) 56 | .kws(kws) 57 | .flags(flags) 58 | .pipe(|cmd| self.run(cmd)) 59 | .await 60 | } else { 61 | self.qs(kws, flags).await 62 | } 63 | } 64 | 65 | /// Qc shows the changelog of a package. 66 | async fn qc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 67 | Cmd::new(["pkcon", "get-update-detail"]) 68 | .kws(kws) 69 | .flags(flags) 70 | .pipe(|cmd| self.run(cmd)) 71 | .await 72 | } 73 | 74 | /// Qi displays local package information: name, version, description, etc. 75 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 76 | self.si(kws, flags).await 77 | } 78 | 79 | /// Qii displays local packages which require X to be installed, aka local 80 | /// reverse dependencies. 81 | async fn qii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 82 | self.sii(kws, flags).await 83 | } 84 | 85 | /// Ql displays files provided by local package. 86 | async fn ql(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 87 | self.run(Cmd::new(["pkcon", "get-files"]).kws(kws).flags(flags)) 88 | .await 89 | } 90 | 91 | /// Qo queries the package which provides FILE. 92 | async fn qo(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 93 | self.run(Cmd::new(["pkcon", "what-provides"]).kws(kws).flags(flags)) 94 | .await 95 | } 96 | 97 | /// Qs searches locally installed package for names or descriptions. 98 | // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, 99 | // when including multiple search terms, only packages with descriptions 100 | // matching ALL of those terms are returned. 101 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 102 | Cmd::new(["pkcon", "get-packages", "--filter", "installed"]) 103 | .flags(flags) 104 | .pipe(|cmd| self.search_regex(cmd, kws)) 105 | .await 106 | } 107 | 108 | /// Qu lists packages which have an update available. 109 | async fn qu(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 110 | Cmd::with_sudo(["pkcon", "get-updates"]) 111 | .kws(kws) 112 | .flags(flags) 113 | .pipe(|cmd| self.run(cmd)) 114 | .await 115 | } 116 | 117 | /// R removes a single package, leaving all of its dependencies installed. 118 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 119 | stream::iter(kws) 120 | .map(Ok) 121 | .try_for_each(|kw| { 122 | Cmd::with_sudo(["pkcon", "remove"]) 123 | .kws([kw]) 124 | .flags(flags) 125 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 126 | }) 127 | .await 128 | } 129 | 130 | /// Rs removes a package and its dependencies which are not required by any 131 | /// other installed package, and not explicitly installed by the user. 132 | async fn rs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 133 | stream::iter(kws) 134 | .map(Ok) 135 | .try_for_each(|kw| { 136 | Cmd::with_sudo(["pkcon", "remove", "--autoremove"]) 137 | .kws([kw]) 138 | .flags(flags) 139 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 140 | }) 141 | .await 142 | } 143 | 144 | /// S installs one or more packages by name. 145 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 146 | Cmd::with_sudo(if self.cfg.needed { 147 | &["pkcon", "install"][..] 148 | } else { 149 | &["pkcon", "install", "--allow-reinstall"][..] 150 | }) 151 | .kws(kws) 152 | .flags(flags) 153 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 154 | .await 155 | } 156 | 157 | /// Si displays remote package information: name, version, description, etc. 158 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 159 | self.run(Cmd::new(["pkcon", "get-details"]).kws(kws).flags(flags)) 160 | .await 161 | } 162 | 163 | /// Sii displays packages which require X to be installed, aka reverse 164 | /// dependencies. 165 | async fn sii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 166 | self.run(Cmd::new(["pkcon", "required-by"]).kws(kws).flags(flags)) 167 | .await 168 | } 169 | 170 | /// Ss searches for package(s) by searching the expression in name, 171 | /// description, short description. 172 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 173 | self.run(Cmd::new(["pkcon", "search", "name"]).kws(kws).flags(flags)) 174 | .await 175 | } 176 | 177 | /// Su updates outdated packages. 178 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 179 | Cmd::with_sudo(["pkcon", "update"]) 180 | .kws(kws) 181 | .flags(flags) 182 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 183 | .await 184 | } 185 | 186 | /// Suy refreshes the local package database, then updates outdated 187 | /// packages. 188 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 189 | self.sy(&[], flags).await?; 190 | self.su(kws, flags).await 191 | } 192 | 193 | /// Sw retrieves all packages from the server, but does not install/upgrade 194 | /// anything. 195 | async fn sw(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 196 | Cmd::with_sudo(["pkcon", "install", "--only-download"]) 197 | .kws(kws) 198 | .flags(flags) 199 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 200 | .await 201 | } 202 | 203 | /// Sy refreshes the local package database. 204 | async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 205 | self.run(Cmd::with_sudo(["pkcon", "refresh"]).flags(flags)) 206 | .await?; 207 | if !kws.is_empty() { 208 | self.s(kws, flags).await?; 209 | } 210 | Ok(()) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/pm/port.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use indoc::indoc; 7 | use tap::prelude::*; 8 | 9 | use super::{NoCacheStrategy, Pm, PmHelper, PromptStrategy, Strategy}; 10 | use crate::{config::Config, error::Result, exec::Cmd}; 11 | 12 | macro_rules! doc_self { 13 | () => { 14 | indoc! {" 15 | The [MacPorts Package Manager](https://www.macports.org/). 16 | "} 17 | }; 18 | } 19 | use doc_self; 20 | 21 | #[doc = doc_self!()] 22 | #[derive(Debug)] 23 | pub struct Port { 24 | cfg: Config, 25 | } 26 | 27 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 28 | prompt: PromptStrategy::CustomPrompt, 29 | ..Strategy::default() 30 | }); 31 | 32 | static STRAT_INSTALL: LazyLock = LazyLock::new(|| Strategy { 33 | prompt: PromptStrategy::CustomPrompt, 34 | no_cache: NoCacheStrategy::Scc, 35 | ..Strategy::default() 36 | }); 37 | 38 | impl Port { 39 | #[must_use] 40 | #[allow(missing_docs)] 41 | pub const fn new(cfg: Config) -> Self { 42 | Self { cfg } 43 | } 44 | } 45 | 46 | #[async_trait] 47 | impl Pm for Port { 48 | /// Gets the name of the package manager. 49 | fn name(&self) -> &'static str { 50 | "port" 51 | } 52 | 53 | fn cfg(&self) -> &Config { 54 | &self.cfg 55 | } 56 | 57 | /// Q generates a list of installed packages. 58 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 59 | self.run(Cmd::new(["port", "installed"]).kws(kws).flags(flags)) 60 | .await 61 | } 62 | 63 | /// Qc shows the changelog of a package. 64 | async fn qc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 65 | self.run(Cmd::new(["port", "log"]).kws(kws).flags(flags)) 66 | .await 67 | } 68 | 69 | /// Qi displays local package information: name, version, description, etc. 70 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 71 | self.si(kws, flags).await 72 | } 73 | 74 | /// Ql displays files provided by local package. 75 | async fn ql(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 76 | self.run(Cmd::new(["port", "contents"]).kws(kws).flags(flags)) 77 | .await 78 | } 79 | 80 | /// Qo queries the package which provides FILE. 81 | async fn qo(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 82 | self.run(Cmd::new(["port", "provides"]).kws(kws).flags(flags)) 83 | .await 84 | } 85 | 86 | /// Qs searches locally installed package for names or descriptions. 87 | // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, 88 | // when including multiple search terms, only packages with descriptions 89 | // matching ALL of those terms are returned. 90 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 91 | self.run(Cmd::new(["port", "-v", "installed"]).kws(kws).flags(flags)) 92 | .await 93 | } 94 | 95 | /// Qu lists packages which have an update available. 96 | async fn qu(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 97 | self.run(Cmd::new(["port", "outdated"]).kws(kws).flags(flags)) 98 | .await 99 | } 100 | 101 | /// R removes a single package, leaving all of its dependencies installed. 102 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 103 | Cmd::with_sudo(["port", "uninstall"]) 104 | .kws(kws) 105 | .flags(flags) 106 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 107 | .await 108 | } 109 | 110 | /// Rss removes a package and its dependencies which are not required by any 111 | /// other installed package. 112 | async fn rss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 113 | Cmd::with_sudo(["port", "uninstall", "--follow-dependencies"]) 114 | .kws(kws) 115 | .flags(flags) 116 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 117 | .await 118 | } 119 | 120 | /// S installs one or more packages by name. 121 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 122 | Cmd::with_sudo(["port", "install"]) 123 | .kws(kws) 124 | .flags(flags) 125 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 126 | .await 127 | } 128 | 129 | /// Sc removes all the cached packages that are not currently installed, and 130 | /// the unused sync database. 131 | async fn sc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 132 | Cmd::with_sudo(if flags.is_empty() { 133 | &["port", "clean", "--all", "inactive"][..] 134 | } else { 135 | &["port", "clean", "--all"][..] 136 | }) 137 | .kws(kws) 138 | .flags(flags) 139 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 140 | .await 141 | } 142 | 143 | /// Scc removes all files from the cache. 144 | async fn scc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 145 | Cmd::with_sudo(if flags.is_empty() { 146 | &["port", "clean", "--all", "installed"][..] 147 | } else { 148 | &["port", "clean", "--all"][..] 149 | }) 150 | .kws(kws) 151 | .flags(flags) 152 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 153 | .await 154 | } 155 | 156 | /// Si displays remote package information: name, version, description, etc. 157 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 158 | self.run(Cmd::new(["port", "info"]).kws(kws).flags(flags)) 159 | .await 160 | } 161 | 162 | /// Ss searches for package(s) by searching the expression in name, 163 | /// description, short description. 164 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 165 | self.run(Cmd::new(["port", "search"]).kws(kws).flags(flags)) 166 | .await 167 | } 168 | 169 | /// Su updates outdated packages. 170 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 171 | Cmd::with_sudo(if flags.is_empty() { 172 | &["port", "upgrade", "outdated"][..] 173 | } else { 174 | &["port", "upgrade"][..] 175 | }) 176 | .kws(kws) 177 | .flags(flags) 178 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 179 | .await 180 | } 181 | 182 | /// Suy refreshes the local package database, then updates outdated 183 | /// packages. 184 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 185 | self.sy(&[], flags).await?; 186 | self.su(kws, flags).await 187 | } 188 | 189 | /// Sy refreshes the local package database. 190 | async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 191 | self.run(Cmd::new(["port", "selfupdate"]).flags(flags)) 192 | .await?; 193 | if !kws.is_empty() { 194 | self.s(kws, flags).await?; 195 | } 196 | Ok(()) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/pm/scoop.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use indoc::indoc; 7 | use tap::prelude::*; 8 | use which::which; 9 | 10 | use super::{NoCacheStrategy, Pm, PmHelper, PromptStrategy, Strategy}; 11 | use crate::{config::Config, error::Result, exec::Cmd}; 12 | 13 | macro_rules! doc_self { 14 | () => { 15 | indoc! {" 16 | The [Scoop CLI Installer](https://scoop.sh/). 17 | "} 18 | }; 19 | } 20 | use doc_self; 21 | 22 | #[doc = doc_self!()] 23 | #[derive(Debug)] 24 | pub struct Scoop { 25 | cfg: Config, 26 | shell: String, 27 | } 28 | 29 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 30 | prompt: PromptStrategy::CustomPrompt, 31 | ..Strategy::default() 32 | }); 33 | 34 | static STRAT_INSTALL: LazyLock = LazyLock::new(|| Strategy { 35 | prompt: PromptStrategy::CustomPrompt, 36 | no_cache: NoCacheStrategy::Scc, 37 | ..Strategy::default() 38 | }); 39 | 40 | impl Scoop { 41 | #[must_use] 42 | #[allow(missing_docs)] 43 | pub fn new(cfg: Config) -> Self { 44 | let shell = which("pwsh").and(Ok("pwsh")).unwrap_or("powershell"); 45 | Self::with_shell(cfg, shell) 46 | } 47 | 48 | #[must_use] 49 | #[allow(missing_docs)] 50 | pub fn with_shell(cfg: Config, shell: &str) -> Self { 51 | Self { 52 | cfg, 53 | shell: shell.to_owned(), 54 | } 55 | } 56 | } 57 | 58 | // Windows is so special! It's better not to "sudo" automatically. 59 | #[async_trait] 60 | impl Pm for Scoop { 61 | /// Gets the name of the package manager. 62 | fn name(&self) -> &'static str { 63 | "scoop" 64 | } 65 | 66 | fn cfg(&self) -> &Config { 67 | &self.cfg 68 | } 69 | 70 | /// Q generates a list of installed packages. 71 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 72 | if kws.is_empty() { 73 | self.run(Cmd::new([&self.shell, "-Command", "scoop", "list"]).flags(flags)) 74 | .await 75 | } else { 76 | self.qs(kws, flags).await 77 | } 78 | } 79 | 80 | /// Qi displays local package information: name, version, description, etc. 81 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 82 | self.si(kws, flags).await 83 | } 84 | 85 | /// Qs searches locally installed package for names or descriptions. 86 | // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, 87 | // when including multiple search terms, only packages with descriptions 88 | // matching ALL of those terms are returned. 89 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 90 | Cmd::new([&self.shell, "-Command", "scoop", "list"]) 91 | .flags(flags) 92 | .pipe(|cmd| self.search_regex_with_header(cmd, kws, 4)) 93 | .await 94 | } 95 | 96 | /// Qu lists packages which have an update available. 97 | async fn qu(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 98 | Cmd::new([&self.shell, "-Command", "scoop", "status"]) 99 | .kws(kws) 100 | .flags(flags) 101 | .pipe(|cmd| self.run(cmd)) 102 | .await 103 | } 104 | 105 | /// R removes a single package, leaving all of its dependencies installed. 106 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 107 | Cmd::new([&self.shell, "-Command", "scoop", "uninstall"]) 108 | .kws(kws) 109 | .flags(flags) 110 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 111 | .await 112 | } 113 | 114 | /// Rn removes a package and skips the generation of configuration backup 115 | /// files. 116 | async fn rn(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 117 | Cmd::new([&self.shell, "-Command", "scoop", "uninstall", "--purge"]) 118 | .kws(kws) 119 | .flags(flags) 120 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 121 | .await 122 | } 123 | 124 | /// S installs one or more packages by name. 125 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 126 | Cmd::new([&self.shell, "-Command", "scoop", "install"]) 127 | .kws(kws) 128 | .flags(flags) 129 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 130 | .await 131 | } 132 | 133 | /// Sc removes all the cached packages that are not currently installed, and 134 | /// the unused sync database. 135 | async fn sc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 136 | Cmd::new([&self.shell, "-Command", "scoop", "cache", "rm"]) 137 | .kws(if kws.is_empty() { &["*"][..] } else { kws }) 138 | .flags(flags) 139 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 140 | .await 141 | } 142 | 143 | /// Scc removes all files from the cache. 144 | async fn scc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 145 | self.sc(kws, flags).await 146 | } 147 | 148 | /// Si displays remote package information: name, version, description, etc. 149 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 150 | Cmd::new([&self.shell, "-Command", "scoop", "info"]) 151 | .kws(kws) 152 | .flags(flags) 153 | .pipe(|cmd| self.run(cmd)) 154 | .await 155 | } 156 | 157 | /// Ss searches for package(s) by searching the expression in name, 158 | /// description, short description. 159 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 160 | Cmd::new([&self.shell, "-Command", "scoop", "search"]) 161 | .kws(kws) 162 | .flags(flags) 163 | .pipe(|cmd| self.run(cmd)) 164 | .await 165 | } 166 | 167 | /// Su updates outdated packages. 168 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 169 | Cmd::new([&self.shell, "-Command", "scoop", "update"]) 170 | .kws(if kws.is_empty() { &["*"][..] } else { kws }) 171 | .flags(flags) 172 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 173 | .await 174 | } 175 | 176 | /// Suy refreshes the local package database, then updates outdated 177 | /// packages. 178 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 179 | self.sy(&[], flags).await?; 180 | self.su(kws, flags).await 181 | } 182 | 183 | /// Sy refreshes the local package database. 184 | async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 185 | self.run(Cmd::new([&self.shell, "-Command", "scoop", "update"]).flags(flags)) 186 | .await?; 187 | if !kws.is_empty() { 188 | self.s(kws, flags).await?; 189 | } 190 | Ok(()) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/pm/tlmgr.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use indoc::indoc; 7 | use tap::prelude::*; 8 | 9 | use super::{DryRunStrategy, Pm, PmHelper, Strategy}; 10 | use crate::{config::Config, error::Result, exec::Cmd}; 11 | 12 | macro_rules! doc_self { 13 | () => { 14 | indoc! {" 15 | The [TexLive Package Manager](https://www.tug.org/texlive/tlmgr.html). 16 | "} 17 | }; 18 | } 19 | use doc_self; 20 | 21 | #[doc = doc_self!()] 22 | #[derive(Debug)] 23 | pub struct Tlmgr { 24 | cfg: Config, 25 | } 26 | 27 | static STRAT_CHECK_DRY: LazyLock = LazyLock::new(|| Strategy { 28 | dry_run: DryRunStrategy::with_flags(["--dry-run"]), 29 | ..Strategy::default() 30 | }); 31 | 32 | impl Tlmgr { 33 | #[must_use] 34 | #[allow(missing_docs)] 35 | pub const fn new(cfg: Config) -> Self { 36 | Self { cfg } 37 | } 38 | } 39 | 40 | #[async_trait] 41 | impl Pm for Tlmgr { 42 | /// Gets the name of the package manager. 43 | fn name(&self) -> &'static str { 44 | "tlmgr" 45 | } 46 | 47 | fn cfg(&self) -> &Config { 48 | &self.cfg 49 | } 50 | 51 | /// Q generates a list of installed packages. 52 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 53 | self.qi(kws, flags).await 54 | } 55 | 56 | /// Qi displays local package information: name, version, description, etc. 57 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 58 | Cmd::new(["tlmgr", "info", "--only-installed"]) 59 | .kws(kws) 60 | .flags(flags) 61 | .pipe(|cmd| self.run(cmd)) 62 | .await 63 | } 64 | 65 | /// Qk verifies one or more packages. 66 | async fn qk(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 67 | self.run(Cmd::new(["tlmgr", "check", "files"]).flags(flags)) 68 | .await 69 | } 70 | 71 | /// Ql displays files provided by local package. 72 | async fn ql(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 73 | Cmd::new(["tlmgr", "info", "--only-installed", "--list"]) 74 | .kws(kws) 75 | .flags(flags) 76 | .pipe(|cmd| self.run(cmd)) 77 | .await 78 | } 79 | 80 | /// R removes a single package, leaving all of its dependencies installed. 81 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 82 | Cmd::new(["tlmgr", "remove"]) 83 | .kws(kws) 84 | .flags(flags) 85 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_CHECK_DRY)) 86 | .await 87 | } 88 | 89 | /// S installs one or more packages by name. 90 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 91 | Cmd::new(["tlmgr", "install"]) 92 | .kws(kws) 93 | .flags(flags) 94 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_CHECK_DRY)) 95 | .await 96 | } 97 | 98 | /// Si displays remote package information: name, version, description, etc. 99 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 100 | self.run(Cmd::new(["tlmgr", "info"]).kws(kws).flags(flags)) 101 | .await 102 | } 103 | 104 | /// Sl displays a list of all packages in all installation sources that are 105 | /// handled by the package management. 106 | async fn sl(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 107 | self.run(Cmd::new(["tlmgr", "info"]).flags(flags)).await 108 | } 109 | 110 | /// Ss searches for package(s) by searching the expression in name, 111 | /// description, short description. 112 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 113 | Cmd::new(["tlmgr", "search", "--global"]) 114 | .kws(kws) 115 | .flags(flags) 116 | .pipe(|cmd| self.run(cmd)) 117 | .await 118 | } 119 | 120 | /// Su updates outdated packages. 121 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 122 | Cmd::new(if kws.is_empty() { 123 | &["tlmgr", "update", "--self", "--all"][..] 124 | } else { 125 | &["tlmgr", "update", "--self"][..] 126 | }) 127 | .kws(kws) 128 | .flags(flags) 129 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_CHECK_DRY)) 130 | .await 131 | } 132 | 133 | /// Suy refreshes the local package database, then updates outdated 134 | /// packages. 135 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 136 | self.su(kws, flags).await 137 | } 138 | 139 | /// U upgrades or adds package(s) to the system and installs the required 140 | /// dependencies from sync repositories. 141 | async fn u(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 142 | Cmd::new(["tlmgr", "install", "--file"]) 143 | .kws(kws) 144 | .flags(flags) 145 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_CHECK_DRY)) 146 | .await 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/pm/unknown.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use async_trait::async_trait; 4 | use indoc::indoc; 5 | 6 | use super::Pm; 7 | use crate::config::Config; 8 | 9 | macro_rules! doc_self { 10 | () => { 11 | indoc! {" 12 | An empty mapping for unidentified package managers. 13 | "} 14 | }; 15 | } 16 | use doc_self; 17 | 18 | #[doc = doc_self!()] 19 | #[derive(Debug)] 20 | pub struct Unknown { 21 | name: String, 22 | cfg: Config, 23 | } 24 | 25 | impl Unknown { 26 | #[must_use] 27 | /// Creates a new [`Unknown`] package manager with the given name. 28 | pub(crate) fn new(name: &str) -> Self { 29 | Self { 30 | name: format!("unknown package manager: {name}"), 31 | cfg: Config::default(), 32 | } 33 | } 34 | } 35 | 36 | #[async_trait] 37 | impl Pm for Unknown { 38 | /// Gets the name of the package manager. 39 | fn name(&self) -> &str { 40 | &self.name 41 | } 42 | 43 | fn cfg(&self) -> &Config { 44 | &self.cfg 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/pm/winget.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use indoc::indoc; 7 | use tap::prelude::*; 8 | 9 | use super::{Pm, PmHelper, PromptStrategy, Strategy}; 10 | use crate::{config::Config, error::Result, exec::Cmd}; 11 | 12 | macro_rules! doc_self { 13 | () => { 14 | indoc! {" 15 | The [Windows Package Manager CLI](https://github.com/microsoft/winget-cli). 16 | "} 17 | }; 18 | } 19 | use doc_self; 20 | 21 | #[doc = doc_self!()] 22 | #[derive(Debug)] 23 | pub struct Winget { 24 | cfg: Config, 25 | } 26 | 27 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 28 | prompt: PromptStrategy::CustomPrompt, 29 | ..Strategy::default() 30 | }); 31 | 32 | static STRAT_INSTALL: LazyLock = LazyLock::new(|| Strategy { 33 | prompt: PromptStrategy::CustomPrompt, 34 | ..Strategy::default() 35 | }); 36 | 37 | impl Winget { 38 | #[must_use] 39 | #[allow(missing_docs)] 40 | pub const fn new(cfg: Config) -> Self { 41 | Self { cfg } 42 | } 43 | } 44 | 45 | // Windows is so special! It's better not to "sudo" automatically. 46 | #[async_trait] 47 | impl Pm for Winget { 48 | /// Gets the name of the package manager. 49 | fn name(&self) -> &'static str { 50 | "winget" 51 | } 52 | 53 | fn cfg(&self) -> &Config { 54 | &self.cfg 55 | } 56 | 57 | /// Q generates a list of installed packages. 58 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 59 | if kws.is_empty() { 60 | self.run(Cmd::new(["winget", "list", "--accept-source-agreements"]).flags(flags)) 61 | .await 62 | } else { 63 | self.qs(kws, flags).await 64 | } 65 | } 66 | 67 | /// Qi displays local package information: name, version, description, etc. 68 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 69 | self.si(kws, flags).await 70 | } 71 | 72 | /// Qs searches locally installed package for names or descriptions. 73 | // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, 74 | // when including multiple search terms, only packages with descriptions 75 | // matching ALL of those terms are returned. 76 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 77 | Cmd::new(["winget", "list", "--accept-source-agreements"]) 78 | .flags(flags) 79 | .pipe(|cmd| self.search_regex(cmd, kws)) 80 | .await 81 | } 82 | 83 | /// R removes a single package, leaving all of its dependencies installed. 84 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 85 | Cmd::new(["winget", "uninstall", "--accept-source-agreements"]) 86 | .kws(kws) 87 | .flags(flags) 88 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 89 | .await 90 | } 91 | 92 | /// Rn removes a package and skips the generation of configuration backup 93 | /// files. 94 | async fn rn(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 95 | Cmd::new([ 96 | "winget", 97 | "uninstall", 98 | "--accept-source-agreements", 99 | "--purge", 100 | ]) 101 | .kws(kws) 102 | .flags(flags) 103 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 104 | .await 105 | } 106 | 107 | /// S installs one or more packages by name. 108 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 109 | Cmd::new([ 110 | "winget", 111 | "install", 112 | "--accept-package-agreements", 113 | "--accept-source-agreements", 114 | ]) 115 | .kws(kws) 116 | .flags(flags) 117 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 118 | .await 119 | } 120 | 121 | /// Si displays remote package information: name, version, description, etc. 122 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 123 | Cmd::new(["winget", "show", "--accept-source-agreements"]) 124 | .kws(kws) 125 | .flags(flags) 126 | .pipe(|cmd| self.run(cmd)) 127 | .await 128 | } 129 | 130 | /// Ss searches for package(s) by searching the expression in name, 131 | /// description, short description. 132 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 133 | Cmd::new(["winget", "search", "--accept-source-agreements"]) 134 | .kws(kws) 135 | .flags(flags) 136 | .pipe(|cmd| self.run(cmd)) 137 | .await 138 | } 139 | 140 | /// Sy refreshes the local package database. 141 | async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 142 | Cmd::new(["winget", "source", "update", "--accept-source-agreements"]) 143 | .flags(flags) 144 | .pipe(|cmd| self.run(cmd)) 145 | .await?; 146 | if !kws.is_empty() { 147 | self.s(kws, flags).await?; 148 | } 149 | Ok(()) 150 | } 151 | 152 | /// Su updates outdated packages. 153 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 154 | Cmd::new([ 155 | "winget", 156 | "upgrade", 157 | "--accept-package-agreements", 158 | "--accept-source-agreements", 159 | ]) 160 | .kws(if kws.is_empty() { &["--all"][..] } else { kws }) 161 | .flags(flags) 162 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 163 | .await 164 | } 165 | 166 | /// Suy refreshes the local package database, then updates outdated 167 | /// packages. 168 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 169 | self.sy(&[], flags).await?; 170 | self.su(kws, flags).await 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/pm/xbps.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::{io::Write, sync::LazyLock}; 4 | 5 | use async_trait::async_trait; 6 | use futures::prelude::*; 7 | use indoc::indoc; 8 | use tap::Pipe; 9 | 10 | use super::{Pm, PmHelper, PmMode, PromptStrategy, Strategy}; 11 | use crate::{ 12 | config::Config, 13 | error::{Error, Result}, 14 | exec::{Cmd, StatusCode}, 15 | print::println_err, 16 | }; 17 | 18 | macro_rules! doc_self { 19 | () => { 20 | indoc! {" 21 | The [X Binary Package System](https://github.com/void-linux/xbps). 22 | "} 23 | }; 24 | } 25 | use doc_self; 26 | 27 | #[doc = doc_self!()] 28 | #[derive(Debug)] 29 | pub struct Xbps { 30 | cfg: Config, 31 | } 32 | 33 | const PKG_NOT_FOUND_CODE: StatusCode = 2; 34 | 35 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 36 | prompt: PromptStrategy::native_no_confirm(["--yes"]), 37 | ..Strategy::default() 38 | }); 39 | 40 | impl Xbps { 41 | #[must_use] 42 | #[allow(missing_docs)] 43 | pub const fn new(cfg: Config) -> Self { 44 | Self { cfg } 45 | } 46 | } 47 | 48 | #[async_trait] 49 | impl Pm for Xbps { 50 | /// Gets the name of the package manager. 51 | fn name(&self) -> &'static str { 52 | "xbps" 53 | } 54 | 55 | fn cfg(&self) -> &crate::config::Config { 56 | &self.cfg 57 | } 58 | 59 | /// Q generates a list of installed packages. 60 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 61 | if kws.is_empty() { 62 | return self 63 | .run(Cmd::new(["xbps-query", "-l"]).kws(kws).flags(flags)) 64 | .await; 65 | } 66 | 67 | let lines: Vec<_> = stream::iter(kws) 68 | .map(Ok) 69 | .and_then(|&pkg| async { 70 | let cmd = Cmd::new(["xbps-query", "--property", "pkgver", pkg]).flags(flags); 71 | match self 72 | .check_output(cmd, PmMode::Mute, &Strategy::default()) 73 | .await 74 | { 75 | Ok(line) => Ok(Ok(line)), 76 | Err(Error::CmdStatusCodeError { 77 | code: PKG_NOT_FOUND_CODE, 78 | output, 79 | }) => Ok(Err((pkg.to_owned(), output))), 80 | Err(e) => Err(e), 81 | } 82 | }) 83 | .try_collect() 84 | .await?; 85 | 86 | let mut stdout = std::io::stdout(); 87 | lines.into_iter().try_for_each(|line| match line { 88 | Ok(line) => stdout.write_all(&line).map_err(Into::into), 89 | Err((missing, output)) => { 90 | println_err(format_args!("package `{missing}` was not found")); 91 | Err(Error::CmdStatusCodeError { 92 | code: PKG_NOT_FOUND_CODE, 93 | output, 94 | }) 95 | } 96 | }) 97 | } 98 | 99 | /// Qe lists packages installed explicitly (not as dependencies). 100 | async fn qe(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 101 | if kws.is_empty() { 102 | return self 103 | .run(Cmd::new(["xbps-query", "-m"]).kws(kws).flags(flags)) 104 | .await; 105 | } 106 | 107 | let lines: Vec, String>> = stream::iter(kws) 108 | .filter(|pkg| async { 109 | let check_cmd = 110 | Cmd::new(["xbps-query", "--property", "automatic-install", pkg]).flags(flags); 111 | self.check_output(check_cmd, PmMode::Mute, &Strategy::default()) 112 | .await 113 | // If a package is manually installed, 114 | // then the automatic-install field is empty. 115 | .map_or(true, |auto_stat| auto_stat.is_empty()) 116 | }) 117 | .map(Ok) 118 | .and_then(|&pkg| async { 119 | let cmd = Cmd::new(["xbps-query", "--property", "pkgver", pkg]).flags(flags); 120 | match self 121 | .check_output(cmd, PmMode::Mute, &Strategy::default()) 122 | .await 123 | { 124 | Ok(line) => Ok(Ok(line)), 125 | Err(Error::CmdStatusCodeError { 126 | code: PKG_NOT_FOUND_CODE, 127 | .. 128 | }) => Ok(Err(pkg.to_owned())), 129 | Err(e) => Err(e), 130 | } 131 | }) 132 | .try_collect() 133 | .await?; 134 | 135 | let mut stdout = std::io::stdout(); 136 | lines.into_iter().try_fold(Ok(()), |acc, line| { 137 | std::io::Result::Ok(match line { 138 | Ok(line) => { 139 | stdout.write_all(&line)?; 140 | acc 141 | } 142 | Err(missing) => { 143 | println_err(format_args!("package `{missing}` was not found")); 144 | Err(Error::CmdStatusCodeError { 145 | code: PKG_NOT_FOUND_CODE, 146 | output: vec![], 147 | }) 148 | } 149 | }) 150 | })? 151 | } 152 | 153 | /// Qi displays local package information: name, version, description, etc. 154 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 155 | self.run(Cmd::new(["xbps-query", "-S"]).kws(kws).flags(flags)) 156 | .await 157 | } 158 | 159 | /// Qii displays local packages which require X to be installed, aka local 160 | /// reverse dependencies. 161 | async fn qii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 162 | self.run(Cmd::new(["xbps-query", "-X"]).kws(kws).flags(flags)) 163 | .await 164 | } 165 | 166 | /// Ql displays files provided by local package. 167 | async fn ql(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 168 | self.run(Cmd::new(["xbps-query", "-f"]).kws(kws).flags(flags)) 169 | .await 170 | } 171 | 172 | /// Qs searches locally installed package for names or descriptions. 173 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 174 | self.run(Cmd::new(["xbps-query", "-s"]).kws(kws).flags(flags)) 175 | .await 176 | } 177 | 178 | /// R removes a single package, leaving all of its dependencies installed. 179 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 180 | Cmd::with_sudo(["xbps-remove"]) 181 | .kws(kws) 182 | .flags(flags) 183 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 184 | .await 185 | } 186 | 187 | /// Rs removes a package and its dependencies which are not required by any 188 | /// other installed package, and not explicitly installed by the user. 189 | async fn rs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 190 | Cmd::with_sudo(["xbps-remove", "-R"]) 191 | .kws(kws) 192 | .flags(flags) 193 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 194 | .await 195 | } 196 | 197 | /// S installs one or more packages by name. 198 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 199 | Cmd::with_sudo(["xbps-install"]) 200 | .kws(kws) 201 | .flags(flags) 202 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 203 | .await 204 | } 205 | 206 | /// Sc removes all the cached packages that are not currently installed, and 207 | /// the unused sync database. 208 | async fn sc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 209 | Cmd::with_sudo(["xbps-remove", "-O"]) 210 | .kws(kws) 211 | .flags(flags) 212 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 213 | .await 214 | } 215 | 216 | /// Si displays remote package information: name, version, description, etc. 217 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 218 | self.run(Cmd::new(["xbps-query", "-RS"]).kws(kws).flags(flags)) 219 | .await 220 | } 221 | 222 | /// Sii displays packages which require X to be installed, aka reverse 223 | /// dependencies. 224 | async fn sii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 225 | self.run(Cmd::new(["xbps-query", "-RX"]).kws(kws).flags(flags)) 226 | .await 227 | } 228 | 229 | /// Ss searches for package(s) by searching the expression in name, 230 | /// description, short description. 231 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 232 | self.run(Cmd::new(["xbps-query", "-Rs"]).kws(kws).flags(flags)) 233 | .await 234 | } 235 | 236 | /// Suy refreshes the local package database, then updates outdated 237 | /// packages. 238 | async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 239 | Cmd::with_sudo(["xbps-install", "-Su"]) 240 | .kws(kws) 241 | .flags(flags) 242 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 243 | .await 244 | } 245 | 246 | /// Sy refreshes the local package database. 247 | async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 248 | self.run(Cmd::new(["xbps-install", "-S"]).kws(kws).flags(flags)) 249 | .await 250 | } 251 | 252 | /// Sw retrieves all packages from the server, but does not install/upgrade 253 | /// anything. 254 | async fn sw(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 255 | Cmd::with_sudo(["xbps-install", "-D"]) 256 | .kws(kws) 257 | .flags(flags) 258 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 259 | .await 260 | } 261 | 262 | /// Su updates outdated packages. 263 | async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 264 | Cmd::with_sudo(["xbps-install", "-u"]) 265 | .kws(kws) 266 | .flags(flags) 267 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 268 | .await 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/pm/zypper.rs: -------------------------------------------------------------------------------- 1 | #![doc = doc_self!()] 2 | 3 | use std::sync::LazyLock; 4 | 5 | use async_trait::async_trait; 6 | use indoc::indoc; 7 | use tap::prelude::*; 8 | 9 | use super::{DryRunStrategy, NoCacheStrategy, Pm, PmHelper, PmMode, PromptStrategy, Strategy}; 10 | use crate::{ 11 | config::Config, 12 | error::Result, 13 | exec::{self, Cmd}, 14 | }; 15 | 16 | macro_rules! doc_self { 17 | () => { 18 | indoc! {" 19 | The [Zypper Package Manager](https://en.opensuse.org/Portal:Zypper). 20 | "} 21 | }; 22 | } 23 | use doc_self; 24 | 25 | #[doc = doc_self!()] 26 | #[derive(Debug)] 27 | pub struct Zypper { 28 | cfg: Config, 29 | } 30 | 31 | static STRAT_CHECK_DRY: LazyLock = LazyLock::new(|| Strategy { 32 | dry_run: DryRunStrategy::with_flags(["--dry-run"]), 33 | ..Strategy::default() 34 | }); 35 | 36 | static STRAT_PROMPT: LazyLock = LazyLock::new(|| Strategy { 37 | prompt: PromptStrategy::native_no_confirm(["-y"]), 38 | dry_run: DryRunStrategy::with_flags(["--dry-run"]), 39 | ..Strategy::default() 40 | }); 41 | 42 | static STRAT_INSTALL: LazyLock = LazyLock::new(|| Strategy { 43 | prompt: PromptStrategy::native_no_confirm(["-y"]), 44 | no_cache: NoCacheStrategy::Scc, 45 | dry_run: DryRunStrategy::with_flags(["--dry-run"]), 46 | }); 47 | 48 | impl Zypper { 49 | #[must_use] 50 | #[allow(missing_docs)] 51 | pub const fn new(cfg: Config) -> Self { 52 | Self { cfg } 53 | } 54 | 55 | async fn check_dry(&self, cmd: Cmd) -> Result<()> { 56 | self.run_with(cmd, self.default_mode(), &STRAT_CHECK_DRY) 57 | .await 58 | } 59 | } 60 | 61 | #[async_trait] 62 | impl Pm for Zypper { 63 | /// Gets the name of the package manager. 64 | fn name(&self) -> &'static str { 65 | "zypper" 66 | } 67 | 68 | fn cfg(&self) -> &Config { 69 | &self.cfg 70 | } 71 | 72 | /// Q generates a list of installed packages. 73 | async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 74 | if kws.is_empty() { 75 | Cmd::new(["rpm", "-qa", "--qf", "%{NAME} %{VERSION}\\n"]) 76 | .flags(flags) 77 | .pipe(|cmd| self.run(cmd)) 78 | .await 79 | } else { 80 | self.qs(kws, flags).await 81 | } 82 | } 83 | 84 | /// Qc shows the changelog of a package. 85 | async fn qc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 86 | Cmd::new(["rpm", "-q", "--changelog"]) 87 | .kws(kws) 88 | .flags(flags) 89 | .pipe(|cmd| self.run(cmd)) 90 | .await 91 | } 92 | 93 | /// Qi displays local package information: name, version, description, etc. 94 | async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 95 | self.si(kws, flags).await 96 | } 97 | 98 | /// Ql displays files provided by local package. 99 | async fn ql(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 100 | self.run(Cmd::new(["rpm", "-ql"]).kws(kws).flags(flags)) 101 | .await 102 | } 103 | 104 | /// Qm lists packages that are installed but are not available in any 105 | /// installation source (anymore). 106 | async fn qm(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 107 | let cmd = Cmd::new(["zypper", "search", "-si"]).kws(kws).flags(flags); 108 | let out_bytes = self 109 | .check_output(cmd, PmMode::Mute, &Strategy::default()) 110 | .await?; 111 | let out = String::from_utf8(out_bytes)?; 112 | 113 | exec::grep_print(&out, &["System Packages"])?; 114 | Ok(()) 115 | } 116 | 117 | /// Qo queries the package which provides FILE. 118 | async fn qo(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 119 | self.run(Cmd::new(["rpm", "-qf"]).kws(kws).flags(flags)) 120 | .await 121 | } 122 | 123 | /// Qp queries a package supplied through a file supplied on the command 124 | /// line rather than an entry in the package management database. 125 | async fn qp(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 126 | self.run(Cmd::new(["rpm", "-qip"]).kws(kws).flags(flags)) 127 | .await 128 | } 129 | 130 | /// Qs searches locally installed package for names or descriptions. 131 | // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, 132 | // when including multiple search terms, only packages with descriptions 133 | // matching ALL of those terms are returned. 134 | async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 135 | Cmd::new(["zypper", "search", "--installed-only"]) 136 | .kws(kws) 137 | .flags(flags) 138 | .pipe(|cmd| self.check_dry(cmd)) 139 | .await 140 | } 141 | 142 | /// Qu lists packages which have an update available. 143 | async fn qu(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 144 | self.check_dry(Cmd::new(["zypper", "list-updates"]).kws(kws).flags(flags)) 145 | .await 146 | } 147 | 148 | /// R removes a single package, leaving all of its dependencies installed. 149 | async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 150 | Cmd::with_sudo(["zypper", "remove"]) 151 | .kws(kws) 152 | .flags(flags) 153 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 154 | .await 155 | } 156 | 157 | /// Rss removes a package and its dependencies which are not required by any 158 | /// other installed package. 159 | async fn rss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 160 | Cmd::with_sudo(["zypper", "remove", "--clean-deps"]) 161 | .kws(kws) 162 | .flags(flags) 163 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT)) 164 | .await 165 | } 166 | 167 | /// S installs one or more packages by name. 168 | async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 169 | Cmd::with_sudo(["zypper", "install"]) 170 | .kws(kws) 171 | .flags(flags) 172 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 173 | .await 174 | } 175 | 176 | /// Sc removes all the cached packages that are not currently installed, and 177 | /// the unused sync database. 178 | async fn sc(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 179 | let strat = Strategy { 180 | prompt: PromptStrategy::CustomPrompt, 181 | ..Strategy::default() 182 | }; 183 | Cmd::with_sudo(["zypper", "clean"]) 184 | .flags(flags) 185 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &strat)) 186 | .await 187 | } 188 | 189 | /// Scc removes all files from the cache. 190 | async fn scc(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 191 | self.sc(_kws, flags).await 192 | } 193 | 194 | /// Sg lists all packages belonging to the GROUP. 195 | async fn sg(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 196 | Cmd::new(if kws.is_empty() { 197 | ["zypper", "patterns"] 198 | } else { 199 | ["zypper", "info"] 200 | }) 201 | .kws(kws) 202 | .flags(flags) 203 | .pipe(|cmd| self.run(cmd)) 204 | .await 205 | } 206 | 207 | /// Si displays remote package information: name, version, description, etc. 208 | async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 209 | Cmd::new(["zypper", "info", "--requires"]) 210 | .kws(kws) 211 | .flags(flags) 212 | .pipe(|cmd| self.check_dry(cmd)) 213 | .await 214 | } 215 | 216 | /// Sl displays a list of all packages in all installation sources that are 217 | /// handled by the package management. 218 | async fn sl(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 219 | let cmd = &["zypper", "packages", "-R"]; 220 | if kws.is_empty() { 221 | let cmd = Cmd::new(cmd).kws(kws).flags(flags); 222 | return self.check_dry(cmd).await; 223 | } 224 | let cmd = Cmd::new(cmd).flags(flags); 225 | let out = self 226 | .check_output(cmd, PmMode::Mute, &STRAT_CHECK_DRY) 227 | .await? 228 | .pipe(String::from_utf8)?; 229 | exec::grep_print_with_header(&out, kws, 4) 230 | } 231 | 232 | /// Ss searches for package(s) by searching the expression in name, 233 | /// description, short description. 234 | async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 235 | self.check_dry(Cmd::new(["zypper", "search"]).kws(kws).flags(flags)) 236 | .await 237 | } 238 | 239 | /// Su updates outdated packages. 240 | async fn su(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 241 | Cmd::with_sudo(["zypper", "--no-refresh", "dist-upgrade"]) 242 | .flags(flags) 243 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 244 | .await 245 | } 246 | 247 | /// Suy refreshes the local package database, then updates outdated 248 | /// packages. 249 | async fn suy(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { 250 | Cmd::with_sudo(["zypper", "dist-upgrade"]) 251 | .flags(flags) 252 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 253 | .await 254 | } 255 | 256 | /// Sw retrieves all packages from the server, but does not install/upgrade 257 | /// anything. 258 | async fn sw(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 259 | Cmd::with_sudo(["zypper", "install", "--download-only"]) 260 | .kws(kws) 261 | .flags(flags) 262 | .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL)) 263 | .await 264 | } 265 | 266 | /// Sy refreshes the local package database. 267 | async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 268 | self.check_dry(Cmd::with_sudo(["zypper", "refresh"]).flags(flags)) 269 | .await?; 270 | if !kws.is_empty() { 271 | self.s(kws, flags).await?; 272 | } 273 | Ok(()) 274 | } 275 | 276 | /// U upgrades or adds package(s) to the system and installs the required 277 | /// dependencies from sync repositories. 278 | async fn u(&self, kws: &[&str], flags: &[&str]) -> Result<()> { 279 | self.s(kws, flags).await 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/print.rs: -------------------------------------------------------------------------------- 1 | //! Output messages and prompts. 2 | 3 | #![allow(missing_docs, clippy::module_name_repetitions)] 4 | 5 | pub mod prompt; 6 | pub(crate) mod style; 7 | 8 | use std::fmt::{self, Debug, Display}; 9 | 10 | use console::{Style, style}; 11 | use dialoguer::theme::ColorfulTheme; 12 | 13 | /// The right indentation to be applied on prompt prefixes. 14 | static PROMPT_INDENT: usize = 9; 15 | 16 | macro_rules! prompt_format { 17 | () => { 18 | "{:>indent$}" 19 | }; 20 | } 21 | 22 | macro_rules! plain_format { 23 | () => { 24 | concat!(prompt_format!(), " {}") 25 | }; 26 | } 27 | 28 | macro_rules! quoted_format { 29 | () => { 30 | concat!(prompt_format!(), " `{}`") 31 | }; 32 | } 33 | 34 | /// Writes an error after the given prompt. 35 | #[allow(clippy::missing_errors_doc)] 36 | pub fn write_err(f: &mut fmt::Formatter, prompt: impl Display, err: impl Debug) -> fmt::Result { 37 | write!( 38 | f, 39 | concat!(prompt_format!(), " {:?}"), 40 | prompt, 41 | err, 42 | indent = PROMPT_INDENT, 43 | ) 44 | } 45 | 46 | /// Prints out a message after the given prompt. 47 | pub fn println(prompt: impl Display, msg: impl Display) { 48 | println!( 49 | plain_format!(), 50 | style::MESSAGE.apply_to(prompt), 51 | msg, 52 | indent = PROMPT_INDENT, 53 | ); 54 | } 55 | 56 | /// Prints out an error message. 57 | pub fn println_err(msg: impl Display) { 58 | println!( 59 | plain_format!(), 60 | &*prompt::ERROR, 61 | msg, 62 | indent = PROMPT_INDENT, 63 | ); 64 | } 65 | 66 | /// Prints out a backtick-quoted message after the given prompt. 67 | pub fn println_quoted(prompt: impl Display, msg: impl Display) { 68 | println!( 69 | quoted_format!(), 70 | style::MESSAGE.apply_to(prompt), 71 | msg, 72 | indent = PROMPT_INDENT, 73 | ); 74 | } 75 | 76 | /// Returns a [`dialoguer`] theme with the given prompt. 77 | pub(crate) fn question_theme(prompt: impl Display) -> impl dialoguer::theme::Theme { 78 | let prompt_prefix = style::QUESTION.apply_to(format!( 79 | prompt_format!(), 80 | style::QUESTION.apply_to(prompt), 81 | indent = PROMPT_INDENT, 82 | )); 83 | ColorfulTheme { 84 | success_prefix: prompt_prefix.clone(), 85 | error_prefix: prompt_prefix.clone().red(), 86 | prompt_prefix, 87 | prompt_style: Style::new(), 88 | prompt_suffix: style(String::new()), 89 | active_item_prefix: style(" *".into()).bold().for_stderr(), 90 | active_item_style: Style::new().bold(), 91 | inactive_item_prefix: style(" ".into()).for_stderr(), 92 | ..ColorfulTheme::default() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/print/prompt.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use crate::print::style; 4 | 5 | type StyledStr<'a> = console::StyledObject<&'a str>; 6 | 7 | pub static CANCELED: LazyLock = LazyLock::new(|| style::MESSAGE.apply_to("Canceled")); 8 | pub static PENDING: LazyLock = LazyLock::new(|| style::MESSAGE.apply_to("Pending")); 9 | pub static RUNNING: LazyLock = LazyLock::new(|| style::MESSAGE.apply_to("Running")); 10 | pub static INFO: LazyLock = LazyLock::new(|| style::MESSAGE.apply_to("Info")); 11 | pub static ERROR: LazyLock = LazyLock::new(|| style::ERROR.apply_to("Error")); 12 | -------------------------------------------------------------------------------- /src/print/style.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use console::Style; 4 | 5 | pub static MESSAGE: LazyLock