├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── install.sh └── src ├── action.rs ├── error.rs ├── lib.rs ├── macros.rs ├── main.rs ├── task.rs ├── utils.rs └── vendor.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | all: 13 | name: All 14 | 15 | strategy: 16 | matrix: 17 | os: 18 | - ubuntu-latest 19 | - macos-latest 20 | - windows-latest 21 | 22 | runs-on: ${{ matrix.os }} 23 | 24 | env: 25 | RUSTFLAGS: --deny warnings 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Install Rust Toolchain Components 31 | uses: dtolnay/rust-toolchain@stable 32 | 33 | - uses: Swatinem/rust-cache@v2 34 | 35 | - name: Test 36 | run: cargo test --all 37 | 38 | - name: Clippy 39 | run: cargo clippy --all --all-targets -- -D warnings 40 | 41 | - name: Format 42 | run: cargo fmt --all --check 43 | 44 | - name: Test upt --help 45 | run: cargo run -- --help 46 | 47 | - name: Test upt list 48 | run: cargo run -- list 49 | 50 | msys2: 51 | name: MSYS2 52 | 53 | runs-on: windows-latest 54 | 55 | env: 56 | RUSTFLAGS: --deny warnings 57 | 58 | steps: 59 | - uses: actions/checkout@v4 60 | 61 | - name: Install Rust Toolchain Components 62 | uses: dtolnay/rust-toolchain@stable 63 | 64 | - uses: Swatinem/rust-cache@v2 65 | 66 | - name: Test 67 | run: cargo test --all 68 | 69 | - name: Clippy 70 | run: cargo clippy --all --all-targets -- -D warnings 71 | 72 | - name: Format 73 | run: cargo fmt --all --check 74 | 75 | - uses: msys2/setup-msys2@v2 76 | with: 77 | path-type: inherit 78 | 79 | - name: Test upt --help on Windows Bash 80 | if: runner.os == 'Windows' 81 | run: cargo run -- --help 82 | shell: bash 83 | 84 | - name: Test upt --help on MSYS2 85 | shell: msys2 {0} 86 | run: cargo run -- --help 87 | 88 | - name: Test upt list 89 | shell: msys2 {0} 90 | run: cargo run -- list -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[0-9]+.[0-9]+.[0-9]+* 7 | 8 | jobs: 9 | release: 10 | name: Publish to GitHub Release 11 | permissions: 12 | contents: write 13 | outputs: 14 | rc: ${{ steps.check-tag.outputs.rc }} 15 | 16 | strategy: 17 | matrix: 18 | include: 19 | - target: aarch64-unknown-linux-musl 20 | os: ubuntu-latest 21 | use-cross: true 22 | cargo-flags: "" 23 | - target: aarch64-apple-darwin 24 | os: macos-latest 25 | use-cross: true 26 | cargo-flags: "" 27 | - target: aarch64-pc-windows-msvc 28 | os: windows-latest 29 | use-cross: true 30 | cargo-flags: "" 31 | - target: x86_64-apple-darwin 32 | os: macos-latest 33 | cargo-flags: "" 34 | - target: x86_64-pc-windows-msvc 35 | os: windows-latest 36 | cargo-flags: "" 37 | - target: x86_64-unknown-linux-musl 38 | os: ubuntu-latest 39 | use-cross: true 40 | cargo-flags: "" 41 | - target: i686-unknown-linux-musl 42 | os: ubuntu-latest 43 | use-cross: true 44 | cargo-flags: "" 45 | - target: i686-pc-windows-msvc 46 | os: windows-latest 47 | use-cross: true 48 | cargo-flags: "" 49 | 50 | runs-on: ${{matrix.os}} 51 | env: 52 | BUILD_CMD: cargo 53 | 54 | steps: 55 | - uses: actions/checkout@v4 56 | 57 | - name: Check Tag 58 | id: check-tag 59 | shell: bash 60 | run: | 61 | ver=${GITHUB_REF##*/} 62 | echo "version=$ver" >> $GITHUB_OUTPUT 63 | if [[ "$ver" =~ [0-9]+.[0-9]+.[0-9]+$ ]]; then 64 | echo "rc=false" >> $GITHUB_OUTPUT 65 | else 66 | echo "rc=true" >> $GITHUB_OUTPUT 67 | fi 68 | 69 | 70 | - name: Install Rust Toolchain Components 71 | uses: dtolnay/rust-toolchain@stable 72 | with: 73 | targets: ${{ matrix.target }} 74 | 75 | - name: Install cross 76 | if: matrix.use-cross 77 | uses: taiki-e/install-action@v2 78 | with: 79 | tool: cross 80 | 81 | - name: Overwrite build command env variable 82 | if: matrix.use-cross 83 | shell: bash 84 | run: echo "BUILD_CMD=cross" >> $GITHUB_ENV 85 | 86 | - name: Show Version Information (Rust, cargo, GCC) 87 | shell: bash 88 | run: | 89 | gcc --version || true 90 | rustup -V 91 | rustup toolchain list 92 | rustup default 93 | cargo -V 94 | rustc -V 95 | 96 | - name: Build 97 | shell: bash 98 | run: $BUILD_CMD build --locked --release --target=${{ matrix.target }} ${{ matrix.cargo-flags }} 99 | 100 | - name: Build Archive 101 | shell: bash 102 | id: package 103 | env: 104 | target: ${{ matrix.target }} 105 | version: ${{ steps.check-tag.outputs.version }} 106 | run: | 107 | set -euxo pipefail 108 | 109 | bin=${GITHUB_REPOSITORY##*/} 110 | dist_dir=`pwd`/dist 111 | name=$bin-$version-$target 112 | executable=target/$target/release/$bin 113 | 114 | if [[ "$RUNNER_OS" == "Windows" ]]; then 115 | executable=$executable.exe 116 | fi 117 | 118 | mkdir $dist_dir 119 | cp $executable $dist_dir 120 | cd $dist_dir 121 | 122 | if [[ "$RUNNER_OS" == "Windows" ]]; then 123 | archive=$dist_dir/$name.zip 124 | 7z a $archive * 125 | echo "archive=dist/$name.zip" >> $GITHUB_OUTPUT 126 | else 127 | archive=$dist_dir/$name.tar.gz 128 | tar -czf $archive * 129 | echo "archive=dist/$name.tar.gz" >> $GITHUB_OUTPUT 130 | fi 131 | 132 | - name: Publish Archive 133 | uses: softprops/action-gh-release@v2 134 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 135 | with: 136 | draft: false 137 | files: ${{ steps.package.outputs.archive }} 138 | prerelease: ${{ steps.check-tag.outputs.rc == 'true' }} 139 | 140 | publish-crate: 141 | name: Publish to crates.io 142 | if: ${{ needs.release.outputs.rc == 'false' }} 143 | runs-on: ubuntu-latest 144 | needs: release 145 | steps: 146 | - uses: actions/checkout@v4 147 | 148 | - uses: dtolnay/rust-toolchain@stable 149 | 150 | - name: Publish 151 | env: 152 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }} 153 | run: cargo publish -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | /.vscode -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "2.6.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 10 | 11 | [[package]] 12 | name = "either" 13 | version = "1.13.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 16 | 17 | [[package]] 18 | name = "errno" 19 | version = "0.3.9" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 22 | dependencies = [ 23 | "libc", 24 | "windows-sys", 25 | ] 26 | 27 | [[package]] 28 | name = "home" 29 | version = "0.5.9" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 32 | dependencies = [ 33 | "windows-sys", 34 | ] 35 | 36 | [[package]] 37 | name = "libc" 38 | version = "0.2.161" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 41 | 42 | [[package]] 43 | name = "linux-raw-sys" 44 | version = "0.4.14" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 47 | 48 | [[package]] 49 | name = "rustix" 50 | version = "0.38.38" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" 53 | dependencies = [ 54 | "bitflags", 55 | "errno", 56 | "libc", 57 | "linux-raw-sys", 58 | "windows-sys", 59 | ] 60 | 61 | [[package]] 62 | name = "upt" 63 | version = "0.9.0" 64 | dependencies = [ 65 | "which", 66 | ] 67 | 68 | [[package]] 69 | name = "which" 70 | version = "6.0.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" 73 | dependencies = [ 74 | "either", 75 | "home", 76 | "rustix", 77 | "winsafe", 78 | ] 79 | 80 | [[package]] 81 | name = "windows-sys" 82 | version = "0.52.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 85 | dependencies = [ 86 | "windows-targets", 87 | ] 88 | 89 | [[package]] 90 | name = "windows-targets" 91 | version = "0.52.6" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 94 | dependencies = [ 95 | "windows_aarch64_gnullvm", 96 | "windows_aarch64_msvc", 97 | "windows_i686_gnu", 98 | "windows_i686_gnullvm", 99 | "windows_i686_msvc", 100 | "windows_x86_64_gnu", 101 | "windows_x86_64_gnullvm", 102 | "windows_x86_64_msvc", 103 | ] 104 | 105 | [[package]] 106 | name = "windows_aarch64_gnullvm" 107 | version = "0.52.6" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 110 | 111 | [[package]] 112 | name = "windows_aarch64_msvc" 113 | version = "0.52.6" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 116 | 117 | [[package]] 118 | name = "windows_i686_gnu" 119 | version = "0.52.6" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 122 | 123 | [[package]] 124 | name = "windows_i686_gnullvm" 125 | version = "0.52.6" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 128 | 129 | [[package]] 130 | name = "windows_i686_msvc" 131 | version = "0.52.6" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 134 | 135 | [[package]] 136 | name = "windows_x86_64_gnu" 137 | version = "0.52.6" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 140 | 141 | [[package]] 142 | name = "windows_x86_64_gnullvm" 143 | version = "0.52.6" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 146 | 147 | [[package]] 148 | name = "windows_x86_64_msvc" 149 | version = "0.52.6" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 152 | 153 | [[package]] 154 | name = "winsafe" 155 | version = "0.0.19" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" 158 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upt" 3 | version = "0.9.0" 4 | edition = "2021" 5 | authors = ["sigoden "] 6 | description = "Universal package management tool for any OS" 7 | license = "MIT OR Apache-2.0" 8 | homepage = "https://github.com/sigoden/upt" 9 | repository = "https://github.com/sigoden/upt" 10 | categories = ["command-line-utilities"] 11 | keywords = ["universal", "package", "management"] 12 | 13 | [dependencies] 14 | which = "6.0.1" 15 | 16 | [profile.release] 17 | lto = true 18 | strip = true 19 | opt-level = "z" 20 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) sigoden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UPT — **U**niversal **P**ackage-management **T**ool 2 | 3 | [![Build status](https://github.com/sigoden/upt/actions/workflows/ci.yml/badge.svg)](https://github.com/sigoden/upt/actions) 4 | [![Crates.io](https://img.shields.io/crates/v/upt.svg)](https://crates.io/crates/upt) 5 | 6 | Upt provides a unified command interface to manage packages for any operating system. 7 | 8 | Upt relies on the platform's package management tool to perform the task, it's more like a wrapper or adaptive alias. 9 | 10 | ## Install 11 | 12 | **Use Cargo** 13 | 14 | Upt is written in the rust so you can install it using [cargo](https://doc.rust-lang.org/stable/cargo/). 15 | 16 | ```sh 17 | cargo install upt 18 | ``` 19 | 20 | **Use Shell (Mac, Linux)** 21 | 22 | ``` 23 | curl -fsSL https://raw.githubusercontent.com/sigoden/upt/main/install.sh | sh -s -- --to /usr/local/bin 24 | ``` 25 | 26 | **Binaries for macOS, Linux, Windows, BSD** 27 | 28 | Download from [GitHub Releases](https://github.com/sigoden/upt/releases), unzip and add `upt` to your $PATH. 29 | 30 | ## Features 31 | 32 | ### Unified command interface 33 | 34 | Each operating system (OS) has its own package management tool, which requires different commands to complete the same operation. 35 | This can be inconvenient when switching between or trying new OSs. 36 | 37 | ```sh 38 | apt install $pkg # Ubuntu, Debian, Linux Mint... 39 | apk add $pkg # Alpine 40 | pacman -S $pkg # Arch, Manjaro... 41 | nix-env -i $pkg # Nixos 42 | xbps-install $pkg # Voidlinux 43 | emerge $pkg # Gentoo 44 | ``` 45 | 46 | With `upt`, You just need to remember one command: 47 | 48 | ```sh 49 | upt install $pkg # Works on any OS 50 | ``` 51 | 52 | Upt identifies the os type and runs the appropriate package management tool to install `$pkg`. 53 | 54 | ### Act as another tool 55 | 56 | Upt can act as another tool and use their syntax by renaming it. 57 | 58 | ```sh 59 | cp upt brew 60 | brew install $pkg 61 | 62 | cp upt pacman 63 | pacman -S $pkg 64 | 65 | cp upt emerge 66 | emerge $pkg 67 | ``` 68 | 69 | In this way, you can use the syntax of the tool you are most familiar with to manage packages. 70 | 71 | ### Supported tools 72 | 73 | ``` 74 | | Tool | Install | Uninstall | Upgrade | Search | Info | Update Index | Upgrade All | List Installed | 75 | | ----------- | --------------------------- | --------------------------- | ------------------------------- | --------------------- | ------------------------------ | ---------------------- | ------------------------ | --------------------------------- | 76 | | upt | upt install $pkg | upt remove/uninstall $pkg | upt upgrade $pkg | upt search $pkg | upt info/show $pkg | upt update | upt upgrade | upt list | 77 | | apk | apk add $pkg | apk del $pkg | apk upgrade $pkg | apk search $pkg | apk info $pkg | apk update | apk upgrade | apk list -I/--installed | 78 | | apt | apt install $pkg | apt remove $pkg | apt install --only-upgrade $pkg | apt search $pkg | apt show $pkg | apt update | apt upgrade | apt list -i/--installed | 79 | | brew | brew install $pkg | brew uninstall $pkg | brew upgrade $pkg | brew search $pkg | brew info $pkg | brew update | brew upgrade | brew list | 80 | | cards | cards install $pkg | cards remove $pkg | cards install -u/--upgrade $pkg | cards search $pkg | cards info $pkg | cards sync | cards upgrade | cards list | 81 | | choco | choco install $pkg | choco uninstall $pkg | choco upgrade $pkg | choco search $pkg | choco info $pkg | - | choco upgrade all | choco list | 82 | | dnf | dnf install $pkg | dnf remove $pkg | dnf upgrade $pkg | dnf search $pkg | dnf info $pkg | dnf check-update | dnf update | dnf list --installed | 83 | | emerge | emerge $pkg | emerge --depclean $pkg | emerge --update $pkg | emerge --search $pkg | emerge --info $pkg | emerge --sync | emerge -vuDN @world | qlist -lv | 84 | | eopkg | eopkg install $pkg | eopkg remove $pkg | eopkg upgrade $pkg | eopkg search $pkg | eopkg info $pkg | eopkg update-repo | eopkg upgrade | eopkg list-installed | 85 | | flatpak | flatpak install $pkg | flatpak uninstall $pkg | flatpak update $pkg | flatpak search $pkg | flatpak info $pkg | - | flatpak update | flatpak list | 86 | | guix | guix install $pkg | guix remove $pkg | guix upgrade $pkg | guix search $pkg | guix show $pkg | guix refresh | guix upgrade | guix package -I/--list-installed | 87 | | nala | nala install $pkg | nala remove $pkg | nala install $pkg | nala search $pkg | nala show $pkg | nala update | nala upgrade | nala list -i/--installed | 88 | | nix-env | nix-env -i/--install $pkg | nix-env -e/--uninstall $pkg | nix-env -u/--upgrade $pkg | nix-env -qaP $pkg | nix-env -qa --description $pkg | nix-channel --update | nix-env -u/--upgrade | nix-env -q/--query --installed | 89 | | opkg | opkg install $pkg | opkg remove $pkg | opkg upgrade $pkg | opkg find $pkg | opkg info $pkg | opkg update | opkg upgrade | opkg list --installed | 90 | | pacman | pacman -S $pkg | pacman -Rs $pkg | pacman -S $pkg | pacman -Ss $pkg | pacman -Si $pkg | pacman -Sy | pacman -Syu | pacman -Q | 91 | | pkg | pkg install $pkg | pkg remove $pkg | pkg install $pkg | pkg search $pkg | pkg info $pkg | pkg update | pkg upgrade | pkg info -a/--all | 92 | | pkg(termux) | pkg install $pkg | pkg uninstall $pkg | pkg install $pkg | pkg search $pkg | pkg show $pkg | pkg update | pkg upgrade | pkg list-installed | 93 | | pkgman | pkgman install $pkg | pkgman uninstall $pkg | pkgman update $pkg | pkgman search $pkg | - | pkgman refresh | pkgman update | pkgman search -i -a | 94 | | prt-get | prt-get install $pkg | prt-get remove $pkg | prt-get update $pkg | prt-get search $pkg | prt-get info $pkg | ports -u | prt-get sysup | prt-get listinst | 95 | | scoop | scoop install $pkg | scoop uninstall $pkg | scoop update $pkg | scoop search $pkg | scoop info $pkg | scoop update | scoop update * | scoop list | 96 | | slackpkg | slackpkg install $pkg | slackpkg remove $pkg | slackpkg upgrade $pkg | slackpkg search $pkg | slackpkg info $pkg | slackpkg update | slackpkg upgrade-all | ls -1 /var/log/packages | 97 | | snap | snap install --classic $pkg | snap remove $pkg | snap refresh $pkg | snap find $pkg | snap info $pkg | - | snap refresh | snap list | 98 | | urpm | urpmi $pkg | urpme $pkg | urpmi $pkg | urpmq -y/--fuzzy $pkg | urpmq -i $pkg | urpmi.update -a | urpmi --auto-update | rpm -q/--query --all | 99 | | winget | winget install $pkg | winget uninstall $pkg | winget upgrade $pkg | winget search $pkg | winget show $pkg | - | winget upgrade --all | winget list | 100 | | xbps | xbps-install $pkg | xbps-remove $pkg | xbps-install -u/--update $pkg | xbps-query -Rs $pkg | xbps-query -RS $pkg | xbps-install -S/--sync | xbps-install -u/--update | xbps-query -l/--list-pkgs | 101 | | yay | yay -S $pkg | yay -Rs $pkg | yay -S $pkg | yay -Ss $pkg | yay -Si $pkg | yay -Sy | yay -Syu | yay -Q | 102 | | yum | yum install $pkg | yum remove $pkg | yum upgrade $pkg | yum search $pkg | yum info $pkg | yum check-update | yum update | yum list --installed | 103 | | zypper | zypper install $pkg | zypper remove $pkg | zypper update $pkg | zypper search $pkg | zypper info $pkg | zypper refresh | zypper update | zypper search -i/--installed-only | 104 | ``` 105 | 106 | ### OS Tools 107 | 108 | ``` 109 | +------------------------------------------------------+----------------------+ 110 | | OS | Tools | 111 | +------------------------------------------------------+----------------------+ 112 | | windows | scoop, choco, winget | 113 | +------------------------------------------------------+----------------------+ 114 | | macos | brew, port | 115 | +------------------------------------------------------+----------------------+ 116 | | ubuntu, debian, linuxmint, pop, deepin, elementary | apt | 117 | | kali, raspbian, aosc, zorin, antix, devuan, bodhi | | 118 | | lxle, sparky | | 119 | +------------------------------------------------------+----------------------+ 120 | | fedora, redhat, rhel, amzn, ol, almalinux, rocky | dnf, yum | 121 | | oubes, centos, qubes, eurolinux | | 122 | +------------------------------------------------------+----------------------+ 123 | | arch, manjaro, endeavouros, arcolinux, garuda | pacman | 124 | | antergos, kaos | | 125 | +------------------------------------------------------+----------------------+ 126 | | alpine, postmarket | apk | 127 | +------------------------------------------------------+----------------------+ 128 | | opensuse, opensuse-leap, opensuse-tumbleweed | zypper | 129 | +------------------------------------------------------+----------------------+ 130 | | nixos | nix-env | 131 | +------------------------------------------------------+----------------------+ 132 | | gentoo, funtoo | emerge | 133 | +------------------------------------------------------+----------------------+ 134 | | void | xbps | 135 | +------------------------------------------------------+----------------------+ 136 | | mageia | urpm | 137 | +------------------------------------------------------+----------------------+ 138 | | slackware | slackpkg | 139 | +------------------------------------------------------+----------------------+ 140 | | solus | eopkg | 141 | +------------------------------------------------------+----------------------+ 142 | | openwrt | opkg | 143 | +------------------------------------------------------+----------------------+ 144 | | nutyx | cards | 145 | +------------------------------------------------------+----------------------+ 146 | | crux | prt-get | 147 | +------------------------------------------------------+----------------------+ 148 | | freebsd, ghostbsd | pkg | 149 | +------------------------------------------------------+----------------------+ 150 | | android | pkg(termux) | 151 | +------------------------------------------------------+----------------------+ 152 | | haiku | pkgman | 153 | +------------------------------------------------------+----------------------+ 154 | | windows/msys2 | pacman | 155 | +------------------------------------------------------+----------------------+ 156 | | * | apt, dnf, pacman | 157 | +------------------------------------------------------+----------------------+ 158 | ``` 159 | 160 | Upt will determine which package management tool to use based on the above table. 161 | 162 | Some platforms may support multiple package management tools, upt selects one of them in order. 163 | 164 | You can specify the package manager that UPT should use by setting the `UPT_TOOL` environment variable. 165 | 166 | ```sh 167 | UPT_TOOL=brew upt install $pkg # equal to `brew install $pkg` 168 | UPT_TOOL=nix-env upt install $pkg # equal to `nix-env -i $pkg` 169 | ``` 170 | 171 | ## License 172 | 173 | Copyright (c) 2023-∞ upt-developers. 174 | 175 | Upt is made available under the terms of either the MIT License or the Apache License 2.0, at your option. 176 | 177 | See the LICENSE-APACHE and LICENSE-MIT files for license details. 178 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | repo=sigoden/upt 4 | crate=upt 5 | url=https://github.com/$repo 6 | releases=$url/releases 7 | 8 | help() { 9 | cat <&2 32 | } 33 | 34 | err() { 35 | if [ ! -z ${td-} ]; then 36 | rm -rf $td 37 | fi 38 | 39 | say_err "error: $@" 40 | exit 1 41 | } 42 | 43 | need() { 44 | if ! command -v $1 > /dev/null 2>&1; then 45 | err "need $1 (command not found)" 46 | fi 47 | } 48 | 49 | force=false 50 | while test $# -gt 0; do 51 | case $1 in 52 | --force | -f) 53 | force=true 54 | ;; 55 | --help | -h) 56 | help 57 | exit 0 58 | ;; 59 | --tag) 60 | tag=$2 61 | shift 62 | ;; 63 | --target) 64 | target=$2 65 | shift 66 | ;; 67 | --to) 68 | dest=$2 69 | shift 70 | ;; 71 | *) 72 | ;; 73 | esac 74 | shift 75 | done 76 | 77 | # Dependencies 78 | need curl 79 | need install 80 | need mkdir 81 | need mktemp 82 | need tar 83 | 84 | # Optional dependencies 85 | if [ -z ${tag-} ]; then 86 | need grep 87 | need cut 88 | fi 89 | 90 | if [ -z ${target-} ]; then 91 | need cut 92 | fi 93 | 94 | if [ -z ${dest-} ]; then 95 | dest="/usr/local/bin" 96 | if [ ! -d $dest ]; then 97 | dest="/usr/bin" 98 | fi 99 | fi 100 | 101 | 102 | if [ -z ${tag-} ]; then 103 | tag=$(curl -sSf https://api.github.com/repos/$repo/releases/latest | 104 | grep tag_name | 105 | cut -d'"' -f4 106 | ) 107 | fi 108 | 109 | if [ -z ${target-} ]; then 110 | # bash compiled with MINGW (e.g. git-bash, used in github windows runners), 111 | # unhelpfully includes a version suffix in `uname -s` output, so handle that. 112 | # e.g. MINGW64_NT-10-0.19044 113 | kernel=$(uname -s | cut -d- -f1) 114 | uname_target="`uname -m`-$kernel" 115 | 116 | case $uname_target in 117 | aarch64-Linux) target=aarch64-unknown-linux-musl;; 118 | arm64-Darwin) target=aarch64-apple-darwin;; 119 | x86_64-Darwin) target=x86_64-apple-darwin;; 120 | x86_64-Linux) target=x86_64-unknown-linux-musl;; 121 | x86_64-Windows_NT) target=x86_64-pc-windows-msvc;; 122 | x86_64-MINGW64_NT) target=x86_64-pc-windows-msvc;; 123 | *) 124 | err 'Could not determine target from output of `uname -m`-`uname -s`, please use `--target`:' $uname_target 125 | ;; 126 | esac 127 | fi 128 | 129 | # windows archives are zips, not tarballs 130 | case $target in 131 | x86_64-pc-windows-msvc) extension=zip; need unzip;; 132 | *) extension=tar.gz;; 133 | esac 134 | 135 | archive="$releases/download/$tag/$crate-$tag-$target.$extension" 136 | 137 | say_err "Repository: $url" 138 | say_err "Crate: $crate" 139 | say_err "Tag: $tag" 140 | say_err "Target: $target" 141 | say_err "Destination: $dest" 142 | say_err "Archive: $archive" 143 | 144 | td=$(mktemp -d || mktemp -d -t tmp) 145 | 146 | if [ "$extension" = "zip" ]; then 147 | # unzip on windows cannot always handle stdin, so download first. 148 | curl -sSfL $archive > $td/$crate.zip 149 | unzip -d $td $td/$crate.zip 150 | else 151 | curl -sSfL $archive | tar -C $td -xz 152 | fi 153 | 154 | for f in $(ls $td); do 155 | test -x $td/$f || continue 156 | 157 | if [ -e "$dest/$f" ] && [ $force = false ]; then 158 | err "$f already exists in $dest" 159 | else 160 | mkdir -p $dest 161 | install -m 755 $td/$f $dest 162 | fi 163 | done 164 | 165 | rm -rf $td 166 | -------------------------------------------------------------------------------- /src/action.rs: -------------------------------------------------------------------------------- 1 | use crate::UptError; 2 | 3 | use std::cell::Cell; 4 | use std::str::FromStr; 5 | 6 | #[derive(Debug, Clone, PartialEq, Default)] 7 | pub(crate) struct Action { 8 | cmd: String, 9 | subcmd: Vec, 10 | options: Vec>, 11 | args: Vec, 12 | has_pkg: bool, 13 | } 14 | 15 | impl FromStr for Action { 16 | type Err = UptError; 17 | fn from_str(s: &str) -> Result { 18 | if s.is_empty() { 19 | return Ok(Default::default()); 20 | } 21 | let words: Vec<&str> = s.split(' ').collect(); 22 | let mut has_pkg = false; 23 | let mut options: Vec> = vec![]; 24 | let mut args = vec![]; 25 | if words.len() < 2 { 26 | return Err(UptError::InvalidAction(s.to_string())); 27 | } 28 | let (cmd, subcmd, reminder) = if words[1].starts_with('-') || words[1] == "$" { 29 | (words[0].to_string(), vec![], &words[1..]) 30 | } else { 31 | (words[0].to_string(), split(words[1]), &words[2..]) 32 | }; 33 | for elem in reminder { 34 | if elem == &"$" { 35 | has_pkg = true; 36 | continue; 37 | } 38 | if elem.starts_with('-') { 39 | options.push(split(elem)); 40 | } else { 41 | args.push(elem.to_string()); 42 | } 43 | } 44 | Ok(Action { 45 | cmd, 46 | subcmd, 47 | options, 48 | args, 49 | has_pkg, 50 | }) 51 | } 52 | } 53 | 54 | impl Action { 55 | pub fn parse(&self, args: &[String], confirm: &str) -> Option<(Option, bool)> { 56 | if self.invalid() { 57 | return None; 58 | } 59 | let (options, pkg) = self.parse_args(args)?; 60 | if (pkg.is_none() && self.has_pkg) || (pkg.is_some() && !self.has_pkg) { 61 | return None; 62 | } 63 | let no_confirm_options: Vec = options 64 | .iter() 65 | .filter(|v| !confirm.split('/').any(|y| &y == v)) 66 | .cloned() 67 | .collect(); 68 | 69 | let confirm = no_confirm_options.len() != options.len(); 70 | if !self.satisfy_options(&no_confirm_options) { 71 | return None; 72 | } 73 | Some((pkg, confirm)) 74 | } 75 | 76 | pub fn to_cmd(&self, pkg: &str, confirm: &str) -> Option> { 77 | if self.invalid() { 78 | return None; 79 | } 80 | let mut segs = vec![self.cmd.to_string()]; 81 | if let Some(action) = self.subcmd.first() { 82 | segs.push(action.clone()); 83 | } 84 | for item in &self.options { 85 | segs.push(item[0].clone()); 86 | } 87 | if !self.args.is_empty() { 88 | segs.extend(self.args.iter().cloned()); 89 | } 90 | if !pkg.is_empty() { 91 | segs.push(pkg.to_string()); 92 | } 93 | if !confirm.is_empty() { 94 | segs.push(confirm.to_string()); 95 | } 96 | Some(segs) 97 | } 98 | 99 | pub fn help(&self) -> Option { 100 | if self.invalid() { 101 | return None; 102 | } 103 | let mut segs: Vec = vec![self.cmd.clone()]; 104 | 105 | if !self.subcmd.is_empty() { 106 | segs.push(join(&self.subcmd)); 107 | } 108 | 109 | for item in &self.options { 110 | if item.len() > 1 { 111 | segs.push(join(item)); 112 | } else { 113 | segs.push(item[0].clone()); 114 | } 115 | } 116 | if self.has_pkg { 117 | segs.push(String::from("")); 118 | } 119 | Some(segs.join(" ")) 120 | } 121 | 122 | fn invalid(&self) -> bool { 123 | self.cmd.is_empty() 124 | } 125 | 126 | fn parse_args(&self, args: &[String]) -> Option<(Vec, Option)> { 127 | if args.len() < 2 { 128 | return None; 129 | } 130 | if self.cmd != args[0] { 131 | return None; 132 | } 133 | let reminder = if self.subcmd.is_empty() { 134 | &args[1..] 135 | } else { 136 | if !self.subcmd.contains(&args[1]) { 137 | return None; 138 | } 139 | &args[2..] 140 | }; 141 | let mut options: Vec = vec![]; 142 | let mut operands: Vec = vec![]; 143 | for arg in reminder.iter() { 144 | if arg.starts_with("--") { 145 | options.push(arg.to_string()); 146 | } else if arg.starts_with('-') { 147 | // split combined short options, -Syy => ["-S", "-y", "-y"] 148 | for i in 1..arg.len() { 149 | let single_arg = "-".to_string() + &arg[i..=i]; 150 | options.push(single_arg); 151 | } 152 | } else { 153 | operands.push(arg.to_string()); 154 | } 155 | } 156 | let pkg = if operands.is_empty() { 157 | None 158 | } else { 159 | Some(operands.join(" ")) 160 | }; 161 | Some((options, pkg)) 162 | } 163 | 164 | fn satisfy_options(&self, options: &[String]) -> bool { 165 | let marks: Vec> = self.options.iter().map(|_| Cell::new(false)).collect(); 166 | if options.len() != self.options.len() { 167 | return false; 168 | } 169 | options.iter().all(|v| { 170 | self.options.iter().enumerate().any(|(i, y)| { 171 | let mark = marks.get(i).unwrap(); 172 | if !mark.get() && y.iter().any(|z| z == v) { 173 | mark.set(true); 174 | return true; 175 | } 176 | false 177 | }) 178 | }) 179 | } 180 | } 181 | 182 | /// used in vendor! 183 | pub(crate) fn must_from_str(s: &str, name: &str, field: &str) -> Action { 184 | match Action::from_str(s) { 185 | Ok(p) => p, 186 | Err(_) => panic!("Failed to parse {}.{} from '{}' ", name, field, s), 187 | } 188 | } 189 | 190 | fn split(v: &str) -> Vec { 191 | v.split('/').map(|x| x.to_string()).collect::>() 192 | } 193 | 194 | fn join(v: &[String]) -> String { 195 | v.join("/") 196 | } 197 | 198 | #[cfg(test)] 199 | mod tests { 200 | use super::Action; 201 | use std::str::FromStr; 202 | 203 | #[test] 204 | fn test_action_from_str() { 205 | assert_eq!( 206 | Action::from_str("upt install $").unwrap(), 207 | Action { 208 | cmd: "upt".to_string(), 209 | subcmd: vec!["install".to_string()], 210 | options: vec![], 211 | args: vec![], 212 | has_pkg: true, 213 | } 214 | ); 215 | assert_eq!( 216 | Action::from_str("upt search $").unwrap(), 217 | Action { 218 | cmd: "upt".to_string(), 219 | subcmd: vec!["search".to_string()], 220 | options: vec![], 221 | args: vec![], 222 | has_pkg: true, 223 | } 224 | ); 225 | assert_eq!( 226 | Action::from_str("upt remove/uninstall $").unwrap(), 227 | Action { 228 | cmd: "upt".to_string(), 229 | subcmd: vec!["remove".to_string(), "uninstall".to_string()], 230 | options: vec![], 231 | args: vec![], 232 | has_pkg: true, 233 | } 234 | ); 235 | assert_eq!( 236 | Action::from_str("apt list --installed").unwrap(), 237 | Action { 238 | cmd: "apt".to_string(), 239 | subcmd: vec!["list".to_string()], 240 | options: vec![vec!["--installed".to_string()]], 241 | args: vec![], 242 | has_pkg: false, 243 | } 244 | ); 245 | assert_eq!( 246 | Action::from_str("pacman -R -s $").unwrap(), 247 | Action { 248 | cmd: "pacman".to_string(), 249 | subcmd: vec![], 250 | options: vec![vec!["-R".to_string()], vec!["-s".to_string()]], 251 | args: vec![], 252 | has_pkg: true, 253 | } 254 | ); 255 | assert_eq!( 256 | Action::from_str("pacman -S -y -y").unwrap(), 257 | Action { 258 | cmd: "pacman".to_string(), 259 | subcmd: vec![], 260 | options: vec![ 261 | vec!["-S".to_string()], 262 | vec!["-y".to_string()], 263 | vec!["-y".to_string()] 264 | ], 265 | args: vec![], 266 | has_pkg: false, 267 | } 268 | ); 269 | assert_eq!( 270 | Action::from_str("pacman -S $").unwrap(), 271 | Action { 272 | cmd: "pacman".to_string(), 273 | subcmd: vec![], 274 | options: vec![vec!["-S".to_string()]], 275 | args: vec![], 276 | has_pkg: true, 277 | } 278 | ); 279 | assert_eq!( 280 | Action::from_str("scoop update *").unwrap(), 281 | Action { 282 | cmd: "scoop".to_string(), 283 | subcmd: vec!["update".to_string()], 284 | options: vec![], 285 | args: vec!["*".to_string()], 286 | has_pkg: false, 287 | } 288 | ); 289 | assert_eq!( 290 | Action::from_str("choco upgrade all").unwrap(), 291 | Action { 292 | cmd: "choco".to_string(), 293 | subcmd: vec!["upgrade".to_string()], 294 | options: vec![], 295 | args: vec!["all".to_string()], 296 | has_pkg: false, 297 | } 298 | ); 299 | } 300 | 301 | macro_rules! check_action_parse { 302 | ($input:expr, $confirm:expr, [$($args:expr),*], ($pkg:expr, $confirm_result:expr)) => { 303 | { 304 | let action = Action::from_str($input).unwrap(); 305 | let args = vec![$($args.to_string()),*]; 306 | let pkg = if $pkg.len() == 0 { 307 | None 308 | } else { 309 | Some($pkg.to_string()) 310 | }; 311 | assert_eq!(action.parse(&args, $confirm).unwrap(), (pkg, $confirm_result)); 312 | } 313 | }; 314 | ($input:expr, $confirm:expr, [$($args:expr),*]) => { 315 | { 316 | let action = Action::from_str($input).unwrap(); 317 | let args = vec![ $($args.to_string()),*]; 318 | assert_eq!(action.parse(&args, $confirm), None); 319 | } 320 | } 321 | } 322 | 323 | #[test] 324 | fn test_action_parse() { 325 | check_action_parse!( 326 | "apt install $", 327 | "-y/--confirm", 328 | ["apt", "install", "vim"], 329 | ("vim", false) 330 | ); 331 | check_action_parse!( 332 | "apt install $", 333 | "-y/--confirm", 334 | ["apt", "install", "-y", "vim"], 335 | ("vim", true) 336 | ); 337 | check_action_parse!( 338 | "apt install $", 339 | "-y/--confirm", 340 | ["apt", "install", "--confirm", "vim"], 341 | ("vim", true) 342 | ); 343 | check_action_parse!( 344 | "apt install $", 345 | "-y/--confirm", 346 | ["apt", "install", "vim", "jq"], 347 | ("vim jq", false) 348 | ); 349 | check_action_parse!("apt install $", "-y/--confirm", ["upt", "install", "vim"]); 350 | check_action_parse!("apt search $", "", ["apt", "search", "vim"], ("vim", false)); 351 | check_action_parse!( 352 | "apt list --installed", 353 | "", 354 | ["apt", "list", "--installed"], 355 | ("", false) 356 | ); 357 | check_action_parse!( 358 | "pacman -R -s $", 359 | "--noconfirm", 360 | ["pacman", "-R", "-s", "--noconfirm", "vim"], 361 | ("vim", true) 362 | ); 363 | check_action_parse!( 364 | "pacman -R -s $", 365 | "--noconfirm", 366 | ["pacman", "-Rs", "vim"], 367 | ("vim", false) 368 | ); 369 | check_action_parse!( 370 | "pacman -R -s $", 371 | "--noconfirm", 372 | ["pacman", "-Rs", "--noconfirm", "vim", "jq"], 373 | ("vim jq", true) 374 | ); 375 | check_action_parse!("pacman -S -y -y", "", ["pacman", "-Syy"], ("", false)); 376 | check_action_parse!("pacman -S $", "", ["pacman", "-S", "vim"], ("vim", false)); 377 | check_action_parse!("apt search $", "", ["apt", "search"]); 378 | check_action_parse!("apt upgrade", "", ["apt", "upgrade", "vim"]); 379 | check_action_parse!("pacman -S -y -y", "", ["pacman", "-Sy"]); 380 | check_action_parse!("pacman -S -y -y", "", ["pacman", "-Syyy"]); 381 | check_action_parse!("pacman -Q -i", "", ["pacman", "-Qiy"]); 382 | } 383 | 384 | macro_rules! check_action_to_cmd { 385 | ($input:expr, ($pkg:expr, $confirm:expr), $cmd:expr) => {{ 386 | let action = Action::from_str($input).unwrap(); 387 | assert_eq!( 388 | action.to_cmd($pkg, $confirm).map(|v| v.join(" ")), 389 | Some($cmd.to_string()) 390 | ); 391 | }}; 392 | ($input:expr, ($pkg:expr, $confirm:expr)) => {{ 393 | let action = Action::from_str($input).unwrap(); 394 | assert!(action.to_cmd($pkg, $confirm).is_none()); 395 | }}; 396 | } 397 | 398 | #[test] 399 | fn test_action_to_cmd() { 400 | check_action_to_cmd!("apt install $", ("vim", ""), "apt install vim"); 401 | check_action_to_cmd!("apt install $", ("vim", "-y"), "apt install vim -y"); 402 | check_action_to_cmd!("apt install $", ("vim jq", ""), "apt install vim jq"); 403 | check_action_to_cmd!("apt search $", ("vim", ""), "apt search vim"); 404 | check_action_to_cmd!("apt list --installed", ("", ""), "apt list --installed"); 405 | check_action_to_cmd!( 406 | "pacman -R -s $", 407 | ("vim", "--noconfirm"), 408 | "pacman -R -s vim --noconfirm" 409 | ); 410 | check_action_to_cmd!("pacman -R -s $", ("vim", ""), "pacman -R -s vim"); 411 | check_action_to_cmd!( 412 | "pacman -R -s $", 413 | ("vim jq", "--noconfirm"), 414 | "pacman -R -s vim jq --noconfirm" 415 | ); 416 | check_action_to_cmd!("pacman -S -y -y", ("", ""), "pacman -S -y -y"); 417 | check_action_to_cmd!("pacman -S $", ("vim", ""), "pacman -S vim"); 418 | check_action_to_cmd!("scoop update *", ("", ""), "scoop update *"); 419 | check_action_to_cmd!("choco upgrade all", ("", "-y"), "choco upgrade all -y"); 420 | } 421 | 422 | macro_rules! check_action_help { 423 | ($input:expr, $help:expr) => {{ 424 | let action = Action::from_str($input).unwrap(); 425 | assert_eq!(action.help(), Some($help.to_string())); 426 | }}; 427 | ($input:expr) => {{ 428 | let action = Action::from_str($input).unwrap(); 429 | assert!(action.help().is_none()); 430 | }}; 431 | } 432 | 433 | #[test] 434 | fn test_action_help() { 435 | check_action_help!("upt install $", "upt install "); 436 | check_action_help!("upt search $", "upt search "); 437 | check_action_help!("upt list -i/--installed", "upt list -i/--installed"); 438 | check_action_help!("pacman -S -y -y", "pacman -S -y -y"); 439 | check_action_help!("pacman -S $", "pacman -S "); 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | #[derive(Debug, PartialEq)] 5 | pub enum UptError { 6 | NoVendor(String), 7 | NoDetectVendor, 8 | InvalidTask, 9 | InvalidAction(String), 10 | InvalidArgs(String), 11 | DisplayHelp(String), 12 | } 13 | 14 | impl Error for UptError {} 15 | 16 | impl fmt::Display for UptError { 17 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 18 | use UptError::*; 19 | match self { 20 | NoVendor(v) => write!(f, "The package management tool '{}' is not supported.", v), 21 | NoDetectVendor => write!( 22 | f, 23 | "No package management tool available, use `$UPT_TOOL` to specify one." 24 | ), 25 | InvalidTask => write!(f, "The package management tool cannot perform the task."), 26 | InvalidAction(v) => write!(f, "Invalid action '{}'.", v), 27 | InvalidArgs(v) => write!(f, "Invalid arguments.\n\n{}", v), 28 | DisplayHelp(v) => write!(f, "{}", v), 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod macros; 3 | 4 | mod action; 5 | mod error; 6 | mod task; 7 | mod utils; 8 | mod vendor; 9 | 10 | pub use error::UptError; 11 | pub use utils::detect_os; 12 | pub use vendor::{detect_vendor, init_vendor, Vendor}; 13 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! vendors { 2 | ( 3 | $( 4 | { 5 | name: $name:literal, 6 | confirm: $confirm:literal, 7 | install: $install:literal, 8 | remove: $remove:literal, 9 | upgrade: $upgrade:literal, 10 | search: $search:literal, 11 | info: $show:literal, 12 | update_index: $update_index:literal, 13 | upgrade_all: $upgrade_all:literal, 14 | list_installed: $list_installed:literal, 15 | }, 16 | )+ 17 | ) => { 18 | pub fn init_vendor(name: &str) -> Result<$crate::Vendor, $crate::UptError> { 19 | use $crate::action::must_from_str; 20 | match name { 21 | $( 22 | $name => { 23 | let vendor = $crate::Vendor { 24 | name: $name.to_string(), 25 | confirm: $confirm.to_string(), 26 | install: must_from_str($install, $name, "install"), 27 | remove: must_from_str($remove, $name, "remove"), 28 | upgrade: must_from_str($upgrade, $name, "upgrade"), 29 | search: must_from_str($search, $name, "search"), 30 | info: must_from_str($show, $name, "show"), 31 | update_index: must_from_str($update_index, $name, "update_index"), 32 | upgrade_all: must_from_str($upgrade_all, $name, "upgrade_all"), 33 | list_installed: must_from_str($list_installed, $name, "list_installed"), 34 | }; 35 | Ok(vendor) 36 | }, 37 | )+ 38 | _ => Err(UptError::NoVendor(name.to_string())) 39 | } 40 | } 41 | 42 | pub(crate) fn which_cmd(name: &str) -> Option<&'static str> { 43 | match name { 44 | $( 45 | $name => { 46 | let (cmd, _) = $install.split_once(' ')?; 47 | Some(cmd) 48 | } 49 | )+ 50 | _ => None 51 | } 52 | } 53 | 54 | #[allow(unused)] 55 | pub(crate) fn support_tools() -> Vec<&'static str> { 56 | vec![$( $name,)+] 57 | } 58 | } 59 | } 60 | 61 | macro_rules! os_vendors { 62 | ($($os:literal => $($tool:literal),+);+$(;)?) => { 63 | pub fn detect_vendor(os: &str) -> std::result::Result<$crate::Vendor, $crate::UptError> { 64 | let pairs: Vec<(&str, &str)> = match os { 65 | $( 66 | $os => vec![$($tool),+].into_iter().filter_map(|tool| which_cmd(tool).map(|bin_name| (tool, bin_name))).collect(), 67 | )+ 68 | "windows/msys2" => vec![("pacman","pacman")], 69 | _ => ["apt", "dnf", "pacman"].into_iter().map(|tool| (tool, tool)).collect(), 70 | }; 71 | match $crate::utils::find_tool(&pairs) { 72 | Some(tool) => $crate::vendor::init_vendor(&tool), 73 | None => Err(UptError::NoDetectVendor), 74 | } 75 | } 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::process::Command; 3 | use std::{env, process}; 4 | use upt::{detect_os, detect_vendor, init_vendor, UptError, Vendor}; 5 | 6 | fn main() { 7 | match run() { 8 | Ok(c) => { 9 | process::exit(c); 10 | } 11 | Err(e) => { 12 | eprintln!("Error: {}", e); 13 | process::exit(1); 14 | } 15 | } 16 | } 17 | 18 | fn run() -> Result> { 19 | let env_args = env::args().collect::>(); 20 | let bin = Path::new(&env_args[0]) 21 | .file_stem() 22 | .unwrap() 23 | .to_str() 24 | .unwrap(); 25 | let vendor = init_vendor(bin)?; 26 | let mut args = vec![bin.to_string()]; 27 | args.extend(env_args.iter().skip(1).cloned()); 28 | let os = detect_os().unwrap_or_default(); 29 | let cmd_args = match create_cmd(&vendor, &args, &os) { 30 | Ok(v) => v, 31 | Err(UptError::DisplayHelp(t)) => { 32 | println!("{t}"); 33 | return Ok(0); 34 | } 35 | Err(e) => return Err(e.into()), 36 | }; 37 | if let Ok(v) = std::env::var("UPT_DRY_RUN") { 38 | if v == "true" || v == "1" { 39 | println!("{}", cmd_args.join(" ")); 40 | return Ok(0); 41 | } 42 | } 43 | let cmd = &cmd_args[0]; 44 | let cmd = match which::which(cmd) { 45 | Ok(v) => v, 46 | Err(_) => return Err(format!("Command '{cmd}' not found.").into()), 47 | }; 48 | let status = Command::new(cmd).args(&cmd_args[1..]).status()?; 49 | 50 | Ok(status.code().unwrap_or_default()) 51 | } 52 | 53 | fn create_cmd(vendor: &Vendor, args: &[String], os: &str) -> Result, UptError> { 54 | let tool = match std::env::var("UPT_TOOL") { 55 | Ok(v) => init_vendor(&v)?, 56 | Err(_) => detect_vendor(os)?, 57 | }; 58 | let task = vendor.parse(args, tool.name())?; 59 | let cmd = tool.eval(&task)?; 60 | Ok(cmd) 61 | } 62 | -------------------------------------------------------------------------------- /src/task.rs: -------------------------------------------------------------------------------- 1 | /// General tasks that every vender provides 2 | #[derive(Debug, PartialEq)] 3 | pub enum Task { 4 | /// install packages 5 | Install { pkg: String, confirm: bool }, 6 | /// remove packages 7 | Remove { pkg: String, confirm: bool }, 8 | /// upgrade packages 9 | Upgrade { pkg: String, confirm: bool }, 10 | /// search for a package 11 | Search { pkg: String }, 12 | /// show a package info 13 | Info { pkg: String }, 14 | /// sync packages index 15 | UpdateIndex, 16 | /// upgrade all outdated packages 17 | UpgradeAll { confirm: bool }, 18 | /// list all installed packages 19 | ListInstalled, 20 | } 21 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use which::which; 2 | 3 | pub fn find_tool(pairs: &[(&str, &str)]) -> Option { 4 | match pairs.len() { 5 | 0 => None, 6 | 1 => { 7 | let (tool, bin_name) = &pairs[0]; 8 | if which(bin_name).is_ok() { 9 | Some(tool.to_string()) 10 | } else { 11 | None 12 | } 13 | } 14 | _ => { 15 | let handles: Vec<_> = pairs 16 | .iter() 17 | .map(|(tool, bin_name)| { 18 | let tool = tool.to_string(); 19 | let bin_name = bin_name.to_string(); 20 | std::thread::spawn(move || { 21 | if which(&bin_name).is_ok() { 22 | Some(tool) 23 | } else { 24 | None 25 | } 26 | }) 27 | }) 28 | .collect(); 29 | for handle in handles { 30 | if let Ok(Some(tool)) = handle.join() { 31 | return Some(tool); 32 | } 33 | } 34 | None 35 | } 36 | } 37 | } 38 | 39 | #[cfg(target_os = "windows")] 40 | pub fn detect_os() -> Option { 41 | if std::env::var("MSYSTEM").is_ok() { 42 | let os = "windows/msys2"; 43 | if let Ok(output) = std::process::Command::new("sh") 44 | .arg("-c") 45 | .arg("which pacman") 46 | .output() 47 | { 48 | if output.status.success() { 49 | return Some(os.to_string()); 50 | } 51 | } 52 | } 53 | Some("windows".to_string()) 54 | } 55 | 56 | #[cfg(target_os = "macos")] 57 | pub fn detect_os() -> Option { 58 | Some("macos".to_string()) 59 | } 60 | 61 | #[cfg(target_os = "android")] 62 | pub fn detect_os() -> Option { 63 | Some("android".to_string()) 64 | } 65 | 66 | #[cfg(target_os = "haiku")] 67 | pub fn detect_os() -> Option { 68 | Some("haiku".to_string()) 69 | } 70 | 71 | #[cfg(not(any( 72 | target_os = "windows", 73 | target_os = "macos", 74 | target_os = "android", 75 | target_os = "haiku" 76 | )))] 77 | pub fn detect_os() -> Option { 78 | let release = std::fs::read_to_string("/etc/os-release").ok()?; 79 | let id = release.lines().find(|l| l.starts_with("ID="))?; 80 | let id = id[3..].trim_matches('"'); 81 | Some(id.to_string()) 82 | } 83 | -------------------------------------------------------------------------------- /src/vendor.rs: -------------------------------------------------------------------------------- 1 | use crate::action::Action; 2 | use crate::error::UptError; 3 | use crate::task::Task; 4 | 5 | os_vendors!( 6 | "windows" => "scoop", "choco", "winget"; 7 | "macos" => "brew", "port"; 8 | // apt 9 | "ubuntu" => "apt"; 10 | "debian" => "apt"; 11 | "linuxmint" => "apt"; 12 | "pop" => "apt"; 13 | "deepin" => "apt"; 14 | "elementary" => "apt"; 15 | "kali" => "apt"; 16 | "raspbian" => "apt"; 17 | "aosc" => "apt"; 18 | "zorin" => "apt"; 19 | "antix" => "apt"; 20 | "devuan" => "apt"; 21 | "bodhi" => "apt"; 22 | "lxle" => "apt"; 23 | "sparky" => "apt"; 24 | // dnf 25 | "fedora" => "dnf", "yum"; 26 | "redhat" => "dnf", "yum"; 27 | "rhel" => "dnf", "yum"; 28 | "amzn" => "dnf", "yum"; 29 | "ol" => "dnf", "yum"; 30 | "almalinux" => "dnf", "yum"; 31 | "rocky" => "dnf", "yum"; 32 | "oubes" => "dnf", "yum"; 33 | "centos" => "dnf", "yum"; 34 | "qubes" => "dnf", "yum"; 35 | "eurolinux" => "dnf", "yum"; 36 | // pacman 37 | "arch" => "pacman"; 38 | "manjaro" => "pacman"; 39 | "endeavouros" => "pacman"; 40 | "arcolinux" => "pacman"; 41 | "garuda" => "pacman"; 42 | "antergos" => "pacman"; 43 | "kaos" => "pacman"; 44 | // apk 45 | "alpine" => "apk"; 46 | "postmarket" => "apk"; 47 | // zypper 48 | "opensuse" => "zypper"; 49 | "opensuse-leap" => "zypper"; 50 | "opensuse-tumbleweed" => "zypper"; 51 | // nix 52 | "nixos" => "nix-env"; 53 | // emerge 54 | "gentoo" => "emerge"; 55 | "funtoo" => "emerge"; 56 | // xps 57 | "void" => "xbps"; 58 | // urpm 59 | "mageia" => "urpm"; 60 | // slackpkg 61 | "slackware" => "slackpkg"; 62 | // eopkg 63 | "solus" => "eopkg"; 64 | // opkg 65 | "openwrt" => "opkg"; 66 | // cards 67 | "nutyx" => "cards"; 68 | // prt-get 69 | "crux" => "prt-get"; 70 | // pkg 71 | "freebsd" => "pkg"; 72 | "ghostbsd" => "pkg"; 73 | // pkg(termux) 74 | "android" => "pkg(termux)"; 75 | // pkgman 76 | "haiku" => "pkgman"; 77 | ); 78 | 79 | vendors![ 80 | { 81 | name: "upt", 82 | confirm: "-y/--yes", 83 | install: "upt install $", 84 | remove: "upt remove/uninstall $", 85 | upgrade: "upt upgrade $", 86 | search: "upt search $", 87 | info: "upt info/show $", 88 | update_index: "upt update", 89 | upgrade_all: "upt upgrade", 90 | list_installed: "upt list", 91 | }, 92 | { 93 | name: "apk", 94 | confirm: "", 95 | install: "apk add $", 96 | remove: "apk del $", 97 | upgrade: "apk upgrade $", 98 | search: "apk search $", 99 | info: "apk info $", 100 | update_index: "apk update", 101 | upgrade_all: "apk upgrade", 102 | list_installed: "apk list -I/--installed", 103 | }, 104 | { 105 | name: "apt", 106 | confirm: "-y/--yes", 107 | install: "apt install $", 108 | remove: "apt remove $", 109 | upgrade: "apt install --only-upgrade $", 110 | search: "apt search $", 111 | info: "apt show $", 112 | update_index: "apt update", 113 | upgrade_all: "apt upgrade", 114 | list_installed: "apt list -i/--installed", 115 | }, 116 | { 117 | name: "brew", 118 | confirm: "", 119 | install: "brew install $", 120 | remove: "brew uninstall $", 121 | upgrade: "brew upgrade $", 122 | search: "brew search $", 123 | info: "brew info $", 124 | update_index: "brew update", 125 | upgrade_all: "brew upgrade", 126 | list_installed: "brew list", 127 | }, 128 | { 129 | name: "cards", 130 | confirm: "", 131 | install: "cards install $", 132 | remove: "cards remove $", 133 | upgrade: "cards install -u/--upgrade $", 134 | search: "cards search $", 135 | info: "cards info $", 136 | update_index: "cards sync", 137 | upgrade_all: "cards upgrade", 138 | list_installed: "cards list", 139 | }, 140 | { 141 | name: "choco", 142 | confirm: "-y/--yes", 143 | install: "choco install $", 144 | remove: "choco uninstall $", 145 | upgrade: "choco upgrade $", 146 | search: "choco search $", 147 | info: "choco info $", 148 | update_index: "", 149 | upgrade_all: "choco upgrade all", 150 | list_installed: "choco list", 151 | }, 152 | { 153 | name: "dnf", 154 | confirm: "-y/--assumeyes", 155 | install: "dnf install $", 156 | remove: "dnf remove $", 157 | upgrade: "dnf upgrade $", 158 | search: "dnf search $", 159 | info: "dnf info $", 160 | update_index: "dnf check-update", 161 | upgrade_all: "dnf update", 162 | list_installed: "dnf list --installed", 163 | }, 164 | { 165 | name: "emerge", 166 | confirm: "", 167 | install: "emerge $", 168 | remove: "emerge --depclean $", 169 | upgrade: "emerge --update $", 170 | search: "emerge --search $", 171 | info: "emerge --info $", 172 | update_index: "emerge --sync", 173 | upgrade_all: "emerge -vuDN @world", 174 | list_installed: "qlist -Iv", 175 | }, 176 | { 177 | name: "eopkg", 178 | confirm: "-y/--yes-all", 179 | install: "eopkg install $", 180 | remove: "eopkg remove $", 181 | upgrade: "eopkg upgrade $", 182 | search: "eopkg search $", 183 | info: "eopkg info $", 184 | update_index: "eopkg update-repo", 185 | upgrade_all: "eopkg upgrade", 186 | list_installed: "eopkg list-installed", 187 | }, 188 | { 189 | name: "flatpak", 190 | confirm: " -y/--assumeyes", 191 | install: "flatpak install $", 192 | remove: "flatpak uninstall $", 193 | upgrade: "flatpak update $", 194 | search: "flatpak search $", 195 | info: "flatpak info $", 196 | update_index: "", 197 | upgrade_all: "flatpak update", 198 | list_installed: "flatpak list", 199 | }, 200 | { 201 | name: "guix", 202 | confirm: "", 203 | install: "guix install $", 204 | remove: "guix remove $", 205 | upgrade: "guix upgrade $", 206 | search: "guix search $", 207 | info: "guix show $", 208 | update_index: "guix refresh", 209 | upgrade_all: "guix upgrade", 210 | list_installed: "guix package -I/--list-installed", 211 | }, 212 | { 213 | name: "nala", 214 | confirm: "-y/--assume-yes", 215 | install: "nala install $", 216 | remove: "nala remove $", 217 | upgrade: "nala install $", 218 | search: "nala search $", 219 | info: "nala show $", 220 | update_index: "nala update", 221 | upgrade_all: "nala upgrade", 222 | list_installed: "nala list -i/--installed", 223 | }, 224 | { 225 | name: "nix-env", 226 | confirm: "", 227 | install: "nix-env -i/--install $", 228 | remove: "nix-env -e/--uninstall $", 229 | upgrade: "nix-env -u/--upgrade $", 230 | search: "nix-env -qaP $", 231 | info: "nix-env -qa --description $", 232 | update_index: "nix-channel --update", 233 | upgrade_all: "nix-env -u/--upgrade", 234 | list_installed: "nix-env -q/--query --installed", 235 | }, 236 | { 237 | name: "opkg", 238 | confirm: "", 239 | install: "opkg install $", 240 | remove: "opkg remove $", 241 | upgrade: "opkg upgrade $", 242 | search: "opkg find $", 243 | info: "opkg info $", 244 | update_index: "opkg update", 245 | upgrade_all: "opkg upgrade", 246 | list_installed: "opkg list-installed", 247 | }, 248 | { 249 | name: "pacman", 250 | confirm: "--noconfirm", 251 | install: "pacman -S $", 252 | remove: "pacman -R -s $", 253 | upgrade: "pacman -S $", 254 | search: "pacman -S -s $", 255 | info: "pacman -S -i $", 256 | update_index: "pacman -S -y", 257 | upgrade_all: "pacman -S -y -u", 258 | list_installed: "pacman -Q", 259 | }, 260 | { 261 | name: "pkg", 262 | confirm: "-y/--yes", 263 | install: "pkg install $", 264 | remove: "pkg remove $", 265 | upgrade: "pkg install $", 266 | search: "pkg search $", 267 | info: "pkg info $", 268 | update_index: "pkg update", 269 | upgrade_all: "pkg upgrade", 270 | list_installed: "pkg info -a/--all", 271 | }, 272 | { 273 | name: "pkg(termux)", 274 | confirm: "-y/--yes", 275 | install: "pkg install $", 276 | remove: "pkg uninstall $", 277 | upgrade: "pkg install $", 278 | search: "pkg search $", 279 | info: "pkg show $", 280 | update_index: "pkg update", 281 | upgrade_all: "pkg upgrade", 282 | list_installed: "pkg list-installed", 283 | }, 284 | { 285 | name: "pkgman", 286 | confirm: "-y", 287 | install: "pkgman install $", 288 | remove: "pkgman uninstall $", 289 | upgrade: "pkgman update $", 290 | search: "pkgman search $", 291 | info: "", 292 | update_index: "pkgman refresh", 293 | upgrade_all: "pkgman update", 294 | list_installed: "pkgman search -i/--installed-only -a/--all", 295 | }, 296 | { 297 | name: "prt-get", 298 | confirm: "", 299 | install: "prt-get install $", 300 | remove: "prt-get remove $", 301 | upgrade: "prt-get update $", 302 | search: "prt-get search $", 303 | info: "prt-get info $", 304 | update_index: "ports -u", 305 | upgrade_all: "prt-get sysup", 306 | list_installed: "prt-get listinst", 307 | }, 308 | { 309 | name: "scoop", 310 | confirm: "", 311 | install: "scoop install $", 312 | remove: "scoop uninstall $", 313 | upgrade: "scoop update $", 314 | search: "scoop search $", 315 | info: "scoop info $", 316 | update_index: "scoop update", 317 | upgrade_all: "scoop update *", 318 | list_installed: "scoop list", 319 | }, 320 | { 321 | name: "slackpkg", 322 | confirm: "", 323 | install: "slackpkg install $", 324 | remove: "slackpkg remove $", 325 | upgrade: "slackpkg upgrade $", 326 | search: "slackpkg search $", 327 | info: "slackpkg info $", 328 | update_index: "slackpkg update", 329 | upgrade_all: "slackpkg upgrade-all", 330 | list_installed: "ls -1 /var/log/packages", 331 | }, 332 | { 333 | name: "snap", 334 | confirm: "", 335 | install: "snap install --classic $", 336 | remove: "snap remove $", 337 | upgrade: "snap refresh $", 338 | search: "snap find $", 339 | info: "snap info $", 340 | update_index: "", 341 | upgrade_all: "snap refresh", 342 | list_installed: "snap list", 343 | }, 344 | { 345 | name: "urpm", 346 | confirm: "", 347 | install: "urpmi $", 348 | remove: "urpme $", 349 | upgrade: "urpmi $", 350 | search: "urpmq -y/--fuzzy $", 351 | info: "urpmq -i $", 352 | update_index: "urpmi.update -a", 353 | upgrade_all: "urpmi --auto-update", 354 | list_installed: "rpm -q/--query --all", 355 | }, 356 | { 357 | name: "winget", 358 | confirm: "", 359 | install: "winget install $", 360 | remove: "winget uninstall $", 361 | upgrade: "winget upgrade $", 362 | search: "winget search $", 363 | info: "winget show $", 364 | update_index: "", 365 | upgrade_all: "winget upgrade --all", 366 | list_installed: "winget list", 367 | }, 368 | { 369 | name: "xbps", 370 | confirm: "-y/--yes", 371 | install: "xbps-install $", 372 | remove: "xbps-remove $", 373 | upgrade: "xbps-install -u/--update $", 374 | search: "xbps-query -Rs $", 375 | info: "xbps-query -RS $", 376 | update_index: "xbps-install -S/--sync", 377 | upgrade_all: "xbps-install -u/--update", 378 | list_installed: "xbps-query -l/--list-pkgs", 379 | }, 380 | { 381 | name: "yay", 382 | confirm: "--noconfirm", 383 | install: "yay -S $", 384 | remove: "yay -R -s $", 385 | upgrade: "yay -S $", 386 | search: "yay -S -s $", 387 | info: "yay -S -i $", 388 | update_index: "yay -S -y", 389 | upgrade_all: "yay -S -y -u", 390 | list_installed: "yay -Q", 391 | }, 392 | { 393 | name: "yum", 394 | confirm: "-y/--assumeyes", 395 | install: "yum install $", 396 | remove: "yum remove $", 397 | upgrade: "yum update $", 398 | search: "yum search $", 399 | info: "yum info $", 400 | update_index: "yum check-update", 401 | upgrade_all: "yum update", 402 | list_installed: "yum list --installed", 403 | }, 404 | { 405 | name: "zypper", 406 | confirm: "-y/--no-confirm", 407 | install: "zypper install $", 408 | remove: "zypper remove $", 409 | upgrade: "zypper update $", 410 | search: "zypper search $", 411 | info: "zypper info $", 412 | update_index: "zypper refresh", 413 | upgrade_all: "zypper update", 414 | list_installed: "zypper search -i/--installed-only", 415 | }, 416 | ]; 417 | 418 | /// Represent a kind of package management tool. e.g. apt, pacman, yum... 419 | #[derive(Debug, Clone, PartialEq)] 420 | pub struct Vendor { 421 | pub(crate) name: String, 422 | pub(crate) confirm: String, 423 | pub(crate) install: Action, 424 | pub(crate) remove: Action, 425 | pub(crate) upgrade: Action, 426 | pub(crate) search: Action, 427 | pub(crate) info: Action, 428 | pub(crate) update_index: Action, 429 | pub(crate) upgrade_all: Action, 430 | pub(crate) list_installed: Action, 431 | } 432 | 433 | impl Vendor { 434 | pub fn name(&self) -> &str { 435 | &self.name 436 | } 437 | 438 | /// Parse command line, figure out the task to perform 439 | pub fn parse(&self, args: &[String], upt_tool: &str) -> Result { 440 | if self.is_help(args) { 441 | return Err(UptError::DisplayHelp(self.help(upt_tool))); 442 | } 443 | if let Some((Some(pkg), yes)) = self.install.parse(args, &self.confirm) { 444 | return Ok(Task::Install { pkg, confirm: yes }); 445 | } 446 | if let Some((Some(pkg), yes)) = self.remove.parse(args, &self.confirm) { 447 | return Ok(Task::Remove { pkg, confirm: yes }); 448 | } 449 | if let Some((Some(pkg), yes)) = self.upgrade.parse(args, &self.confirm) { 450 | return Ok(Task::Upgrade { pkg, confirm: yes }); 451 | } 452 | if let Some((Some(pkg), _)) = self.search.parse(args, "") { 453 | return Ok(Task::Search { pkg }); 454 | } 455 | if let Some((Some(pkg), _)) = self.info.parse(args, "") { 456 | return Ok(Task::Info { pkg }); 457 | } 458 | if self.update_index.parse(args, "").is_some() { 459 | return Ok(Task::UpdateIndex); 460 | } 461 | if let Some((_, yes)) = self.upgrade_all.parse(args, &self.confirm) { 462 | return Ok(Task::UpgradeAll { confirm: yes }); 463 | } 464 | if self.list_installed.parse(args, "").is_some() { 465 | return Ok(Task::ListInstalled); 466 | } 467 | Err(UptError::InvalidArgs(self.help(upt_tool))) 468 | } 469 | 470 | /// Convert the task to command line, which invokes the os's package management tool. 471 | pub fn eval(&self, task: &Task) -> Result, UptError> { 472 | let cmd = match task { 473 | Task::Install { pkg, confirm: yes } => self.install.to_cmd(pkg, self.yes_str(yes)), 474 | Task::Remove { pkg, confirm: yes } => self.remove.to_cmd(pkg, self.yes_str(yes)), 475 | Task::Upgrade { pkg, confirm: yes } => self.upgrade.to_cmd(pkg, self.yes_str(yes)), 476 | Task::Search { pkg } => self.search.to_cmd(pkg, ""), 477 | Task::Info { pkg } => self.info.to_cmd(pkg, ""), 478 | Task::UpdateIndex => self.update_index.to_cmd("", ""), 479 | Task::UpgradeAll { confirm: yes } => self.upgrade_all.to_cmd("", self.yes_str(yes)), 480 | Task::ListInstalled => self.list_installed.to_cmd("", ""), 481 | }; 482 | cmd.ok_or(UptError::InvalidTask) 483 | } 484 | 485 | fn yes_str(&self, yes: &bool) -> &str { 486 | if !*yes || self.confirm.is_empty() { 487 | return ""; 488 | } 489 | match self.confirm.split_once('/') { 490 | Some((v, _)) => v, 491 | None => self.confirm.as_str(), 492 | } 493 | } 494 | 495 | fn is_help(&self, args: &[String]) -> bool { 496 | args.len() < 2 497 | || args 498 | .iter() 499 | .skip(1) 500 | .any(|arg| ["-h", "--help"].iter().any(|option| option == arg)) 501 | } 502 | 503 | /// Dump help message 504 | fn help(&self, upt_tool: &str) -> String { 505 | let mut lines: Vec = Vec::new(); 506 | lines.push(String::from("Usage: ")); 507 | let helps = vec![ 508 | (self.install.help(), "Install packages"), 509 | (self.remove.help(), "Remove packages"), 510 | (self.upgrade.help(), "Upgrade packages"), 511 | (self.search.help(), "Search for packages"), 512 | (self.info.help(), "Show package details"), 513 | (self.update_index.help(), "Update package indexes"), 514 | (self.upgrade_all.help(), "Upgrade all packages"), 515 | (self.list_installed.help(), "List all installed packages"), 516 | ]; 517 | let helps: Vec<(&String, &str)> = helps 518 | .iter() 519 | .filter(|(v, _)| v.is_some()) 520 | .map(|(v, d)| (v.as_ref().unwrap(), *d)) 521 | .collect(); 522 | let width = helps.iter().map(|(v, _)| v.len()).max().unwrap() + 6; 523 | for (cmd, description) in &helps { 524 | lines.push(format!(" {: { 542 | assert_eq!($vendor.parse(&[ $($arg.to_string()),* ], "-").unwrap(), Task::$task { pkg: $pkg.to_string(), confirm: $confirm }) 543 | }; 544 | ($vendor:expr, [$($arg:expr),*], ($task:tt, pkg=$pkg:expr)) => { 545 | assert_eq!($vendor.parse(&[ $($arg.to_string()),* ], "-").unwrap(), Task::$task { pkg: $pkg.to_string() }) 546 | }; 547 | ($vendor:expr, [$($arg:expr),*], ($task:tt, confirm=$confirm:expr)) => { 548 | assert_eq!($vendor.parse(&[ $($arg.to_string()),* ], "-").unwrap(), Task::$task { confirm: $confirm }) 549 | }; 550 | ($vendor:expr, [$($arg:expr),*], $task:tt) => { 551 | assert_eq!($vendor.parse(&[ $($arg.to_string()),* ], "-").unwrap(), Task::$task) 552 | }; 553 | ($vendor:expr, [$($arg:expr),*]) => { 554 | assert!($vendor.parse(&[ $($arg.to_string()),* ], "-").is_err()) 555 | } 556 | } 557 | 558 | #[test] 559 | fn test_parse() { 560 | let upt = init_vendor("upt").unwrap(); 561 | check_parse!(upt, ["upt", "install", "vim"], (Install, "vim", false)); 562 | check_parse!(upt, ["upt", "install", "-y", "vim"], (Install, "vim", true)); 563 | check_parse!( 564 | upt, 565 | ["upt", "install", "--yes", "vim"], 566 | (Install, "vim", true) 567 | ); 568 | check_parse!( 569 | upt, 570 | ["upt", "remove", "--yes", "vim", "jq"], 571 | (Remove, "vim jq", true) 572 | ); 573 | check_parse!( 574 | upt, 575 | ["upt", "uninstall", "--yes", "vim", "jq"], 576 | (Remove, "vim jq", true) 577 | ); 578 | check_parse!(upt, ["upt", "upgrade", "vim"], (Upgrade, "vim", false)); 579 | check_parse!(upt, ["upt", "search", "vim"], (Search, pkg = "vim")); 580 | check_parse!( 581 | upt, 582 | ["upt", "search", "vim", "jq"], 583 | (Search, pkg = "vim jq") 584 | ); 585 | check_parse!(upt, ["upt", "info", "vim"], (Info, pkg = "vim")); 586 | check_parse!(upt, ["upt", "update"], UpdateIndex); 587 | check_parse!(upt, ["upt", "upgrade"], (UpgradeAll, confirm = false)); 588 | check_parse!(upt, ["upt", "upgrade", "-y"], (UpgradeAll, confirm = true)); 589 | check_parse!(upt, ["upt", "list"], ListInstalled); 590 | check_parse!(upt, ["upt", "install"]); 591 | check_parse!(upt, ["upt", "install", "--ye"]); 592 | check_parse!(upt, ["upt", "update", "--yes"]); 593 | } 594 | 595 | macro_rules! check_eval { 596 | ($vendor:expr, ($task:tt, $pkg:expr, $confirm:expr), $cmd:expr) => { 597 | assert_eq!( 598 | $vendor 599 | .eval(&Task::$task { 600 | pkg: $pkg.to_string(), 601 | confirm: $confirm 602 | }) 603 | .unwrap() 604 | .join(" "), 605 | $cmd.to_string() 606 | ) 607 | }; 608 | ($vendor:expr, ($task:tt, pkg=$pkg:expr), $cmd:expr) => { 609 | assert_eq!( 610 | $vendor 611 | .eval(&Task::$task { 612 | pkg: $pkg.to_string() 613 | }) 614 | .unwrap() 615 | .join(" "), 616 | $cmd.to_string() 617 | ) 618 | }; 619 | ($vendor:expr, ($task:tt, confirm=$confirm:expr), $cmd:expr) => { 620 | assert_eq!( 621 | $vendor 622 | .eval(&Task::$task { confirm: $confirm }) 623 | .unwrap() 624 | .join(" "), 625 | $cmd.to_string() 626 | ) 627 | }; 628 | ($vendor:expr, $task:tt, $cmd:expr) => { 629 | assert_eq!( 630 | $vendor.eval(&Task::$task).unwrap().join(" "), 631 | $cmd.to_string() 632 | ) 633 | }; 634 | ($vendor:expr) => { 635 | assert!($vendor.eval(&Task::$task).is_none()) 636 | }; 637 | } 638 | 639 | #[test] 640 | fn test_eval() { 641 | let upt = init_vendor("upt").unwrap(); 642 | check_eval!(upt, (Install, "vim", false), "upt install vim"); 643 | check_eval!(upt, (Install, "vim jq", true), "upt install vim jq -y"); 644 | check_eval!(upt, (Remove, "vim jq", false), "upt remove vim jq"); 645 | check_eval!(upt, (Upgrade, "vim", true), "upt upgrade vim -y"); 646 | check_eval!(upt, (Search, pkg = "vim"), "upt search vim"); 647 | check_eval!(upt, (Info, pkg = "vim"), "upt info vim"); 648 | check_eval!(upt, UpdateIndex, "upt update"); 649 | check_eval!(upt, (UpgradeAll, confirm = false), "upt upgrade"); 650 | check_eval!(upt, (UpgradeAll, confirm = true), "upt upgrade -y"); 651 | check_eval!(upt, ListInstalled, "upt list"); 652 | 653 | let pacman = init_vendor("pacman").unwrap(); 654 | check_eval!(pacman, (Install, "vim", false), "pacman -S vim"); 655 | check_eval!( 656 | pacman, 657 | (Install, "vim jq", true), 658 | "pacman -S vim jq --noconfirm" 659 | ); 660 | check_eval!(pacman, (Remove, "vim jq", false), "pacman -R -s vim jq"); 661 | check_eval!(pacman, (Upgrade, "vim", true), "pacman -S vim --noconfirm"); 662 | check_eval!(pacman, (Search, pkg = "vim"), "pacman -S -s vim"); 663 | check_eval!(pacman, (Info, pkg = "vim"), "pacman -S -i vim"); 664 | check_eval!(pacman, UpdateIndex, "pacman -S -y"); 665 | check_eval!(pacman, (UpgradeAll, confirm = false), "pacman -S -y -u"); 666 | check_eval!( 667 | pacman, 668 | (UpgradeAll, confirm = true), 669 | "pacman -S -y -u --noconfirm" 670 | ); 671 | check_eval!(pacman, ListInstalled, "pacman -Q"); 672 | } 673 | 674 | #[test] 675 | fn test_vendors() { 676 | for tool in support_tools() { 677 | init_vendor(tool).unwrap(); 678 | } 679 | } 680 | } 681 | --------------------------------------------------------------------------------