├── .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 | [](https://crates.io/crates/pacaptr)
6 |
7 |
10 |
11 |
14 |
15 | [](https://crates.io/crates/pacaptr)
16 | [](https://docs.rs/pacaptr)
17 | [](https://rami3l.github.io/pacaptr)
18 | [](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 | [](https://github.com/rami3l/homebrew-tap)
90 |
91 | ```bash
92 | brew install rami3l/tap/pacaptr
93 | ```
94 |
95 | ### Scoop
96 |
97 | [](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 | [](https://community.chocolatey.org/packages/pacaptr)
107 | [](https://community.chocolatey.org/packages/pacaptr)
108 |
109 | ```powershell
110 | choco install pacaptr
111 | ```
112 |
113 | ### Cargo
114 |
115 | [](https://crates.io/crates/pacaptr)
116 | [](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