├── .envrc ├── .github └── workflows │ ├── build.yml │ └── nix.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── alpacka-cli ├── Cargo.toml ├── README.md └── src │ ├── cli │ ├── clap.rs │ ├── install.rs │ ├── list_generations.rs │ └── mod.rs │ └── main.rs ├── contrib └── packages.json ├── flake.lock ├── flake.nix └── src ├── config.rs ├── lib.rs ├── manifest.rs ├── package.rs └── smith ├── enums.rs ├── git.rs └── mod.rs /.envrc: -------------------------------------------------------------------------------- 1 | use flake -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # built against standard cargo 2 | name: "cargo build and publish" 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | push: 7 | branches: 8 | - 'main' 9 | - 'ci*' # Allow testing CI fixes without opening a PR 10 | 11 | jobs: 12 | linux: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: install rust 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: stable 20 | profile: minimal 21 | override: true 22 | - uses: Swatinem/rust-cache@v1 23 | - name: build alpacka 24 | uses: actions-rs/cargo@v1 25 | with: 26 | command: build 27 | args: --release -p alpacka-cli 28 | - name: upload alpacka binary 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: "alpacka-linux-x86_64" 32 | path: "target/release/alpacka" 33 | if-no-files-found: error 34 | retention-days: 7 35 | 36 | macos: 37 | runs-on: macos-latest 38 | steps: 39 | - name: Install dependencies 40 | run: brew install openssl 41 | - uses: actions/checkout@v2 42 | - name: install rust 43 | uses: actions-rs/toolchain@v1 44 | with: 45 | toolchain: stable 46 | profile: minimal 47 | override: true 48 | - uses: Swatinem/rust-cache@v1 49 | - name: build apacka 50 | uses: actions-rs/cargo@v1 51 | with: 52 | command: build 53 | args: --release -p alpacka-cli 54 | - name: upload alpacka x86 mac binary 55 | uses: actions/upload-artifact@v2 56 | with: 57 | name: "alpacka-macos-x86_64" 58 | path: "target/release/alpacka" 59 | if-no-files-found: error 60 | retention-days: 7 61 | 62 | macos-m1: 63 | runs-on: macos-latest 64 | steps: 65 | - name: Install dependencies 66 | run: brew install openssl git 67 | - uses: actions/checkout@v2 68 | - name: set SDKROOT 69 | run: | 70 | echo "SDKROOT=$(xcrun --sdk macosx --show-sdk-path)" >> $GITHUB_ENV 71 | - uses: goto-bus-stop/setup-zig@v2 72 | - name: install rust 73 | uses: actions-rs/toolchain@v1 74 | with: 75 | toolchain: stable 76 | target: aarch64-apple-darwin 77 | profile: minimal 78 | override: true 79 | - uses: Swatinem/rust-cache@v1 80 | - name: install zigbuild 81 | uses: actions-rs/cargo@v1 82 | with: 83 | command: install 84 | args: cargo-zigbuild 85 | - name: build alpacka 86 | uses: actions-rs/cargo@v1 87 | with: 88 | command: zigbuild 89 | args: --release --features vendor --target aarch64-apple-darwin -p alpacka-cli 90 | env: 91 | SDKROOT: ${{ env.SDKROOT }} 92 | - name: upload alpacka aarch64 binary 93 | uses: actions/upload-artifact@v2 94 | with: 95 | name: "alpacka-macos-aarch64" 96 | path: "target/aarch64-apple-darwin/release/alpacka" 97 | if-no-files-found: error 98 | retention-days: 7 99 | -------------------------------------------------------------------------------- /.github/workflows/nix.yml: -------------------------------------------------------------------------------- 1 | name: "nix test suite" 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened] 5 | push: 6 | branches: 7 | - 'main' 8 | - 'ci*' # Allow testing CI fixes without opening a PR 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | tests: 15 | strategy: 16 | matrix: 17 | include: 18 | # Latest and greatest release of Nix 19 | - name: nixpkgs (latest) 20 | install_url: https://nixos.org/nix/install 21 | # The 22.11 branch ships with Nix 2.11.1 22 | - name: nixpkgs (22.11) 23 | install_url: https://releases.nixos.org/nix/nix-2.11.1/install 24 | nixpkgs-override: "--override-input nixpkgs github:NixOS/nixpkgs/release-22.11" 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: cachix/install-nix-action@v21 29 | with: 30 | install_url: ${{ matrix.install_url }} 31 | extra_nix_config: | 32 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 33 | - uses: cachix/cachix-action@v12 34 | with: 35 | name: nyoom-engineering 36 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 37 | - name: flake checks 38 | run: nix flake check --keep-going --print-build-logs ${{ matrix.nixpkgs-override }} --no-sandbox 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | # Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # These are files generated by nix 13 | /result/ 14 | result 15 | /.direnv 16 | 17 | # these are files generated by alpacka testing 18 | /pack 19 | *.bin 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug unit tests in library 'alpacka'", 11 | "cargo": { 12 | "args": [ 13 | "test", 14 | "--no-run", 15 | "--lib", 16 | "--package=alpacka" 17 | ], 18 | "filter": { 19 | "name": "alpacka", 20 | "kind": "lib" 21 | } 22 | }, 23 | "args": [], 24 | "cwd": "${workspaceFolder}" 25 | }, 26 | { 27 | "type": "lldb", 28 | "request": "launch", 29 | "name": "Debug executable 'alpacka'", 30 | "cargo": { 31 | "args": [ 32 | "build", 33 | "--bin=alpacka", 34 | "--package=alpacka" 35 | ], 36 | "filter": { 37 | "name": "alpacka", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | }, 44 | { 45 | "type": "lldb", 46 | "request": "launch", 47 | "name": "Debug unit tests in executable 'alpacka'", 48 | "cargo": { 49 | "args": [ 50 | "test", 51 | "--no-run", 52 | "--bin=alpacka", 53 | "--package=alpacka" 54 | ], 55 | "filter": { 56 | "name": "alpacka", 57 | "kind": "bin" 58 | } 59 | }, 60 | "args": [], 61 | "cwd": "${workspaceFolder}" 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Alpacka", 4 | "hasher", 5 | "libgit", 6 | "Neovim", 7 | "nvim", 8 | "rkyv", 9 | "typetag" 10 | ], 11 | "cSpell.enableFiletypes": ["nix"], 12 | "cSpell.language": "en,en-GB,lorem" 13 | } 14 | -------------------------------------------------------------------------------- /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 = "ahash" 7 | version = "0.7.6" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 10 | dependencies = [ 11 | "getrandom", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "alpacka" 18 | version = "0.1.0" 19 | dependencies = [ 20 | "bytecheck", 21 | "error-stack", 22 | "git2", 23 | "ptr_meta", 24 | "rayon", 25 | "rkyv", 26 | "serde", 27 | "serde_json", 28 | "tempfile", 29 | "tracing", 30 | ] 31 | 32 | [[package]] 33 | name = "alpacka-cli" 34 | version = "0.1.0" 35 | dependencies = [ 36 | "alpacka", 37 | "bytecheck", 38 | "clap", 39 | "dirs-sys", 40 | "error-stack", 41 | "openssl-probe", 42 | "ptr_meta", 43 | "rayon", 44 | "rkyv", 45 | "serde", 46 | "serde_json", 47 | "tracing", 48 | "tracing-error", 49 | "tracing-subscriber", 50 | ] 51 | 52 | [[package]] 53 | name = "anstream" 54 | version = "0.3.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" 57 | dependencies = [ 58 | "anstyle", 59 | "anstyle-parse", 60 | "anstyle-query", 61 | "anstyle-wincon", 62 | "colorchoice", 63 | "is-terminal", 64 | "utf8parse", 65 | ] 66 | 67 | [[package]] 68 | name = "anstyle" 69 | version = "1.0.0" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" 72 | 73 | [[package]] 74 | name = "anstyle-parse" 75 | version = "0.2.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 78 | dependencies = [ 79 | "utf8parse", 80 | ] 81 | 82 | [[package]] 83 | name = "anstyle-query" 84 | version = "1.0.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 87 | dependencies = [ 88 | "windows-sys", 89 | ] 90 | 91 | [[package]] 92 | name = "anstyle-wincon" 93 | version = "1.0.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" 96 | dependencies = [ 97 | "anstyle", 98 | "windows-sys", 99 | ] 100 | 101 | [[package]] 102 | name = "anyhow" 103 | version = "1.0.71" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" 106 | 107 | [[package]] 108 | name = "autocfg" 109 | version = "1.1.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 112 | 113 | [[package]] 114 | name = "bitflags" 115 | version = "1.3.2" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 118 | 119 | [[package]] 120 | name = "bitvec" 121 | version = "1.0.1" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 124 | dependencies = [ 125 | "funty", 126 | "radium", 127 | "tap", 128 | "wyz", 129 | ] 130 | 131 | [[package]] 132 | name = "bytecheck" 133 | version = "0.6.11" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" 136 | dependencies = [ 137 | "bytecheck_derive", 138 | "ptr_meta", 139 | "simdutf8", 140 | ] 141 | 142 | [[package]] 143 | name = "bytecheck_derive" 144 | version = "0.6.11" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" 147 | dependencies = [ 148 | "proc-macro2", 149 | "quote", 150 | "syn 1.0.109", 151 | ] 152 | 153 | [[package]] 154 | name = "cc" 155 | version = "1.0.79" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 158 | dependencies = [ 159 | "jobserver", 160 | ] 161 | 162 | [[package]] 163 | name = "cfg-if" 164 | version = "1.0.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 167 | 168 | [[package]] 169 | name = "clap" 170 | version = "4.3.4" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed" 173 | dependencies = [ 174 | "clap_builder", 175 | "clap_derive", 176 | "once_cell", 177 | ] 178 | 179 | [[package]] 180 | name = "clap_builder" 181 | version = "4.3.4" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636" 184 | dependencies = [ 185 | "anstream", 186 | "anstyle", 187 | "bitflags", 188 | "clap_lex", 189 | "strsim", 190 | ] 191 | 192 | [[package]] 193 | name = "clap_derive" 194 | version = "4.3.2" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" 197 | dependencies = [ 198 | "heck", 199 | "proc-macro2", 200 | "quote", 201 | "syn 2.0.18", 202 | ] 203 | 204 | [[package]] 205 | name = "clap_lex" 206 | version = "0.5.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" 209 | 210 | [[package]] 211 | name = "colorchoice" 212 | version = "1.0.0" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 215 | 216 | [[package]] 217 | name = "crossbeam-channel" 218 | version = "0.5.8" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" 221 | dependencies = [ 222 | "cfg-if", 223 | "crossbeam-utils", 224 | ] 225 | 226 | [[package]] 227 | name = "crossbeam-deque" 228 | version = "0.8.3" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" 231 | dependencies = [ 232 | "cfg-if", 233 | "crossbeam-epoch", 234 | "crossbeam-utils", 235 | ] 236 | 237 | [[package]] 238 | name = "crossbeam-epoch" 239 | version = "0.9.15" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" 242 | dependencies = [ 243 | "autocfg", 244 | "cfg-if", 245 | "crossbeam-utils", 246 | "memoffset", 247 | "scopeguard", 248 | ] 249 | 250 | [[package]] 251 | name = "crossbeam-utils" 252 | version = "0.8.16" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 255 | dependencies = [ 256 | "cfg-if", 257 | ] 258 | 259 | [[package]] 260 | name = "dirs-sys" 261 | version = "0.4.1" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 264 | dependencies = [ 265 | "libc", 266 | "option-ext", 267 | "redox_users", 268 | "windows-sys", 269 | ] 270 | 271 | [[package]] 272 | name = "either" 273 | version = "1.8.1" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 276 | 277 | [[package]] 278 | name = "errno" 279 | version = "0.3.1" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 282 | dependencies = [ 283 | "errno-dragonfly", 284 | "libc", 285 | "windows-sys", 286 | ] 287 | 288 | [[package]] 289 | name = "errno-dragonfly" 290 | version = "0.1.2" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 293 | dependencies = [ 294 | "cc", 295 | "libc", 296 | ] 297 | 298 | [[package]] 299 | name = "error-stack" 300 | version = "0.3.1" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "5f00447f331c7f726db5b8532ebc9163519eed03c6d7c8b73c90b3ff5646ac85" 303 | dependencies = [ 304 | "anyhow", 305 | "rustc_version", 306 | "tracing-error", 307 | ] 308 | 309 | [[package]] 310 | name = "fastrand" 311 | version = "1.9.0" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 314 | dependencies = [ 315 | "instant", 316 | ] 317 | 318 | [[package]] 319 | name = "form_urlencoded" 320 | version = "1.2.0" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" 323 | dependencies = [ 324 | "percent-encoding", 325 | ] 326 | 327 | [[package]] 328 | name = "funty" 329 | version = "2.0.0" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 332 | 333 | [[package]] 334 | name = "getrandom" 335 | version = "0.2.10" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 338 | dependencies = [ 339 | "cfg-if", 340 | "libc", 341 | "wasi", 342 | ] 343 | 344 | [[package]] 345 | name = "git2" 346 | version = "0.17.2" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "7b989d6a7ca95a362cf2cfc5ad688b3a467be1f87e480b8dad07fee8c79b0044" 349 | dependencies = [ 350 | "bitflags", 351 | "libc", 352 | "libgit2-sys", 353 | "log", 354 | "openssl-probe", 355 | "openssl-sys", 356 | "url", 357 | ] 358 | 359 | [[package]] 360 | name = "hashbrown" 361 | version = "0.12.3" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 364 | dependencies = [ 365 | "ahash", 366 | ] 367 | 368 | [[package]] 369 | name = "heck" 370 | version = "0.4.1" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 373 | 374 | [[package]] 375 | name = "hermit-abi" 376 | version = "0.2.6" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 379 | dependencies = [ 380 | "libc", 381 | ] 382 | 383 | [[package]] 384 | name = "hermit-abi" 385 | version = "0.3.1" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 388 | 389 | [[package]] 390 | name = "idna" 391 | version = "0.4.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" 394 | dependencies = [ 395 | "unicode-bidi", 396 | "unicode-normalization", 397 | ] 398 | 399 | [[package]] 400 | name = "instant" 401 | version = "0.1.12" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 404 | dependencies = [ 405 | "cfg-if", 406 | ] 407 | 408 | [[package]] 409 | name = "io-lifetimes" 410 | version = "1.0.11" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" 413 | dependencies = [ 414 | "hermit-abi 0.3.1", 415 | "libc", 416 | "windows-sys", 417 | ] 418 | 419 | [[package]] 420 | name = "is-terminal" 421 | version = "0.4.7" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" 424 | dependencies = [ 425 | "hermit-abi 0.3.1", 426 | "io-lifetimes", 427 | "rustix", 428 | "windows-sys", 429 | ] 430 | 431 | [[package]] 432 | name = "itoa" 433 | version = "1.0.6" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 436 | 437 | [[package]] 438 | name = "jobserver" 439 | version = "0.1.26" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" 442 | dependencies = [ 443 | "libc", 444 | ] 445 | 446 | [[package]] 447 | name = "lazy_static" 448 | version = "1.4.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 451 | 452 | [[package]] 453 | name = "libc" 454 | version = "0.2.146" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" 457 | 458 | [[package]] 459 | name = "libgit2-sys" 460 | version = "0.15.2+1.6.4" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "a80df2e11fb4a61f4ba2ab42dbe7f74468da143f1a75c74e11dee7c813f694fa" 463 | dependencies = [ 464 | "cc", 465 | "libc", 466 | "libssh2-sys", 467 | "libz-sys", 468 | "openssl-sys", 469 | "pkg-config", 470 | ] 471 | 472 | [[package]] 473 | name = "libssh2-sys" 474 | version = "0.3.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" 477 | dependencies = [ 478 | "cc", 479 | "libc", 480 | "libz-sys", 481 | "openssl-sys", 482 | "pkg-config", 483 | "vcpkg", 484 | ] 485 | 486 | [[package]] 487 | name = "libz-sys" 488 | version = "1.1.9" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" 491 | dependencies = [ 492 | "cc", 493 | "libc", 494 | "pkg-config", 495 | "vcpkg", 496 | ] 497 | 498 | [[package]] 499 | name = "linux-raw-sys" 500 | version = "0.3.8" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" 503 | 504 | [[package]] 505 | name = "log" 506 | version = "0.4.19" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" 509 | 510 | [[package]] 511 | name = "matchers" 512 | version = "0.1.0" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 515 | dependencies = [ 516 | "regex-automata", 517 | ] 518 | 519 | [[package]] 520 | name = "memoffset" 521 | version = "0.9.0" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" 524 | dependencies = [ 525 | "autocfg", 526 | ] 527 | 528 | [[package]] 529 | name = "nu-ansi-term" 530 | version = "0.46.0" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 533 | dependencies = [ 534 | "overload", 535 | "winapi", 536 | ] 537 | 538 | [[package]] 539 | name = "num_cpus" 540 | version = "1.15.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 543 | dependencies = [ 544 | "hermit-abi 0.2.6", 545 | "libc", 546 | ] 547 | 548 | [[package]] 549 | name = "once_cell" 550 | version = "1.18.0" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 553 | 554 | [[package]] 555 | name = "openssl-probe" 556 | version = "0.1.5" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 559 | 560 | [[package]] 561 | name = "openssl-src" 562 | version = "111.26.0+1.1.1u" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37" 565 | dependencies = [ 566 | "cc", 567 | ] 568 | 569 | [[package]] 570 | name = "openssl-sys" 571 | version = "0.9.88" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617" 574 | dependencies = [ 575 | "cc", 576 | "libc", 577 | "openssl-src", 578 | "pkg-config", 579 | "vcpkg", 580 | ] 581 | 582 | [[package]] 583 | name = "option-ext" 584 | version = "0.2.0" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 587 | 588 | [[package]] 589 | name = "overload" 590 | version = "0.1.1" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 593 | 594 | [[package]] 595 | name = "percent-encoding" 596 | version = "2.3.0" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" 599 | 600 | [[package]] 601 | name = "pin-project-lite" 602 | version = "0.2.9" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 605 | 606 | [[package]] 607 | name = "pkg-config" 608 | version = "0.3.27" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 611 | 612 | [[package]] 613 | name = "proc-macro2" 614 | version = "1.0.60" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" 617 | dependencies = [ 618 | "unicode-ident", 619 | ] 620 | 621 | [[package]] 622 | name = "ptr_meta" 623 | version = "0.1.4" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" 626 | dependencies = [ 627 | "ptr_meta_derive", 628 | ] 629 | 630 | [[package]] 631 | name = "ptr_meta_derive" 632 | version = "0.1.4" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" 635 | dependencies = [ 636 | "proc-macro2", 637 | "quote", 638 | "syn 1.0.109", 639 | ] 640 | 641 | [[package]] 642 | name = "quote" 643 | version = "1.0.28" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" 646 | dependencies = [ 647 | "proc-macro2", 648 | ] 649 | 650 | [[package]] 651 | name = "radium" 652 | version = "0.7.0" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 655 | 656 | [[package]] 657 | name = "rayon" 658 | version = "1.7.0" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" 661 | dependencies = [ 662 | "either", 663 | "rayon-core", 664 | ] 665 | 666 | [[package]] 667 | name = "rayon-core" 668 | version = "1.11.0" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" 671 | dependencies = [ 672 | "crossbeam-channel", 673 | "crossbeam-deque", 674 | "crossbeam-utils", 675 | "num_cpus", 676 | ] 677 | 678 | [[package]] 679 | name = "redox_syscall" 680 | version = "0.2.16" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 683 | dependencies = [ 684 | "bitflags", 685 | ] 686 | 687 | [[package]] 688 | name = "redox_syscall" 689 | version = "0.3.5" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 692 | dependencies = [ 693 | "bitflags", 694 | ] 695 | 696 | [[package]] 697 | name = "redox_users" 698 | version = "0.4.3" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 701 | dependencies = [ 702 | "getrandom", 703 | "redox_syscall 0.2.16", 704 | "thiserror", 705 | ] 706 | 707 | [[package]] 708 | name = "regex" 709 | version = "1.8.4" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" 712 | dependencies = [ 713 | "regex-syntax 0.7.2", 714 | ] 715 | 716 | [[package]] 717 | name = "regex-automata" 718 | version = "0.1.10" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 721 | dependencies = [ 722 | "regex-syntax 0.6.29", 723 | ] 724 | 725 | [[package]] 726 | name = "regex-syntax" 727 | version = "0.6.29" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 730 | 731 | [[package]] 732 | name = "regex-syntax" 733 | version = "0.7.2" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" 736 | 737 | [[package]] 738 | name = "rend" 739 | version = "0.4.0" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "581008d2099240d37fb08d77ad713bcaec2c4d89d50b5b21a8bb1996bbab68ab" 742 | dependencies = [ 743 | "bytecheck", 744 | ] 745 | 746 | [[package]] 747 | name = "rkyv" 748 | version = "0.7.42" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" 751 | dependencies = [ 752 | "bitvec", 753 | "bytecheck", 754 | "hashbrown", 755 | "ptr_meta", 756 | "rend", 757 | "rkyv_derive", 758 | "seahash", 759 | "tinyvec", 760 | "uuid", 761 | ] 762 | 763 | [[package]] 764 | name = "rkyv_derive" 765 | version = "0.7.42" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" 768 | dependencies = [ 769 | "proc-macro2", 770 | "quote", 771 | "syn 1.0.109", 772 | ] 773 | 774 | [[package]] 775 | name = "rustc_version" 776 | version = "0.4.0" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 779 | dependencies = [ 780 | "semver", 781 | ] 782 | 783 | [[package]] 784 | name = "rustix" 785 | version = "0.37.20" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" 788 | dependencies = [ 789 | "bitflags", 790 | "errno", 791 | "io-lifetimes", 792 | "libc", 793 | "linux-raw-sys", 794 | "windows-sys", 795 | ] 796 | 797 | [[package]] 798 | name = "ryu" 799 | version = "1.0.13" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 802 | 803 | [[package]] 804 | name = "scopeguard" 805 | version = "1.1.0" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 808 | 809 | [[package]] 810 | name = "seahash" 811 | version = "4.1.0" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" 814 | 815 | [[package]] 816 | name = "semver" 817 | version = "1.0.17" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" 820 | 821 | [[package]] 822 | name = "serde" 823 | version = "1.0.164" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" 826 | dependencies = [ 827 | "serde_derive", 828 | ] 829 | 830 | [[package]] 831 | name = "serde_derive" 832 | version = "1.0.164" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" 835 | dependencies = [ 836 | "proc-macro2", 837 | "quote", 838 | "syn 2.0.18", 839 | ] 840 | 841 | [[package]] 842 | name = "serde_json" 843 | version = "1.0.97" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a" 846 | dependencies = [ 847 | "itoa", 848 | "ryu", 849 | "serde", 850 | ] 851 | 852 | [[package]] 853 | name = "sharded-slab" 854 | version = "0.1.4" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 857 | dependencies = [ 858 | "lazy_static", 859 | ] 860 | 861 | [[package]] 862 | name = "simdutf8" 863 | version = "0.1.4" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" 866 | 867 | [[package]] 868 | name = "smallvec" 869 | version = "1.10.0" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 872 | 873 | [[package]] 874 | name = "strsim" 875 | version = "0.10.0" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 878 | 879 | [[package]] 880 | name = "syn" 881 | version = "1.0.109" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 884 | dependencies = [ 885 | "proc-macro2", 886 | "quote", 887 | "unicode-ident", 888 | ] 889 | 890 | [[package]] 891 | name = "syn" 892 | version = "2.0.18" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" 895 | dependencies = [ 896 | "proc-macro2", 897 | "quote", 898 | "unicode-ident", 899 | ] 900 | 901 | [[package]] 902 | name = "tap" 903 | version = "1.0.1" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 906 | 907 | [[package]] 908 | name = "tempfile" 909 | version = "3.6.0" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" 912 | dependencies = [ 913 | "autocfg", 914 | "cfg-if", 915 | "fastrand", 916 | "redox_syscall 0.3.5", 917 | "rustix", 918 | "windows-sys", 919 | ] 920 | 921 | [[package]] 922 | name = "thiserror" 923 | version = "1.0.40" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" 926 | dependencies = [ 927 | "thiserror-impl", 928 | ] 929 | 930 | [[package]] 931 | name = "thiserror-impl" 932 | version = "1.0.40" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" 935 | dependencies = [ 936 | "proc-macro2", 937 | "quote", 938 | "syn 2.0.18", 939 | ] 940 | 941 | [[package]] 942 | name = "thread_local" 943 | version = "1.1.7" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 946 | dependencies = [ 947 | "cfg-if", 948 | "once_cell", 949 | ] 950 | 951 | [[package]] 952 | name = "tinyvec" 953 | version = "1.6.0" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 956 | dependencies = [ 957 | "tinyvec_macros", 958 | ] 959 | 960 | [[package]] 961 | name = "tinyvec_macros" 962 | version = "0.1.1" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 965 | 966 | [[package]] 967 | name = "tracing" 968 | version = "0.1.37" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 971 | dependencies = [ 972 | "cfg-if", 973 | "pin-project-lite", 974 | "tracing-attributes", 975 | "tracing-core", 976 | ] 977 | 978 | [[package]] 979 | name = "tracing-attributes" 980 | version = "0.1.24" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" 983 | dependencies = [ 984 | "proc-macro2", 985 | "quote", 986 | "syn 2.0.18", 987 | ] 988 | 989 | [[package]] 990 | name = "tracing-core" 991 | version = "0.1.31" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" 994 | dependencies = [ 995 | "once_cell", 996 | "valuable", 997 | ] 998 | 999 | [[package]] 1000 | name = "tracing-error" 1001 | version = "0.2.0" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" 1004 | dependencies = [ 1005 | "tracing", 1006 | "tracing-subscriber", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "tracing-log" 1011 | version = "0.1.3" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 1014 | dependencies = [ 1015 | "lazy_static", 1016 | "log", 1017 | "tracing-core", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "tracing-subscriber" 1022 | version = "0.3.17" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" 1025 | dependencies = [ 1026 | "matchers", 1027 | "nu-ansi-term", 1028 | "once_cell", 1029 | "regex", 1030 | "sharded-slab", 1031 | "smallvec", 1032 | "thread_local", 1033 | "tracing", 1034 | "tracing-core", 1035 | "tracing-log", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "unicode-bidi" 1040 | version = "0.3.13" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 1043 | 1044 | [[package]] 1045 | name = "unicode-ident" 1046 | version = "1.0.9" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" 1049 | 1050 | [[package]] 1051 | name = "unicode-normalization" 1052 | version = "0.1.22" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1055 | dependencies = [ 1056 | "tinyvec", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "url" 1061 | version = "2.4.0" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" 1064 | dependencies = [ 1065 | "form_urlencoded", 1066 | "idna", 1067 | "percent-encoding", 1068 | ] 1069 | 1070 | [[package]] 1071 | name = "utf8parse" 1072 | version = "0.2.1" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1075 | 1076 | [[package]] 1077 | name = "uuid" 1078 | version = "1.3.4" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" 1081 | 1082 | [[package]] 1083 | name = "valuable" 1084 | version = "0.1.0" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1087 | 1088 | [[package]] 1089 | name = "vcpkg" 1090 | version = "0.2.15" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1093 | 1094 | [[package]] 1095 | name = "version_check" 1096 | version = "0.9.4" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1099 | 1100 | [[package]] 1101 | name = "wasi" 1102 | version = "0.11.0+wasi-snapshot-preview1" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1105 | 1106 | [[package]] 1107 | name = "winapi" 1108 | version = "0.3.9" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1111 | dependencies = [ 1112 | "winapi-i686-pc-windows-gnu", 1113 | "winapi-x86_64-pc-windows-gnu", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "winapi-i686-pc-windows-gnu" 1118 | version = "0.4.0" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1121 | 1122 | [[package]] 1123 | name = "winapi-x86_64-pc-windows-gnu" 1124 | version = "0.4.0" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1127 | 1128 | [[package]] 1129 | name = "windows-sys" 1130 | version = "0.48.0" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1133 | dependencies = [ 1134 | "windows-targets", 1135 | ] 1136 | 1137 | [[package]] 1138 | name = "windows-targets" 1139 | version = "0.48.0" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 1142 | dependencies = [ 1143 | "windows_aarch64_gnullvm", 1144 | "windows_aarch64_msvc", 1145 | "windows_i686_gnu", 1146 | "windows_i686_msvc", 1147 | "windows_x86_64_gnu", 1148 | "windows_x86_64_gnullvm", 1149 | "windows_x86_64_msvc", 1150 | ] 1151 | 1152 | [[package]] 1153 | name = "windows_aarch64_gnullvm" 1154 | version = "0.48.0" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 1157 | 1158 | [[package]] 1159 | name = "windows_aarch64_msvc" 1160 | version = "0.48.0" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 1163 | 1164 | [[package]] 1165 | name = "windows_i686_gnu" 1166 | version = "0.48.0" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 1169 | 1170 | [[package]] 1171 | name = "windows_i686_msvc" 1172 | version = "0.48.0" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 1175 | 1176 | [[package]] 1177 | name = "windows_x86_64_gnu" 1178 | version = "0.48.0" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 1181 | 1182 | [[package]] 1183 | name = "windows_x86_64_gnullvm" 1184 | version = "0.48.0" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 1187 | 1188 | [[package]] 1189 | name = "windows_x86_64_msvc" 1190 | version = "0.48.0" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 1193 | 1194 | [[package]] 1195 | name = "wyz" 1196 | version = "0.5.1" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" 1199 | dependencies = [ 1200 | "tap", 1201 | ] 1202 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "alpacka-cli" 4 | ] 5 | 6 | [workspace.dependencies.tracing] 7 | version = "0.1" 8 | features = [] 9 | optional = false 10 | 11 | 12 | [workspace.dependencies.error-stack] 13 | version = "0.3.1" 14 | features = ["spantrace"] 15 | 16 | [workspace.dependencies.rkyv] 17 | version = "0.7.41" 18 | features = ["validation", "strict"] 19 | 20 | [workspace.dependencies.serde] 21 | version = "1.0" 22 | features = ["derive"] 23 | optional = false 24 | 25 | [workspace.dependencies] 26 | tracing-error = "0.2.0" 27 | rayon = "1.6.1" 28 | serde_json = "1.0.91" 29 | ptr_meta = "0.1.4" 30 | bytecheck = "0.6.11" 31 | 32 | [package] 33 | name = "alpacka" 34 | version = "0.1.0" 35 | description = "The next-generation package manager for neovim." 36 | repository = "https://github.com/nyoom-engineering/alpacka" 37 | readme = "README.md" 38 | authors = ["Shaurya Singh ", "Suyashtnt "] 39 | license = "MIT" 40 | keywords = ["vim", "neovim", "package", "plugin", "manager"] 41 | categories = ["text-editors"] 42 | edition = "2021" 43 | 44 | [dependencies] 45 | git2 = { version = "0.17.2" } 46 | tempfile = "3.3.0" 47 | tracing = { workspace = true } 48 | error-stack = { workspace = true} 49 | rkyv = { workspace = true } 50 | bytecheck = { workspace = true } 51 | ptr_meta = { workspace = true } 52 | rayon = { workspace = true } 53 | serde = { workspace = true } 54 | serde_json = { workspace = true } 55 | 56 | [features] 57 | vendor = ["git2/vendored-openssl", "git2/vendored-libgit2"] 58 | 59 | [profile.release] 60 | opt-level = 3 61 | strip = true 62 | lto = true 63 | codegen-units = 1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nyoom Engineering 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 | # Alpacka 2 | 3 | ## Rust-powered Neovim package manager 4 | 5 | ## Features 6 | 7 | - [x] Blazingly fast Installs. Uses libgit2 directly rather than the git CLI. 8 | - [x] Runs in parallel. Uses rayon to run installs in parallel. 9 | - [x] Lockfile interface. "packages.json" contain all packages to be installed, with frontends being able to generate them. (No frontends yet) 10 | - [x] Cache old versions of lockfiles into a file. This allows for fast rollbacks, as we just look at the previous lockfile's output. 11 | - [x] Extremely fast rollbacks. Usually < 1 second as no resolvers are run. 12 | - [x] CLI to install and inspect packages. 13 | 14 | TODO 15 | 16 | - [ ] Local packages 17 | - [ ] Frontends (Neovim frontend, CLI frontend, etc) 18 | - [ ] Patches 19 | - [ ] Uninstalling packages (Currently they are just deleted from the lockfile, but not from the filesystem) 20 | - [ ] Luarocks support 21 | - [ ] Lazy loading though Neovim frontend 22 | - [ ] Installing/managing neovim versions through the CLI frontend 23 | - [ ] Updating packages through the CLI frontend, incrementing the generation in the lockfile and installing e.g new commit of a branch 24 | - [ ] Conventional commits; Detect breaking changes in installed packages and warn the user. 25 | - [ ] [Nvim pack spec](https://github.com/nvim-lua/nvim-package-specification) support 26 | -------------------------------------------------------------------------------- /alpacka-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "alpacka-cli" 3 | description = "CLI for Alpacka" 4 | license = "MIT" 5 | repository = "https://github.com/nyoom-engineering/alpacka" 6 | readme = "README.md" 7 | keywords = ["alpacka", "cli", "neovim"] 8 | categories = ["command-line-utilities"] 9 | version = "0.1.0" 10 | edition = "2021" 11 | 12 | [[bin]] 13 | name = "alpacka" 14 | path = "src/main.rs" 15 | 16 | [dependencies] 17 | dirs-sys = "0.4.0" 18 | openssl-probe = { version = "0.1.5", optional = true } 19 | alpacka = { path = "../" } 20 | 21 | tracing = { workspace = true } 22 | tracing-error = { workspace = true } 23 | error-stack = { workspace = true } 24 | rkyv = { workspace = true } 25 | bytecheck = { workspace = true } 26 | ptr_meta = { workspace = true } 27 | rayon = { workspace = true } 28 | serde = { workspace = true } 29 | serde_json = { workspace = true } 30 | 31 | [dependencies.clap] 32 | version = "4.1.4" 33 | features = ["derive"] 34 | 35 | [dependencies.tracing-subscriber] 36 | version = "0.3.17" 37 | optional = false 38 | features = ["env-filter"] 39 | 40 | [features] 41 | vendor = ["alpacka/vendor", "dep:openssl-probe"] -------------------------------------------------------------------------------- /alpacka-cli/README.md: -------------------------------------------------------------------------------- 1 | # Alpacka 2 | 3 | ## Rust-powered Neovim package manager 4 | 5 | ### Note: This README is only for the CLI. For a list of alpacka features, see [Alpacka's git repo](https://github.com/nyoom-engineering/alpacka) 6 | 7 | ## Features 8 | 9 | - [x] Install/Uninstall packages from a file 10 | - [x] See all created generations 11 | 12 | TODO 13 | 14 | - [ ] [Nvim pack spec](https://github.com/nvim-lua/nvim-package-specification) support 15 | -------------------------------------------------------------------------------- /alpacka-cli/src/cli/clap.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{Parser, ValueEnum}; 4 | 5 | /// Alpacka: the next-generation package manager for Neovim. 6 | #[derive(Parser, Debug)] 7 | #[command(author, version, about, long_about = None)] 8 | pub enum Cli { 9 | Install { 10 | /// The path to the config file 11 | /// Defaults to `$XDG_CONFIG_HOME/nvim/packages.json` 12 | #[arg(short, long)] 13 | path: Option, 14 | /// The data directory 15 | /// Defaults to `$XDG_DATA_HOME/nvim/site/pack/alpacka` 16 | #[arg(short, long)] 17 | data_dir: Option, 18 | }, 19 | ListGenerations { 20 | /// The data directory containing the generations.rkyv file 21 | /// Defaults to `$XDG_DATA_HOME/nvim/site/pack/alpacka` 22 | #[arg(short, long)] 23 | data_dir: Option, 24 | /// The output format 25 | /// Defaults to `ListGenerationsFormatMethod::Human` 26 | #[arg(short, long)] 27 | format_style: Option, 28 | }, 29 | } 30 | 31 | #[derive(Debug, ValueEnum, Clone)] 32 | pub enum ListGenerationsFormatMethod { 33 | /// Human-readable output 34 | Human, 35 | /// JSON output, to be parsed by another program 36 | Json, 37 | } 38 | -------------------------------------------------------------------------------- /alpacka-cli/src/cli/install.rs: -------------------------------------------------------------------------------- 1 | use alpacka::{ 2 | config::Config, 3 | manifest::{ 4 | add_to_generations, get_latest, ArchivedGenerationsFile, GenerationsFile, Manifest, Plugin, 5 | }, 6 | package::{Config as PackageConfig, Package, WithSmith}, 7 | smith::{enums::Loaders, Git}, 8 | }; 9 | use error_stack::{Context, IntoReport, Result, ResultExt}; 10 | use rayon::prelude::*; 11 | use rkyv::{to_bytes, Deserialize, Infallible}; 12 | use std::{ 13 | collections::{hash_map::DefaultHasher, BTreeMap}, 14 | fmt::{Display, Formatter}, 15 | fs::File, 16 | hash::{Hash, Hasher}, 17 | io::{BufRead, BufReader, Write}, 18 | path::{Path, PathBuf}, 19 | process::{ChildStderr, ChildStdout, Command, Stdio}, 20 | thread::{self, Scope}, 21 | }; 22 | use tracing::{debug, info, warn}; 23 | 24 | use crate::cli::get_generations_from_file; 25 | 26 | #[derive(Debug)] 27 | pub enum Error { 28 | Load, 29 | LoadManifest, 30 | } 31 | 32 | impl Display for Error { 33 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 34 | f.write_str(match self { 35 | Self::Load => "Failed to load alpacka", 36 | Self::LoadManifest => "Failed to load manifest", 37 | }) 38 | } 39 | } 40 | 41 | impl Context for Error {} 42 | 43 | /// Installs the latest generation of plugins 44 | /// 45 | /// # Errors 46 | /// Errors if the config file cannot be opened, or if the generations file cannot be fetched. 47 | /// May also error if a install command cannot be run. 48 | pub fn install(config_path: PathBuf, data_path: &PathBuf) -> Result<(), Error> { 49 | if !data_path.exists() { 50 | std::fs::create_dir_all(data_path) 51 | .into_report() 52 | .attach_printable_lazy(|| { 53 | format!( 54 | "Failed to create alpacka directory. Alpacka directory path: {}", 55 | data_path.display() 56 | ) 57 | }) 58 | .change_context(Error::Load)?; 59 | } 60 | 61 | load_alpacka(data_path, config_path)?; 62 | 63 | Ok(()) 64 | } 65 | 66 | fn load_alpacka(data_path: &Path, config_path: PathBuf) -> Result<(), Error> { 67 | let config_file = std::fs::File::open(config_path) 68 | .into_report() 69 | .attach_printable_lazy(|| "Failed to open config file".to_string()) 70 | .change_context(Error::Load)?; 71 | 72 | let config: Config = serde_json::from_reader(config_file) 73 | .into_report() 74 | .attach_printable_lazy(|| "Failed to parse config file".to_string()) 75 | .change_context(Error::Load)?; 76 | 77 | info!("Config loaded, checking for existing manifest"); 78 | 79 | let config_hash = { 80 | let mut hasher = DefaultHasher::new(); 81 | config.hash(&mut hasher); 82 | hasher.finish() 83 | }; 84 | 85 | let smiths: Vec = vec![Loaders::Git(Git::new())]; 86 | let generation_path = data_path.join("generations.rkyv"); 87 | 88 | let manifest = if generation_path.exists() { 89 | let generations_file = std::fs::read(&generation_path) 90 | .into_report() 91 | .attach_printable_lazy(|| { 92 | format!( 93 | "Failed to read generations file. Generations file path: {}", 94 | generation_path.display() 95 | ) 96 | }) 97 | .change_context(Error::Load)?; 98 | 99 | let generations = get_generations_from_file(&generations_file) 100 | .map_err(|_| Error::Load) 101 | .into_report() 102 | .attach_printable_lazy(|| { 103 | format!( 104 | "Failed to parse generations file. Generations file path: {}", 105 | generation_path.display() 106 | ) 107 | })?; 108 | 109 | // find generation that have the same hash as the current config, and the highest generation 110 | get_latest(generations, config_hash).map_or_else( 111 | || create_manifest_from_config(&smiths, &config, &generation_path, Some(generations)), 112 | |manifest| { 113 | info!( 114 | "Found generation with the same hash as the current config, loading manifest" 115 | ); 116 | let manifest: Manifest = manifest.deserialize(&mut Infallible).unwrap(); 117 | Ok(manifest) 118 | }, 119 | ) 120 | } else { 121 | create_manifest_from_config(&smiths, &config, &generation_path, None) 122 | }?; 123 | 124 | info!("Manifest loaded, creating packages"); 125 | 126 | manifest 127 | .plugins 128 | .par_iter() 129 | .map(|plugin| load_plugin(&smiths, plugin, data_path)) 130 | .collect::>()?; 131 | 132 | Ok(()) 133 | } 134 | 135 | #[tracing::instrument(skip(generations))] 136 | fn create_manifest_from_config( 137 | smiths: &[Loaders], 138 | config: &Config, 139 | generations_path: &Path, 140 | generations: Option<&ArchivedGenerationsFile>, 141 | ) -> Result { 142 | let packages = config 143 | .create_package_list(smiths) 144 | .attach_printable_lazy(|| "Failed to create package list") 145 | .change_context(Error::LoadManifest)?; 146 | 147 | info!("packages created, resolving"); 148 | let resolved_packages = packages 149 | .into_par_iter() 150 | .map(|package| package.resolve_recurse(smiths)) 151 | .collect::, _>>() 152 | .change_context(Error::LoadManifest) 153 | .attach_printable_lazy(|| "Failed to resolve packages!")? 154 | .into_iter() 155 | .flatten() 156 | .collect::>(); 157 | 158 | debug!("Resolved packages: {:#?}", resolved_packages); 159 | 160 | let plugins = resolved_packages 161 | .into_iter() 162 | .map(|(loader_data, package)| { 163 | let smith = smiths 164 | .iter() 165 | .find(|s| s.name() == package.smith) 166 | .ok_or(Error::LoadManifest) 167 | .into_report() 168 | .attach_printable_lazy(|| { 169 | format!("Failed to find smith. Smith name: {}", package.smith) 170 | })?; 171 | 172 | let WithSmith { 173 | smith: smith_to_use, 174 | package, 175 | } = package; 176 | 177 | let Package { 178 | name, 179 | config_package, 180 | } = package; 181 | 182 | let PackageConfig { 183 | optional, 184 | build, 185 | dependencies, 186 | rename, 187 | version: _, 188 | } = config_package; 189 | 190 | let plugin = Plugin { 191 | name: smith 192 | .get_package_name(name) 193 | .ok_or(Error::LoadManifest) 194 | .into_report() 195 | .attach_printable_lazy(|| { 196 | format!("Failed to get package name. Package name: {}", package.name) 197 | })?, 198 | unresolved_name: name.to_string(), 199 | rename: rename.clone(), 200 | optional: optional.unwrap_or(false), 201 | dependencies: dependencies.keys().cloned().collect(), 202 | build: build.clone().unwrap_or_default(), 203 | smith: smith_to_use, 204 | loader_data, 205 | }; 206 | 207 | Ok(plugin) 208 | }) 209 | .collect::, _>>()?; 210 | 211 | let manifest = Manifest { 212 | neovim_version: "0.9.0".to_string(), 213 | plugins, 214 | }; 215 | 216 | info!("resolved manifest, saving"); 217 | 218 | let hash = { 219 | let mut hasher = DefaultHasher::new(); 220 | config.hash(&mut hasher); 221 | hasher.finish() 222 | }; 223 | 224 | let new_generations_file = if let Some(generations) = generations { 225 | add_to_generations(generations, hash, manifest) 226 | } else { 227 | let mut gen_file = GenerationsFile(BTreeMap::new()); 228 | gen_file.add_to_generations(hash, manifest); 229 | gen_file 230 | }; 231 | 232 | // overwrite the generations file 233 | let mut file = File::create(generations_path) 234 | .into_report() 235 | .attach_printable_lazy(|| { 236 | format!( 237 | "Failed to create generations file. Path: {}", 238 | generations_path.display() 239 | ) 240 | }) 241 | .change_context(Error::Load)?; 242 | 243 | let bytes = to_bytes::<_, 1024>(&new_generations_file) 244 | .into_report() 245 | .attach_printable_lazy(|| "Failed to serialize generations file") 246 | .change_context(Error::Load)?; 247 | 248 | file.write_all(&bytes) 249 | .into_report() 250 | .attach_printable_lazy(|| { 251 | format!( 252 | "Failed to write to generations file. Path: {}", 253 | generations_path.display() 254 | ) 255 | }) 256 | .change_context(Error::Load)?; 257 | 258 | info!("generations file saved, getting latest manifest"); 259 | 260 | let manifest = new_generations_file 261 | .0 262 | .into_iter() 263 | .last() 264 | .ok_or(Error::Load) 265 | .into_report() 266 | .attach_printable_lazy(|| "Failed to get latest manifest") 267 | .change_context(Error::LoadManifest)?; 268 | 269 | Ok(manifest.1) 270 | } 271 | 272 | #[tracing::instrument] 273 | fn load_plugin(smiths: &[Loaders], plugin: &Plugin, data_path: &Path) -> Result<(), Error> { 274 | let smith = smiths 275 | .iter() 276 | .find(|s| s.name() == plugin.smith) 277 | .ok_or(Error::LoadManifest) 278 | .into_report() 279 | .attach_printable_lazy(|| format!("Failed to find smith. Smith name: {}", plugin.smith))?; 280 | 281 | let package_path = data_path 282 | .join(if plugin.optional { "opt" } else { "start" }) 283 | .join(plugin.rename.as_ref().unwrap_or(&plugin.name)); 284 | 285 | smith 286 | .load(&plugin.loader_data, &package_path) 287 | .attach_printable_lazy(|| { 288 | format!( 289 | "Failed to load package. Package name: {}, Package path: {}", 290 | plugin.name, 291 | package_path.display() 292 | ) 293 | }) 294 | .change_context(Error::LoadManifest)?; 295 | 296 | let build_script_exists = !plugin.build.is_empty(); 297 | if build_script_exists { 298 | let mut build_arguments = plugin.build.split_whitespace(); 299 | 300 | let build_command = build_arguments 301 | .next() 302 | .ok_or(Error::LoadManifest) 303 | .into_report() 304 | .attach_printable_lazy(|| { 305 | format!( 306 | "Failed to get build command. You may have an invalid build command. Package name: {}, Package path: {}", 307 | plugin.name, 308 | package_path.display() 309 | ) 310 | }) 311 | .change_context(Error::LoadManifest)?; 312 | 313 | let command = Command::new(build_command) 314 | .args(build_arguments) 315 | .stdout(Stdio::piped()) 316 | .stderr(Stdio::piped()) 317 | .current_dir(&package_path) 318 | .spawn() 319 | .into_report() 320 | .attach_printable_lazy(|| { 321 | format!( 322 | "Failed to run build script. Package name: {}, Package path: {}", 323 | plugin.name, 324 | package_path.display() 325 | ) 326 | }) 327 | .change_context(Error::LoadManifest)?; 328 | 329 | let stdout = command.stdout.ok_or(Error::LoadManifest).into_report()?; 330 | 331 | let stderr = command.stderr.ok_or(Error::LoadManifest).into_report()?; 332 | 333 | let stdout = BufReader::new(stdout); 334 | let stderr = BufReader::new(stderr); 335 | 336 | thread::scope(move |threads| create_stdio_readers(&plugin.name, stdout, stderr, threads))?; 337 | } 338 | 339 | Ok(()) 340 | } 341 | 342 | fn create_stdio_readers<'a>( 343 | plugin_name: &'a str, 344 | stdout: BufReader, 345 | stderr: BufReader, 346 | threads: &'a Scope<'a, '_>, 347 | ) -> Result<(), Error> { 348 | let stdout = threads.spawn(move || -> Result<(), Error> { 349 | for line in stdout.lines() { 350 | let line = line.into_report().change_context(Error::Load)?; 351 | info!("STDOUT from build script of {}: {}", plugin_name, line); 352 | } 353 | 354 | Ok(()) 355 | }); 356 | 357 | let stderr = threads.spawn(move || -> Result<(), Error> { 358 | for line in stderr.lines() { 359 | let line = line.into_report().change_context(Error::Load)?; 360 | warn!("STDERR from build script {}: {}", plugin_name, line); 361 | } 362 | 363 | Ok(()) 364 | }); 365 | 366 | stdout.join().unwrap()?; 367 | stderr.join().unwrap()?; 368 | 369 | Ok(()) 370 | } 371 | -------------------------------------------------------------------------------- /alpacka-cli/src/cli/list_generations.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::BTreeMap, 3 | fmt::{Display, Formatter}, 4 | iter::once, 5 | path::Path, 6 | }; 7 | 8 | use error_stack::{ensure, Context, IntoReport, Result, ResultExt}; 9 | use rkyv::{Deserialize, Infallible}; 10 | use tracing::{error, info}; 11 | 12 | use alpacka::manifest::{GenerationsFile, Json, JsonGenerationsFile, Manifest}; 13 | 14 | use crate::cli::get_generations_from_file; 15 | 16 | use super::clap::ListGenerationsFormatMethod; 17 | 18 | #[derive(Debug)] 19 | pub enum Error { 20 | LoadError, 21 | FormattingError, 22 | } 23 | 24 | impl Display for Error { 25 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 26 | f.write_str(match self { 27 | Self::LoadError => "Failed to load alpacka", 28 | Self::FormattingError => "Failed to format output", 29 | }) 30 | } 31 | } 32 | 33 | impl Context for Error {} 34 | 35 | /// List all generations in the generations file. 36 | /// 37 | /// # Errors 38 | /// Errors if the generations file does not exist, or if the generations file is invalid. 39 | /// 40 | /// # Panics 41 | /// Panics if the generations file is invalid. 42 | pub fn list_generations( 43 | data_path: &Path, 44 | format_style: &ListGenerationsFormatMethod, 45 | ) -> Result<(), Error> { 46 | let generations_path = data_path.join("generations.rkyv"); 47 | 48 | ensure!(generations_path.exists(), { 49 | error!("Generations file path does not exist. Aborting"); 50 | Error::LoadError 51 | }); 52 | 53 | let generations_file = std::fs::read(&generations_path) 54 | .into_report() 55 | .attach_printable_lazy(|| { 56 | format!( 57 | "Failed to read generations file. Generations file path: {}", 58 | generations_path.display() 59 | ) 60 | }) 61 | .change_context(Error::LoadError)?; 62 | 63 | let generations = get_generations_from_file(&generations_file) 64 | .map_err(|_e| { 65 | error!( 66 | "Failed to load generations file. Generations file path: {}", 67 | generations_path.display() 68 | ); 69 | Error::LoadError 70 | }) 71 | .into_report()?; 72 | 73 | match format_style { 74 | ListGenerationsFormatMethod::Human => { 75 | for (idx, (hash, manifest)) in generations.0.iter().enumerate() { 76 | let hashed_config_file = hash.0; 77 | let generation_number = hash.1; 78 | 79 | // TODO: use the manifest for output 80 | let _manifest: Manifest = manifest.deserialize(&mut Infallible).unwrap(); 81 | 82 | info!("Manifest number {idx} | Hash {hashed_config_file} | generation {generation_number}"); 83 | } 84 | } 85 | ListGenerationsFormatMethod::Json => { 86 | let deserialized: GenerationsFile = generations.deserialize(&mut Infallible).unwrap(); 87 | 88 | // this is possibly the most cursed solution to this 89 | let json = deserialized.0.into_iter().fold( 90 | JsonGenerationsFile(BTreeMap::new()), 91 | |current, (hash, manifest)| { 92 | let new_map = current 93 | .0 94 | .into_iter() 95 | .chain(once(( 96 | hash.0.to_string(), 97 | Json { 98 | hash: hash.0.to_string(), 99 | generation: hash.1.to_string(), 100 | neovim_version: manifest.neovim_version, 101 | plugins: manifest.plugins, 102 | }, 103 | ))) 104 | .collect(); 105 | 106 | JsonGenerationsFile(new_map) 107 | }, 108 | ); 109 | 110 | let json = serde_json::to_string(&json) 111 | .into_report() 112 | .attach_printable_lazy(|| { 113 | format!( 114 | "Failed to convert generation file {} into JSON", 115 | generations_path.display() 116 | ) 117 | }) 118 | .change_context(Error::FormattingError)?; 119 | 120 | println!("{json}"); 121 | } 122 | } 123 | 124 | Ok(()) 125 | } 126 | -------------------------------------------------------------------------------- /alpacka-cli/src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | use bytecheck::TupleStructCheckError; 2 | use rkyv::{ 3 | check_archived_root, 4 | validation::{validators::DefaultValidatorError, CheckArchiveError}, 5 | }; 6 | 7 | use alpacka::manifest::{ArchivedGenerationsFile, GenerationsFile}; 8 | 9 | pub mod clap; 10 | pub mod install; 11 | pub mod list_generations; 12 | 13 | pub(crate) fn get_generations_from_file( 14 | generations_file: &[u8], 15 | ) -> Result<&ArchivedGenerationsFile, CheckArchiveError> 16 | { 17 | check_archived_root::(generations_file) 18 | } 19 | -------------------------------------------------------------------------------- /alpacka-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::multiple_crate_versions)] 2 | 3 | mod cli; 4 | 5 | use cli::{ 6 | clap::{Cli, ListGenerationsFormatMethod}, 7 | install::install, 8 | list_generations::list_generations, 9 | }; 10 | 11 | use clap::Parser; 12 | use error_stack::{Context, Report, ResultExt}; 13 | 14 | use std::{ 15 | fmt::{Display, Formatter}, 16 | path::PathBuf, 17 | }; 18 | use tracing_subscriber::{fmt::format::PrettyFields, prelude::*}; 19 | 20 | #[derive(Debug)] 21 | struct MainError; 22 | 23 | impl Display for MainError { 24 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 25 | f.write_str("Failed to run alpacka!") 26 | } 27 | } 28 | 29 | impl Context for MainError {} 30 | 31 | fn main() -> error_stack::Result<(), MainError> { 32 | #[cfg(feature = "vendor")] 33 | { 34 | openssl_probe::init_ssl_cert_env_vars(); 35 | } 36 | 37 | Report::set_color_mode(error_stack::fmt::ColorMode::Color); 38 | 39 | let error_handler = tracing_error::ErrorLayer::new(PrettyFields::new()); 40 | 41 | tracing_subscriber::fmt() 42 | .pretty() 43 | .with_env_filter( 44 | tracing_subscriber::EnvFilter::builder() 45 | .with_default_directive(tracing::Level::INFO.into()) 46 | .from_env_lossy(), 47 | ) 48 | .finish() 49 | .with(error_handler) 50 | .init(); 51 | 52 | match Cli::parse() { 53 | Cli::Install { path, data_dir } => cli_install(path, data_dir), 54 | Cli::ListGenerations { 55 | data_dir, 56 | format_style, 57 | } => cli_list_generations(data_dir, format_style), 58 | }?; 59 | 60 | Ok(()) 61 | } 62 | 63 | fn cli_list_generations( 64 | data_dir: Option, 65 | format_style: Option, 66 | ) -> Result<(), Report> { 67 | let data_path = get_data_path(data_dir); 68 | 69 | list_generations( 70 | &data_path, 71 | &format_style.unwrap_or(ListGenerationsFormatMethod::Human), 72 | ) 73 | .change_context(MainError) 74 | } 75 | 76 | fn cli_install(path: Option, data_dir: Option) -> Result<(), Report> { 77 | let data_path = get_data_path(data_dir); 78 | 79 | let config_path = path.unwrap_or_else(|| { 80 | let config_dir = std::env::var_os("XDG_CONFIG_HOME") 81 | .and_then(dirs_sys::is_absolute_path) 82 | .or_else(|| dirs_sys::home_dir().map(|h| h.join(".config"))); 83 | 84 | config_dir.map(|cd| cd.join("nvim/packages.json")).unwrap() 85 | }); 86 | 87 | install(config_path, &data_path).change_context(MainError) 88 | } 89 | 90 | fn get_data_path(data_dir: Option) -> PathBuf { 91 | data_dir.unwrap_or_else(|| { 92 | let data_dir = std::env::var_os("XDG_DATA_HOME") 93 | .and_then(dirs_sys::is_absolute_path) 94 | .or_else(|| dirs_sys::home_dir().map(|h| h.join(".local/share"))); 95 | 96 | data_dir 97 | .map(|dd| dd.join("nvim/site/pack/alpacka/")) 98 | .unwrap() 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /contrib/packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | "github:nyoom-engineering/nyoom.nvim": { 4 | "opt": true, 5 | "version": "branch:main", 6 | "dependencies": { 7 | "github:nyoom-engineering/oxocarbon.nvim": {} 8 | } 9 | }, 10 | "git:github.com:catppuccin/nvim": { 11 | "version": "tag:v0.2.9", 12 | "rename": "catppuccin", 13 | "build": "echo 'hi'" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "advisory-db": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1685821301, 7 | "narHash": "sha256-4XRcnSboLJw1XKjDpg2jBU70jEw/8Bgx4nUmnq3kXbY=", 8 | "owner": "rustsec", 9 | "repo": "advisory-db", 10 | "rev": "af3f3d503f82056785841bee49997bae65eba1c0", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "rustsec", 15 | "repo": "advisory-db", 16 | "type": "github" 17 | } 18 | }, 19 | "crane": { 20 | "inputs": { 21 | "flake-compat": "flake-compat", 22 | "flake-utils": "flake-utils", 23 | "nixpkgs": [ 24 | "nixpkgs" 25 | ], 26 | "rust-overlay": "rust-overlay" 27 | }, 28 | "locked": { 29 | "lastModified": 1686186025, 30 | "narHash": "sha256-SuQjKsO1G87qM5j8VNtq6kIw4ILYE03Y8yL/FoKwR+4=", 31 | "owner": "ipetkov", 32 | "repo": "crane", 33 | "rev": "057d95721ee67d421391dda7031977d247ddec28", 34 | "type": "github" 35 | }, 36 | "original": { 37 | "owner": "ipetkov", 38 | "repo": "crane", 39 | "type": "github" 40 | } 41 | }, 42 | "flake-compat": { 43 | "flake": false, 44 | "locked": { 45 | "lastModified": 1673956053, 46 | "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", 47 | "owner": "edolstra", 48 | "repo": "flake-compat", 49 | "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "edolstra", 54 | "repo": "flake-compat", 55 | "type": "github" 56 | } 57 | }, 58 | "flake-utils": { 59 | "inputs": { 60 | "systems": "systems" 61 | }, 62 | "locked": { 63 | "lastModified": 1685518550, 64 | "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", 65 | "owner": "numtide", 66 | "repo": "flake-utils", 67 | "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", 68 | "type": "github" 69 | }, 70 | "original": { 71 | "owner": "numtide", 72 | "repo": "flake-utils", 73 | "type": "github" 74 | } 75 | }, 76 | "flake-utils_2": { 77 | "inputs": { 78 | "systems": "systems_2" 79 | }, 80 | "locked": { 81 | "lastModified": 1685518550, 82 | "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", 83 | "owner": "numtide", 84 | "repo": "flake-utils", 85 | "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", 86 | "type": "github" 87 | }, 88 | "original": { 89 | "owner": "numtide", 90 | "repo": "flake-utils", 91 | "type": "github" 92 | } 93 | }, 94 | "nixpkgs": { 95 | "locked": { 96 | "lastModified": 1686368671, 97 | "narHash": "sha256-/I1IsENqXtqCzmpcd+poC0vu6JZRlzjHMdi3FXYr5qI=", 98 | "owner": "NixOS", 99 | "repo": "nixpkgs", 100 | "rev": "8f21f3a0a3b65567f21697597d7b061a4731345a", 101 | "type": "github" 102 | }, 103 | "original": { 104 | "owner": "NixOS", 105 | "ref": "nixpkgs-unstable", 106 | "repo": "nixpkgs", 107 | "type": "github" 108 | } 109 | }, 110 | "root": { 111 | "inputs": { 112 | "advisory-db": "advisory-db", 113 | "crane": "crane", 114 | "flake-utils": "flake-utils_2", 115 | "nixpkgs": "nixpkgs", 116 | "rust-overlay": "rust-overlay_2" 117 | } 118 | }, 119 | "rust-overlay": { 120 | "inputs": { 121 | "flake-utils": [ 122 | "crane", 123 | "flake-utils" 124 | ], 125 | "nixpkgs": [ 126 | "crane", 127 | "nixpkgs" 128 | ] 129 | }, 130 | "locked": { 131 | "lastModified": 1685759304, 132 | "narHash": "sha256-I3YBH6MS3G5kGzNuc1G0f9uYfTcNY9NYoRc3QsykLk4=", 133 | "owner": "oxalica", 134 | "repo": "rust-overlay", 135 | "rev": "c535b4f3327910c96dcf21851bbdd074d0760290", 136 | "type": "github" 137 | }, 138 | "original": { 139 | "owner": "oxalica", 140 | "repo": "rust-overlay", 141 | "type": "github" 142 | } 143 | }, 144 | "rust-overlay_2": { 145 | "inputs": { 146 | "flake-utils": [ 147 | "flake-utils" 148 | ], 149 | "nixpkgs": [ 150 | "nixpkgs" 151 | ] 152 | }, 153 | "locked": { 154 | "lastModified": 1686364106, 155 | "narHash": "sha256-h4gCQg+jizmAbdg6UPlhxQVk4A7Ar/zoLa0wx3wBya0=", 156 | "owner": "oxalica", 157 | "repo": "rust-overlay", 158 | "rev": "ba011dd1c5028dbb880bc3b0f427e0ff689e6203", 159 | "type": "github" 160 | }, 161 | "original": { 162 | "owner": "oxalica", 163 | "repo": "rust-overlay", 164 | "type": "github" 165 | } 166 | }, 167 | "systems": { 168 | "locked": { 169 | "lastModified": 1681028828, 170 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 171 | "owner": "nix-systems", 172 | "repo": "default", 173 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 174 | "type": "github" 175 | }, 176 | "original": { 177 | "owner": "nix-systems", 178 | "repo": "default", 179 | "type": "github" 180 | } 181 | }, 182 | "systems_2": { 183 | "locked": { 184 | "lastModified": 1681028828, 185 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 186 | "owner": "nix-systems", 187 | "repo": "default", 188 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 189 | "type": "github" 190 | }, 191 | "original": { 192 | "owner": "nix-systems", 193 | "repo": "default", 194 | "type": "github" 195 | } 196 | } 197 | }, 198 | "root": "root", 199 | "version": 7 200 | } 201 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Alpacka: A generational package manager for neovim"; 3 | nixConfig.substituters = ["https://nyoom-engineering.cachix.org"]; 4 | 5 | inputs = { 6 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | 9 | rust-overlay = { 10 | url = "github:oxalica/rust-overlay"; 11 | inputs.nixpkgs.follows = "nixpkgs"; 12 | inputs.flake-utils.follows = "flake-utils"; 13 | }; 14 | 15 | crane = { 16 | url = "github:ipetkov/crane"; 17 | inputs.nixpkgs.follows = "nixpkgs"; 18 | }; 19 | 20 | advisory-db = { 21 | url = "github:rustsec/advisory-db"; 22 | flake = false; 23 | }; 24 | }; 25 | 26 | outputs = { 27 | self, 28 | nixpkgs, 29 | crane, 30 | flake-utils, 31 | advisory-db, 32 | rust-overlay, 33 | ... 34 | }: 35 | flake-utils.lib.eachDefaultSystem (system: let 36 | pkgs = import nixpkgs { 37 | inherit system; 38 | overlays = [(import rust-overlay)]; 39 | }; 40 | 41 | rustToolchain = pkgs.pkgsBuildHost.rust-bin.stable.latest.default.override { 42 | extensions = ["rust-src" "rust-analyzer"]; 43 | targets = ["aarch64-apple-darwin"]; 44 | }; 45 | 46 | inherit (pkgs) lib; 47 | 48 | craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; 49 | src = craneLib.cleanCargoSource ./.; 50 | 51 | buildInputs = 52 | [ 53 | # libgit2 deps 54 | # including vendored 55 | pkgs.pkg-config 56 | pkgs.openssl 57 | pkgs.perl 58 | ] 59 | ++ lib.optionals pkgs.stdenv.isDarwin [ 60 | # libgit2 deps 61 | pkgs.darwin.apple_sdk.frameworks.Security 62 | # rust dep 63 | pkgs.libiconv 64 | ]; 65 | 66 | # Build *just* the cargo dependencies, so we can reuse 67 | # all of that work (e.g. via cachix) when running in CI 68 | cargoArtifacts = craneLib.buildDepsOnly { 69 | stdenv = pkgs.clangStdenv; 70 | inherit src buildInputs; 71 | }; 72 | 73 | # Build the actual crate itself, reusing the dependency 74 | # artifacts from above. 75 | alpacka = craneLib.buildPackage { 76 | inherit cargoArtifacts src buildInputs; 77 | stdenv = pkgs.clangStdenv; 78 | doCheck = false; 79 | }; 80 | in { 81 | checks = { 82 | # Build the crate as part of `nix flake check` for convenience, with tests 83 | # sandboxing must be disabled 84 | # since nextest doesn't support doctests yet, we are not using it. 85 | alpacka = craneLib.buildPackage { 86 | inherit cargoArtifacts src buildInputs; 87 | stdenv = pkgs.clangStdenv; 88 | doCheck = true; 89 | }; 90 | 91 | # Run clippy (and deny all warnings) on the crate source, 92 | # again, resuing the dependency artifacts from above. 93 | # 94 | # Note that this is done as a separate derivation so that 95 | # we can block the CI if there are issues here, but not 96 | # prevent downstream consumers from building our crate by itself. 97 | alpacka-clippy = craneLib.cargoClippy { 98 | inherit cargoArtifacts src buildInputs; 99 | stdenv = pkgs.clangStdenv; 100 | cargoClippyExtraArgs = "--all-targets -- --deny warnings"; 101 | }; 102 | 103 | alpacka-doc = craneLib.cargoDoc { 104 | inherit cargoArtifacts src buildInputs; 105 | stdenv = pkgs.clangStdenv; 106 | }; 107 | 108 | # Check formatting 109 | alpacka-fmt = craneLib.cargoFmt { 110 | inherit src; 111 | stdenv = pkgs.clangStdenv; 112 | }; 113 | 114 | # Audit dependencies 115 | alpacka-audit = craneLib.cargoAudit { 116 | inherit src advisory-db; 117 | stdenv = pkgs.clangStdenv; 118 | }; 119 | }; 120 | 121 | packages.default = alpacka; 122 | 123 | apps.default = flake-utils.lib.mkApp { 124 | drv = alpacka; 125 | }; 126 | 127 | devShells.default = pkgs.mkShell { 128 | inputsFrom = builtins.attrValues self.checks; 129 | 130 | # Extra inputs can be added here 131 | nativeBuildInputs = with pkgs; 132 | [ 133 | rustToolchain 134 | alejandra 135 | rnix-lsp 136 | pkg-config 137 | openssl 138 | git 139 | ] 140 | ++ buildInputs; 141 | }; 142 | }); 143 | } 144 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | package::{Config as ConfigPackage, Package, WithSmith}, 3 | smith::enums::Loaders, 4 | }; 5 | use error_stack::{Context, Result}; 6 | use serde::{Deserialize, Serialize}; 7 | use std::{collections::BTreeMap, fmt::Display}; 8 | 9 | #[derive(Debug, Deserialize, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord)] 10 | /// The alpacka config format 11 | pub struct Config { 12 | /// All the packages 13 | pub packages: BTreeMap, 14 | } 15 | 16 | #[derive(Debug)] 17 | /// An error that can occur when creating a list of packages 18 | pub enum CreatePackageListError { 19 | /// No loader found for a package 20 | NoLoaderFound(String), 21 | } 22 | 23 | impl Display for CreatePackageListError { 24 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 25 | match self { 26 | Self::NoLoaderFound(name) => { 27 | write!(f, "No loader found for package {name}") 28 | } 29 | } 30 | } 31 | } 32 | 33 | impl Context for CreatePackageListError {} 34 | 35 | impl Config { 36 | /// Create a list of packages with their corresponding smith 37 | /// 38 | /// # Errors 39 | /// This function will return an error if no loader can be found for a package 40 | #[tracing::instrument] 41 | pub fn create_package_list( 42 | &self, 43 | smiths: &[Loaders], 44 | ) -> Result, CreatePackageListError> { 45 | let mut packages = Vec::with_capacity(self.packages.len()); 46 | 47 | for (name, config_package) in &self.packages { 48 | let package = Package { 49 | name, 50 | config_package, 51 | }; 52 | 53 | let smith_idx = smiths 54 | .iter() 55 | .position(|smith| smith.get_package_name(package.name).is_some()) 56 | .ok_or_else(|| CreatePackageListError::NoLoaderFound(name.clone()))?; 57 | 58 | packages.push(WithSmith { 59 | smith: smiths[smith_idx].name().to_string(), 60 | package, 61 | }); 62 | } 63 | 64 | Ok(packages) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] 2 | // multiple crate versions is allowed because of other crates not updating deps 3 | // used underscore binding is allowed because of macros 4 | #![allow(clippy::multiple_crate_versions, clippy::used_underscore_binding)] 5 | 6 | //! Alpacka: the next-generation package manager for neovim. 7 | //! 8 | //! The library version of alpacka is designed to be used by other programs to interact and run the alpacka package manager. 9 | //! It exports library functions to load configurations, manifests, and packages. 10 | //! It also exports functions to run the package manager, such as resolving and loading plugins. 11 | //! 12 | //! This is NOT meant to be used by end-users, but rather by other programs that want to use alpacka as a library, such as a user-facing GUI/neovim plugin. 13 | pub mod config; 14 | pub mod manifest; 15 | pub mod package; 16 | pub mod smith; 17 | -------------------------------------------------------------------------------- /src/manifest.rs: -------------------------------------------------------------------------------- 1 | use bytecheck::CheckBytes; 2 | use rkyv::{to_bytes, Archive, Deserialize as RkyvDeserialize, Infallible}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{ 5 | collections::BTreeMap, 6 | fs::File, 7 | io::{self, BufWriter, Write}, 8 | path::PathBuf, 9 | }; 10 | 11 | use crate::smith::enums::Inputs; 12 | 13 | #[derive( 14 | Archive, 15 | rkyv::Serialize, 16 | rkyv::Deserialize, 17 | Debug, 18 | PartialEq, 19 | Eq, 20 | PartialOrd, 21 | Ord, 22 | Clone, 23 | Copy, 24 | Serialize, 25 | Deserialize, 26 | )] 27 | #[archive_attr(derive(CheckBytes, Eq, PartialEq, PartialOrd, Ord))] 28 | /// A hash of the config file 29 | /// 30 | /// The first value is the hash of the config file 31 | /// The second value is the generation number 32 | pub struct GenerationHash(pub u64, pub u64); 33 | 34 | /// A file which contains a list of all the generations 35 | /// The key is the config hash and the generation number 36 | /// The value is the path to the generation 37 | #[derive(Archive, rkyv::Serialize, rkyv::Deserialize, Debug)] 38 | #[archive_attr(derive(CheckBytes))] 39 | pub struct GenerationsFile(pub BTreeMap); 40 | 41 | #[derive(serde::Serialize, serde::Deserialize)] 42 | pub struct JsonGenerationsFile(pub BTreeMap); 43 | 44 | impl GenerationsFile { 45 | /// Create a new generations file 46 | #[must_use] 47 | pub const fn new() -> Self { 48 | Self(BTreeMap::new()) 49 | } 50 | 51 | /// Save the generations file to a file 52 | /// 53 | /// # Errors 54 | /// This function will return an error if the file can't be created, or if the generations can't be serialized 55 | pub fn save_to_file(&self, generation_path: &PathBuf) -> Result<(), std::io::Error> { 56 | let file = File::create(generation_path)?; 57 | 58 | let bytes = 59 | to_bytes::<_, 1024>(self).map_err(|e| io::Error::new(std::io::ErrorKind::Other, e))?; 60 | 61 | let mut writer = BufWriter::new(file); 62 | 63 | writer 64 | .write_all(&bytes) 65 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 66 | Ok(()) 67 | } 68 | 69 | /// Add a new generation to the generations file 70 | /// 71 | /// returns the [`GenerationHash`] of the new generation 72 | pub fn add_to_generations(&mut self, config_hash: u64, manifest: Manifest) -> GenerationHash { 73 | let generation_number = self.get_next_generation_number(config_hash); 74 | let hash = GenerationHash(config_hash, generation_number); 75 | 76 | self.0.insert(hash, manifest); 77 | 78 | hash 79 | } 80 | 81 | /// Get the latest manifest for a config hash 82 | /// returns [`Option::None`] if there is no generation associated with that config hash 83 | /// 84 | /// Else returns the latest [Manifest] for that config hash 85 | #[must_use] 86 | pub fn get_latest_manifest(&self, config_hash: u64) -> Option<&Manifest> { 87 | self.0 88 | .iter() 89 | .find_map(|(GenerationHash(hash, _), manifest)| { 90 | if *hash == config_hash { 91 | Some(manifest) 92 | } else { 93 | None 94 | } 95 | }) 96 | } 97 | 98 | /// Get the latest generation number for a config hash 99 | /// 100 | /// returns [`Option::None`] if there is no generation associated with that config hash 101 | /// else returns the latest generation number for that config hash 102 | #[must_use] 103 | pub fn get_latest_generation_number(&self, config_hash: u64) -> Option { 104 | self.0 105 | .keys() 106 | .filter(|GenerationHash(hash, _)| *hash == config_hash) 107 | .max_by_key(|GenerationHash(_, generation)| generation) 108 | .map(|GenerationHash(_, generation)| generation) 109 | .copied() 110 | } 111 | 112 | /// Get the next generation number for a config hash 113 | /// returns 1 if there is no manifest for that config hash 114 | #[must_use] 115 | pub fn get_next_generation_number(&self, config_hash: u64) -> u64 { 116 | self.get_latest_generation_number(config_hash).unwrap_or(0) + 1 117 | } 118 | } 119 | 120 | impl Default for GenerationsFile { 121 | fn default() -> Self { 122 | Self::new() 123 | } 124 | } 125 | 126 | /// Get the latest manifest for a config hash 127 | /// returns [`Option::None`] if the config doesn't have any associated generations 128 | /// else, returns the latest [`Manifest`] as a an [`ArchivedManifest`] for that config hash 129 | #[must_use] 130 | pub fn get_latest( 131 | generation_file: &ArchivedGenerationsFile, 132 | config_hash: u64, 133 | ) -> Option<&ArchivedManifest> { 134 | generation_file 135 | .0 136 | .iter() 137 | .find_map(|(ArchivedGenerationHash(hash, _), manifest)| { 138 | if *hash == config_hash { 139 | Some(manifest) 140 | } else { 141 | None 142 | } 143 | }) 144 | } 145 | 146 | /// Get the latest generation number for a config hash 147 | /// returns [`Option::None`] if the config doesn't have any associated generations 148 | /// 149 | /// else, returns the latest generation number for that config hash 150 | #[must_use] 151 | pub fn get_latest_generation_number( 152 | generation_file: &ArchivedGenerationsFile, 153 | config_hash: u64, 154 | ) -> Option { 155 | generation_file 156 | .0 157 | .keys() 158 | .filter(|ArchivedGenerationHash(hash, _)| *hash == config_hash) 159 | .max_by_key(|ArchivedGenerationHash(_, generation)| generation) 160 | .map(|ArchivedGenerationHash(_, generation)| generation) 161 | .copied() 162 | } 163 | 164 | /// Get the next generation number for a config hash 165 | /// returns 1 if the config hash doesn't have any associated generations 166 | #[must_use] 167 | pub fn get_next_generation_number( 168 | generation_file: &ArchivedGenerationsFile, 169 | config_hash: u64, 170 | ) -> u64 { 171 | get_latest_generation_number(generation_file, config_hash).unwrap_or(0) + 1 172 | } 173 | 174 | /// Add a new generation to the generations file 175 | /// 176 | /// returns a deserialized [`GenerationFile`] with the new generation added 177 | /// 178 | /// # Panics 179 | /// Cannot panic, as the only error that can occur is [`Infallible`] 180 | pub fn add_to_generations( 181 | generation_file: &ArchivedGenerationsFile, 182 | config_hash: u64, 183 | manifest: Manifest, 184 | ) -> GenerationsFile { 185 | let mut generations: GenerationsFile = generation_file.deserialize(&mut Infallible).unwrap(); 186 | 187 | generations.add_to_generations(config_hash, manifest); 188 | 189 | generations 190 | } 191 | 192 | /// An alpacka manifest 193 | /// 194 | /// This is the file that is generated by alpacka and is used to install plugins 195 | /// Contains a list of plugins and the neovim version it was built for 196 | #[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] 197 | #[archive_attr(derive(CheckBytes))] 198 | pub struct Manifest { 199 | /// The neovim version this manifest was built for 200 | pub neovim_version: String, 201 | pub plugins: Vec, 202 | } 203 | 204 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 205 | pub struct Json { 206 | /// The neovim version this manifest was built for 207 | pub neovim_version: String, 208 | pub plugins: Vec, 209 | // These need to be strings because u64 cannot fit into JSON 210 | pub hash: String, 211 | pub generation: String, 212 | } 213 | 214 | impl Manifest { 215 | #[must_use] 216 | pub fn new(neovim_version: String, plugins: Vec) -> Self { 217 | Self { 218 | neovim_version, 219 | plugins, 220 | } 221 | } 222 | 223 | /// Save the manifest to a file 224 | /// 225 | /// # Errors 226 | /// This function will return an error if the file can't be created, or if the manifest can't be serialized 227 | #[tracing::instrument] 228 | pub fn save_to_file(&self, path: &PathBuf) -> Result<(), std::io::Error> { 229 | let file = std::fs::File::create(path)?; 230 | let bytes = to_bytes::<_, 1024>(self) 231 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; 232 | let mut writer = std::io::BufWriter::new(file); 233 | 234 | writer 235 | .write_all(&bytes) 236 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; 237 | 238 | Ok(()) 239 | } 240 | } 241 | 242 | /// A plugin, as defined in the manifest 243 | #[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Serialize, Deserialize)] 244 | #[archive_attr(derive(CheckBytes))] 245 | pub struct Plugin { 246 | /// The plugin's name 247 | pub name: String, 248 | /// The plugin's unresolved name 249 | /// This is the name that is used in the config file 250 | /// This is used to resolve dependencies 251 | pub unresolved_name: String, 252 | /// Rename the plugin to this name when loading 253 | pub rename: Option, 254 | /// If the plugin is optional 255 | pub optional: bool, 256 | /// The plugin's dependencies, as a list of plugin names. These are the non-resolved names 257 | pub dependencies: Vec, 258 | /// the name of the loader this plugin is loaded by 259 | pub smith: String, 260 | /// A command which is run in the plugin's directory after loading 261 | pub build: String, 262 | /// The data which is used for the loader 263 | pub loader_data: Inputs, 264 | } 265 | 266 | #[cfg(test)] 267 | mod tests { 268 | use super::*; 269 | use rkyv::to_bytes; 270 | 271 | #[test] 272 | fn test_generation_hash_serialize_deserialize() { 273 | let generation = GenerationHash(123_125_124, 32); 274 | 275 | let bytes = to_bytes::<_, 1024>(&generation).unwrap(); 276 | let deserialized = rkyv::from_bytes::(&bytes).unwrap(); 277 | assert_eq!(generation, deserialized); 278 | } 279 | 280 | #[test] 281 | fn test_generations_file_serialize_deserialize() { 282 | let mut generations_file = GenerationsFile::new(); 283 | let manifest = Manifest { 284 | neovim_version: "0.5.0".to_string(), 285 | plugins: vec![], 286 | }; 287 | 288 | let hash = generations_file.add_to_generations(1, manifest); 289 | 290 | let bytes = to_bytes::<_, 1024>(&generations_file).unwrap(); 291 | let deserialized = rkyv::from_bytes::(&bytes).unwrap(); 292 | 293 | let generations_file_manifest = generations_file.0.get(&hash).unwrap(); 294 | let deserialized_manifest = deserialized.0.get(&hash).unwrap(); 295 | 296 | assert_eq!( 297 | generations_file_manifest.neovim_version, 298 | deserialized_manifest.neovim_version 299 | ); 300 | } 301 | 302 | #[test] 303 | fn test_get_next_generation_number() { 304 | let mut generations_file = GenerationsFile::new(); 305 | let manifest = Manifest { 306 | neovim_version: "0.5.0".to_string(), 307 | plugins: vec![], 308 | }; 309 | 310 | assert_eq!(generations_file.get_next_generation_number(0), 1); 311 | 312 | generations_file.add_to_generations(0, manifest); 313 | 314 | assert_eq!(generations_file.get_next_generation_number(0), 2); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/package.rs: -------------------------------------------------------------------------------- 1 | //! A module which contains structs and types for packages 2 | 3 | use crate::smith::{ 4 | enums::{Inputs, Loaders}, 5 | ResolveError, 6 | }; 7 | use error_stack::{IntoReport, Result, ResultExt}; 8 | use rayon::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | use std::collections::BTreeMap; 11 | 12 | #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 13 | /// A package declaration, as found in a config file 14 | pub struct Config { 15 | /// Don't load the package on startup 16 | pub optional: Option, 17 | /// The package version. Internally uses git tags when using git, else the resolver decides 18 | pub version: Option, 19 | /// rename the package to something else 20 | pub rename: Option, 21 | /// A command to build the package. This is run in the package directory 22 | pub build: Option, 23 | /// A list of dependencies 24 | #[serde(default)] 25 | pub dependencies: BTreeMap, 26 | } 27 | 28 | #[derive(Debug, Clone)] 29 | /// A package declaration, as found in a config file plus some additional information 30 | /// 31 | /// The reason for using references is to avoid cloning the entire config when resolving a package 32 | pub struct Package<'a> { 33 | /// The name of the package 34 | pub name: &'a str, 35 | /// The package's config, as found in the config file 36 | pub config_package: &'a Config, 37 | } 38 | 39 | #[derive(Debug, Clone)] 40 | /// A package declaration and a smith name used to handle said package 41 | pub struct WithSmith<'a> { 42 | /// The name of the smith used to handle this package 43 | pub smith: String, 44 | /// The package to be handled 45 | pub package: Package<'a>, 46 | } 47 | 48 | /// A type alias for a package loader input and a package with a smith 49 | pub type WithLoaderInput<'a> = (Inputs, WithSmith<'a>); 50 | 51 | impl<'a> WithSmith<'a> { 52 | /// Check if this package is optional 53 | #[must_use] 54 | pub fn is_optional(&self) -> bool { 55 | self.package.config_package.optional.unwrap_or(false) 56 | } 57 | 58 | /// Resolve a package to a loader package, which has all the necessary information to load the package. 59 | /// 60 | /// # Errors 61 | /// This function will return an error if the package cannot be resolved. 62 | #[tracing::instrument] 63 | pub fn resolve(&self, smiths: &[Loaders]) -> Result { 64 | let smith = smiths 65 | .iter() 66 | .find(|smith| smith.name() == self.smith) 67 | .ok_or(ResolveError) 68 | .into_report() 69 | .attach_printable_lazy(|| format!("Smith {} not found", self.smith))?; 70 | 71 | smith.resolve(&self.package) 72 | } 73 | 74 | /// Recursively resolve a package to a loader package, which has all the necessary information to load the package. 75 | /// This function will also resolve all dependencies of the package. 76 | /// 77 | /// See [`WithSmith::resolve`] for more information. 78 | /// 79 | /// # Errors 80 | /// This function will return an error if the package or one of its dependencies cannot be resolved. 81 | #[tracing::instrument] 82 | pub fn resolve_recurse( 83 | self, 84 | smiths: &'a [Loaders], 85 | ) -> Result, ResolveError> { 86 | let mut deps = self 87 | .package 88 | .config_package 89 | .dependencies 90 | .par_iter() 91 | .map(|(name, config_package)| { 92 | let pkg = Package { 93 | name, 94 | config_package, 95 | }; 96 | 97 | let smith_to_use = smiths 98 | .iter() 99 | .find(|s| s.get_package_name(pkg.name).is_some()) 100 | .ok_or(ResolveError) 101 | .into_report() 102 | .attach_printable_lazy(|| { 103 | format!("Failed to find smith. Package name: {}", pkg.name) 104 | })?; 105 | 106 | let package = Self { 107 | smith: smith_to_use.name(), 108 | package: pkg, 109 | }; 110 | 111 | package.resolve_recurse(smiths) 112 | }) 113 | .collect::, _>>()? 114 | .into_iter() 115 | .flatten() 116 | .collect::>(); 117 | 118 | let smith_to_use = smiths 119 | .iter() 120 | .find(|s| s.name() == self.smith) 121 | .ok_or(ResolveError) 122 | .into_report() 123 | .attach_printable_lazy(|| { 124 | format!("Failed to find smith. Smith name: {}", self.smith) 125 | })?; 126 | 127 | let loader_data = smith_to_use 128 | .resolve(&self.package) 129 | .attach_printable_lazy(|| { 130 | format!( 131 | "Failed to resolve package. Package name: {}", 132 | self.package.name 133 | ) 134 | }) 135 | .change_context(ResolveError)?; 136 | 137 | let mut final_package = vec![(loader_data, self)]; 138 | deps.append(&mut final_package); 139 | 140 | Ok(deps) 141 | } 142 | } 143 | 144 | #[cfg(test)] 145 | mod tests { 146 | use super::*; 147 | 148 | #[test] 149 | fn test_with_smith_is_optional() { 150 | let with_smith = WithSmith { 151 | smith: "test".to_string(), 152 | package: Package { 153 | name: "test", 154 | config_package: &Config { 155 | optional: Some(true), 156 | version: None, 157 | rename: None, 158 | build: None, 159 | dependencies: BTreeMap::new(), 160 | }, 161 | }, 162 | }; 163 | 164 | assert!(with_smith.is_optional()); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/smith/enums.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use bytecheck::CheckBytes; 4 | use error_stack::Result; 5 | use rkyv::{Archive, Deserialize, Serialize}; 6 | 7 | use super::{git::Input, Git, LoadError, ResolveError, Smith}; 8 | 9 | #[derive(Debug)] 10 | pub enum Loaders { 11 | Git(Git), 12 | } 13 | 14 | impl Loaders { 15 | #[must_use] 16 | pub fn name(&self) -> String { 17 | match self { 18 | Self::Git(git) => git.name(), 19 | } 20 | } 21 | 22 | #[must_use] 23 | pub fn get_package_name(&self, name: &str) -> Option { 24 | match self { 25 | Self::Git(git) => git.get_package_name(name), 26 | } 27 | } 28 | 29 | /// Resolve the input for a package 30 | /// 31 | /// # Errors 32 | /// This function will return an error if the package cannot be resolved. 33 | pub fn resolve(&self, package: &super::Package) -> Result { 34 | Ok(match self { 35 | Self::Git(git) => Inputs::Git(git.resolve(package)?), 36 | }) 37 | } 38 | 39 | /// Load a package 40 | /// 41 | /// # Errors 42 | /// This function will return an error if the package cannot be loaded. 43 | pub fn load(&self, input: &Inputs, path: &Path) -> Result<(), LoadError> { 44 | match input { 45 | Inputs::Git(input) => match self { 46 | Self::Git(git) => git.load(input, path), 47 | }, 48 | } 49 | } 50 | } 51 | 52 | #[derive(Debug, Archive, Serialize, Deserialize, serde::Serialize, serde::Deserialize, Clone)] 53 | #[archive_attr(derive(CheckBytes, Debug))] 54 | pub enum Inputs { 55 | Git(Input), 56 | } 57 | -------------------------------------------------------------------------------- /src/smith/git.rs: -------------------------------------------------------------------------------- 1 | use crate::package::Package; 2 | use bytecheck::CheckBytes; 3 | use error_stack::{Context, IntoReport, Result as ErrorStackResult, ResultExt}; 4 | use git2::{ErrorCode, Repository}; 5 | use rkyv::Archive; 6 | use serde::{Deserialize, Serialize}; 7 | use std::{fmt::Display, path::Path}; 8 | use tracing::debug; 9 | 10 | use super::{LoadError, LoaderInput, ResolveError, Smith}; 11 | 12 | #[derive(Debug)] 13 | /// An error that can occur when resolving a git package 14 | enum GitError { 15 | /// An error occurred when running a libgit2 command 16 | GitError, 17 | /// An IO error occurred 18 | IoError, 19 | } 20 | 21 | impl Display for GitError { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | match self { 24 | Self::IoError => f.write_str("IO error"), 25 | Self::GitError => f.write_str("Git error"), 26 | } 27 | } 28 | } 29 | 30 | impl Context for GitError {} 31 | 32 | #[derive(Debug, Default, Clone)] 33 | /// The way to clone a git repository 34 | pub enum CloneType { 35 | /// Clone using the ssh protocol 36 | Ssh, 37 | /// Clone using the https protocol 38 | #[default] 39 | Https, 40 | } 41 | 42 | #[derive(Debug, Clone, Default)] 43 | /// A smith that can be used to resolve and load a git package. 44 | pub struct Git { 45 | /// The method to use when cloning the repository 46 | pub clone_type: CloneType, 47 | } 48 | 49 | impl Git { 50 | #[must_use] 51 | /// Create a new git smith with the default clone type 52 | pub fn new() -> Self { 53 | Self::new_with_type(CloneType::default()) 54 | } 55 | 56 | #[must_use] 57 | /// Create a new git smith with the given clone type 58 | pub const fn new_with_type(clone_type: CloneType) -> Self { 59 | Self { clone_type } 60 | } 61 | } 62 | 63 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 64 | /// The lock type for a git package 65 | pub enum LockType { 66 | /// Lock to a specific tag 67 | Tag(String), 68 | /// Lock to a specific commit 69 | Commit(String), 70 | /// Lock to a specific branch 71 | Branch(String), 72 | /// Lock to the default branch 73 | #[default] 74 | Default, 75 | } 76 | 77 | #[derive(Debug, rkyv::Serialize, rkyv::Deserialize, Archive, Clone, Serialize, Deserialize)] 78 | #[archive_attr(derive(CheckBytes, Debug))] 79 | /// The input for a git loader 80 | pub struct Input { 81 | /// The commit hash to lock to 82 | commit_hash: String, 83 | /// The remote to use 84 | remote: String, 85 | } 86 | 87 | impl LoaderInput for Input {} 88 | 89 | impl Smith for Git { 90 | type Input = Input; 91 | 92 | fn name(&self) -> String { 93 | "git".to_string() 94 | } 95 | 96 | #[tracing::instrument] 97 | fn resolve(&self, package: &Package) -> ErrorStackResult { 98 | let Some((repo_type, repo_url)) = package 99 | .name 100 | .split_once(':') else { 101 | unreachable!("should be handled by handles_package") 102 | }; 103 | 104 | let repo_type = repo_type.to_owned(); 105 | let repo_url = repo_url.to_owned(); 106 | 107 | let url = match (repo_type.as_str(), repo_url.as_str()) { 108 | ("git", repo) => match self.clone_type { 109 | CloneType::Ssh => format!("git@{repo}.git"), 110 | CloneType::Https => { 111 | let (host, path) = repo 112 | .split_once(':') 113 | .ok_or(ResolveError) 114 | .into_report() 115 | .attach_printable_lazy(|| { 116 | format!( 117 | "Failed to parse git repo: {repo}. Format: git:{{host}}:{{path}}" 118 | ) 119 | })?; 120 | 121 | format!("https://{host}/{path}.git") 122 | } 123 | }, 124 | ("github", repo_url) => match self.clone_type { 125 | CloneType::Ssh => format!("git@github.com:{repo_url}.git"), 126 | CloneType::Https => format!("https://github.com/{repo_url}.git"), 127 | }, 128 | ("gitlab", repo_url) => match self.clone_type { 129 | CloneType::Ssh => format!("git@gitlab.com:{repo_url}.git"), 130 | CloneType::Https => format!("https://gitlab.com/{repo_url}.git"), 131 | }, 132 | ("srht", repo_url) => match self.clone_type { 133 | CloneType::Ssh => format!("git@git.sr.ht:{repo_url}"), 134 | CloneType::Https => format!("https://git.sr.ht/{repo_url}"), 135 | }, 136 | _ => unreachable!("should be handled by handles_package"), 137 | }; 138 | 139 | debug!("url: {url}"); 140 | 141 | let lock_type = match package 142 | .config_package 143 | .version 144 | .as_ref() 145 | .and_then(|v| v.split_once(':')) 146 | { 147 | Some(("tag", tag)) => LockType::Tag(tag.to_string()), 148 | Some(("commit", commit)) => LockType::Commit(commit.to_string()), 149 | Some(("branch", branch)) => LockType::Branch(branch.to_string()), 150 | _ => LockType::Default, 151 | }; 152 | 153 | debug!("lock_type: {lock_type:?}"); 154 | 155 | let temp_git_dir = tempfile::tempdir() 156 | .into_report() 157 | .change_context(GitError::IoError) 158 | .attach_printable_lazy(|| format!("Failed to create temp dir for git repo: {url}")) 159 | .change_context(ResolveError)?; 160 | 161 | // init git repo and add remote 162 | let repo = git2::Repository::init(temp_git_dir.path()) 163 | .into_report() 164 | .change_context(GitError::GitError) 165 | .attach_printable_lazy(|| format!("Failed to init git repo: {url}")) 166 | .change_context(ResolveError)?; 167 | 168 | let mut remote = repo 169 | .remote_anonymous(&url) 170 | .into_report() 171 | .change_context(GitError::GitError) 172 | .attach_printable_lazy(|| format!("Failed to add remote: {url}")) 173 | .change_context(ResolveError)?; 174 | 175 | fetch_remote(&url, &lock_type, &mut remote).change_context(ResolveError)?; 176 | 177 | let fetch_head = repo 178 | .find_reference("FETCH_HEAD") 179 | .into_report() 180 | .change_context(GitError::GitError) 181 | .attach_printable_lazy(|| format!("Failed to find FETCH_HEAD: {url}. Check if the specified commit, tag, or branch exists.")) 182 | .change_context(ResolveError)?; 183 | 184 | let commit_hash = fetch_head 185 | .peel_to_commit() 186 | .into_report() 187 | .change_context(GitError::GitError) 188 | .attach_printable_lazy(|| format!("Failed to peel FETCH_HEAD to commit: {url}")) 189 | .change_context(ResolveError)? 190 | .id() 191 | .to_string(); 192 | 193 | Ok(Input { 194 | commit_hash, 195 | remote: url, 196 | }) 197 | } 198 | 199 | /// Gets the commit messages between a sha and the current HEAD, if not sha is provided, it 200 | /// takes up to 5 of the latest commits on the current branch, provided they exist. 201 | /// 202 | /// ```ignore 203 | /// use alpacka::{package::{Package, Config}, smith::{Smith, Git}}; 204 | /// use std::path::Path; 205 | /// use std::collections::BTreeMap; 206 | /// 207 | /// let curr_dir = Path::new("testing"); 208 | /// 209 | /// let smith = Git::new(); 210 | /// 211 | /// let pkg = smith.resolve(&Package { 212 | /// name: "github:zackartz/testing_repo".to_string(), 213 | /// config_package: Config { 214 | /// version: Some("tag:0.1.1".to_string()), 215 | /// build: None, 216 | /// dependencies: BTreeMap::new(), 217 | /// optional: None, 218 | /// rename: None, 219 | /// } 220 | /// }).unwrap(); 221 | /// 222 | /// smith.load(&pkg, &curr_dir); 223 | /// 224 | /// // sha of 0.1.0 225 | /// let oid = git2::Oid::from_str("90600fc317747afad28add17705199fc1eead17c").unwrap(); 226 | /// 227 | /// let commits = smith.get_change_log(Some(oid), &curr_dir).unwrap(); 228 | /// 229 | /// assert_eq!(commits[0], "Update README.md"); 230 | /// 231 | /// // cleanup 232 | /// std::fs::remove_dir_all(&curr_dir); 233 | /// ``` 234 | #[tracing::instrument] 235 | fn get_change_log( 236 | &self, 237 | old_sha: Option, 238 | path: &Path, 239 | ) -> ErrorStackResult, LoadError> { 240 | let repo = match git2::Repository::open(path) { 241 | Ok(repo) => repo, 242 | Err(e) => { 243 | return Err(e) 244 | .into_report() 245 | .change_context(GitError::GitError) 246 | .attach_printable_lazy(|| format!("Failed to open repo: {path:?}")) 247 | .change_context(LoadError) 248 | } 249 | }; 250 | 251 | let mut revwalk = repo 252 | .revwalk() 253 | .into_report() 254 | .change_context(GitError::GitError) 255 | .attach_printable_lazy(|| "Failed to get revwalk".to_string()) 256 | .change_context(LoadError)?; 257 | 258 | revwalk 259 | .set_sorting(git2::Sort::TOPOLOGICAL) 260 | .into_report() 261 | .change_context(GitError::GitError) 262 | .attach_printable_lazy(|| "Failed to set revwalk sorting".to_string()) 263 | .change_context(LoadError)?; 264 | 265 | let head = repo 266 | .head() 267 | .into_report() 268 | .change_context(GitError::GitError) 269 | .attach_printable_lazy(|| { 270 | format!("Failed to get current HEAD for repository: {path:?}") 271 | }) 272 | .change_context(LoadError)?; 273 | 274 | let branch = head 275 | .peel_to_commit() 276 | .into_report() 277 | .change_context(GitError::GitError) 278 | .attach_printable_lazy(|| { 279 | format!("Failed to get current branch for repository: {path:?}") 280 | }) 281 | .change_context(LoadError)?; 282 | 283 | revwalk 284 | .push(branch.id()) 285 | .into_report() 286 | .change_context(GitError::GitError) 287 | .attach_printable_lazy(|| format!("Failed to push commit to revwalk: {path:?}")) 288 | .change_context(LoadError)?; 289 | 290 | if let Some(sha) = old_sha { 291 | let commit = repo 292 | .find_commit(sha) 293 | .into_report() 294 | .change_context(GitError::GitError) 295 | .attach_printable_lazy(|| "Failed to get commit".to_string()) 296 | .change_context(LoadError)?; 297 | 298 | revwalk 299 | .hide(commit.id()) 300 | .into_report() 301 | .change_context(GitError::GitError) 302 | .attach_printable_lazy(|| format!("Failed to push commit to revwalk {path:?}")) 303 | .change_context(LoadError)?; 304 | } 305 | 306 | let mut messages = vec![]; 307 | let mut count = 0; 308 | for id in revwalk.take(5) { 309 | let commit_id = id 310 | .into_report() 311 | .change_context(GitError::GitError) 312 | .attach_printable_lazy(|| "Failed to get commit_id".to_string()) 313 | .change_context(LoadError)?; 314 | 315 | let commit = repo 316 | .find_commit(commit_id) 317 | .into_report() 318 | .change_context(GitError::GitError) 319 | .attach_printable_lazy(|| format!("Failed to find commit {commit_id}")) 320 | .change_context(LoadError)?; 321 | messages.push(commit.message().unwrap_or("").to_string()); 322 | count += 1; 323 | if old_sha.is_none() && count == 5 { 324 | break; 325 | } 326 | } 327 | 328 | Ok(messages.iter().map(|m| m.trim().to_string()).collect()) 329 | } 330 | 331 | #[tracing::instrument] 332 | fn load(&self, input: &Self::Input, path: &Path) -> ErrorStackResult<(), LoadError> { 333 | let repo = match Repository::open(path) { 334 | Ok(repo) => repo, 335 | Err(e) => match e.code() { 336 | ErrorCode::NotFound => Repository::clone(&input.remote, path) 337 | .into_report() 338 | .change_context(GitError::GitError) 339 | .attach_printable_lazy(|| format!("Failed to clone repo: {}", input.remote)) 340 | .change_context(LoadError)?, 341 | _ => { 342 | return Err(e) 343 | .into_report() 344 | .change_context(GitError::GitError) 345 | .attach_printable_lazy(|| format!("Failed to open repo: {}", input.remote)) 346 | .change_context(LoadError) 347 | } 348 | }, 349 | }; 350 | 351 | repo.remote_anonymous(&input.remote) 352 | .into_report() 353 | .change_context(GitError::GitError) 354 | .attach_printable_lazy(|| format!("Failed to add remote: {}", input.remote)) 355 | .change_context(LoadError)?; 356 | 357 | let commit_hash = git2::Oid::from_str(&input.commit_hash) 358 | .into_report() 359 | .change_context(GitError::GitError) 360 | .attach_printable_lazy(|| format!("Failed to parse commit hash: {}", input.commit_hash)) 361 | .change_context(LoadError)?; 362 | 363 | let commit = repo 364 | .find_commit(commit_hash) 365 | .into_report() 366 | .change_context(GitError::GitError) 367 | .attach_printable_lazy(|| format!("Failed to find commit: {}", input.commit_hash)) 368 | .change_context(LoadError)?; 369 | 370 | debug!("Resetting {} to commit: {:?}", input.remote, commit); 371 | 372 | repo.reset(&commit.into_object(), git2::ResetType::Hard, None) 373 | .into_report() 374 | .change_context(GitError::GitError) 375 | .attach_printable_lazy(|| format!("Failed to reset to FETCH_HEAD: {}", input.remote)) 376 | .change_context(LoadError)?; 377 | 378 | Ok(()) 379 | } 380 | 381 | fn get_package_name(&self, name: &str) -> Option { 382 | match name.split_once(':') { 383 | Some(("git", name)) => name.rsplit_once('/').map(|(_, name)| name.to_string()), 384 | Some(("github", name)) => name.split_once('/').map(|(_, name)| name.to_string()), 385 | Some(("gitlab", name)) => name.split_once('/').map(|(_, name)| name.to_string()), 386 | Some(("srht", name)) => name.split_once('/').map(|(_, name)| name.to_string()), 387 | _ => None, 388 | } 389 | } 390 | } 391 | 392 | /// Fetches the remote repository 393 | /// 394 | /// # Errors 395 | /// Errors if the fetch fails 396 | #[tracing::instrument(skip(remote))] 397 | fn fetch_remote( 398 | url: &String, 399 | lock_type: &LockType, 400 | remote: &mut git2::Remote, 401 | ) -> ErrorStackResult<(), GitError> { 402 | let what_to_fetch = match lock_type { 403 | LockType::Tag(tag) => format!("refs/tags/{tag}:refs/tags/{tag}"), 404 | LockType::Commit(commit) => format!("refs/heads/{commit}:refs/heads/{commit}"), 405 | LockType::Branch(branch) => format!("refs/heads/{branch}:refs/heads/{branch}"), 406 | LockType::Default => { 407 | remote 408 | .connect(git2::Direction::Fetch) 409 | .into_report() 410 | .change_context(GitError::GitError) 411 | .attach_printable_lazy(|| format!("Failed to connect to remote: {url}"))?; 412 | 413 | let default_branch = remote 414 | .default_branch() 415 | .into_report() 416 | .change_context(GitError::GitError) 417 | .attach_printable_lazy(|| format!("Failed to fetch default branch: {url}"))?; 418 | 419 | let default_branch_name = default_branch 420 | .as_str() 421 | .ok_or(GitError::GitError) 422 | .into_report() 423 | .attach_printable_lazy(|| format!("Failed to find default branch: {url}"))?; 424 | 425 | format!("{default_branch_name}:{default_branch_name}") 426 | } 427 | }; 428 | 429 | remote 430 | .fetch(&[what_to_fetch.as_str()], None, None) 431 | .into_report() 432 | .change_context(GitError::GitError) 433 | .attach_printable_lazy(|| format!("Failed to fetch: {url}"))?; 434 | 435 | Ok(()) 436 | } 437 | -------------------------------------------------------------------------------- /src/smith/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod enums; 2 | mod git; 3 | pub use git::Git; 4 | use std::{ 5 | fmt::{Debug as FmtDebug, Display}, 6 | path::Path, 7 | }; 8 | 9 | use crate::package::Package; 10 | use error_stack::{Context, Result as ErrorStackResult}; 11 | 12 | #[derive(Debug)] 13 | /// An error that can occur when resolving a package 14 | pub struct ResolveError; 15 | 16 | impl Display for ResolveError { 17 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 18 | f.write_str("Failed to resolve package") 19 | } 20 | } 21 | 22 | impl Context for ResolveError {} 23 | 24 | #[derive(Debug)] 25 | /// An error that can occur when loading a package 26 | pub struct LoadError; 27 | 28 | impl Display for LoadError { 29 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 30 | f.write_str("Failed to load package") 31 | } 32 | } 33 | 34 | impl Context for LoadError {} 35 | 36 | /// A marker trait for loader inputs. 37 | /// This trait is used to allow the loader input to be serialized and deserialized. 38 | pub trait LoaderInput: FmtDebug + Send + Sync {} 39 | 40 | /// A smith that can be used to resolve and load a package. 41 | /// 42 | /// There are 2 main parts to a smith: 43 | /// 1. A resolver that can resolve a config package to a loader package, which has all the necessary information to load the package. This is cached inside of the generation file. 44 | /// 2. A loader that can download and install the package, and run the build script. 45 | pub trait Smith: FmtDebug + Send + Sync { 46 | type Input: LoaderInput; 47 | 48 | /// Gets the name of the smith 49 | fn name(&self) -> String; 50 | 51 | /// Check if this smith can load the given package. If it can, it will return the name of the package. 52 | /// This is used to find the correct smith for a package 53 | fn get_package_name(&self, name: &str) -> Option; 54 | 55 | /// Resolve a package to a loader package, which has all the necessary information to load the package. 56 | /// This is cached inside of the generation file. 57 | /// 58 | /// # Errors 59 | /// This function will return an error if the package cannot be resolved. 60 | fn resolve(&self, package: &Package) -> ErrorStackResult; 61 | 62 | /// Get latest commits for a git repo. 63 | /// 64 | /// # Errors 65 | /// This function will return an error if it cannot find the changes. 66 | fn get_change_log( 67 | &self, 68 | old_sha: Option, 69 | path: &Path, 70 | ) -> ErrorStackResult, LoadError>; 71 | 72 | /// Loads a package. 73 | /// This downloads and installs the package to the given directory. 74 | /// 75 | /// # Errors 76 | /// This function will return an error if the package cannot be loaded. 77 | fn load(&self, input: &Self::Input, package_path: &Path) -> ErrorStackResult<(), LoadError>; 78 | } 79 | --------------------------------------------------------------------------------