├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── default.nix ├── flake.lock ├── flake.nix ├── package.nix ├── release.sh ├── shell.nix └── src ├── args.rs ├── constants.rs ├── fetch_stable.rs ├── lib.rs ├── main.rs ├── queries ├── builds.rs ├── evals.rs ├── jobset.rs ├── mod.rs └── packages.rs ├── soup.rs └── structs ├── build.rs ├── eval.rs ├── icons.rs ├── inputs.rs └── mod.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Validation 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | check-version-specs: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: cachix/install-nix-action@v31 13 | - name: Check consistency of version specs 14 | run: | 15 | set -euo pipefail 16 | TAG_NAME=$(jq --raw-output .release.tag_name "$GITHUB_EVENT_PATH") 17 | PACKAGE_VERSION=$(nix eval --raw .#hydra-check.version) 18 | [[ "v$PACKAGE_VERSION" == $TAG_NAME* ]] # glob for "-g" git rev suffix 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "Test" 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | 7 | simple-build: 8 | env: 9 | CARGO_TERM_COLOR: always 10 | TERM: xterm-256color 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | # macos-13 for x86_64-darwin 15 | os: [ubuntu-latest, macos-13, macos-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: cachix/install-nix-action@v31 20 | with: 21 | nix_path: nixpkgs=channel:nixos-unstable 22 | - run: nix-build 23 | 24 | flakes: 25 | env: 26 | CARGO_TERM_COLOR: always 27 | TERM: xterm-256color 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | os: [ubuntu-latest, macos-13, macos-latest] 32 | runs-on: ${{ matrix.os }} 33 | steps: 34 | - uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 0 # fetch everything for `git describe` 37 | filter: 'blob:none' # fetch blobs on demand 38 | - uses: cachix/install-nix-action@v31 39 | with: 40 | extra_nix_config: | 41 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 42 | - run: nix eval --raw .#hydra-check.version 43 | - run: nix build --print-build-logs 44 | - run: nix flake check 45 | - run: nix develop -c cargo clippy 46 | - run: nix develop -c cargo fmt --check --all 47 | - run: nix develop -c cargo test --all-features -- --color=always --ignored # run only ignored tests 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # configured by each user 2 | .envrc 3 | .vscode 4 | 5 | secrets.py 6 | runs 7 | .ipynb_* 8 | *.csv 9 | .direnv 10 | /.mypy_cache/ 11 | /*.egg-info/ 12 | /.direnv/ 13 | 14 | # Nix 15 | /**/result* 16 | 17 | # Byte-compiled / optimized / DLL files 18 | __pycache__/ 19 | *.py[cod] 20 | *$py.class 21 | 22 | # C extensions 23 | *.so 24 | 25 | # Distribution / packaging 26 | .Python 27 | build/ 28 | develop-eggs/ 29 | dist/ 30 | downloads/ 31 | eggs/ 32 | .eggs/ 33 | lib/ 34 | lib64/ 35 | parts/ 36 | sdist/ 37 | var/ 38 | wheels/ 39 | share/python-wheels/ 40 | *.egg-info/ 41 | .installed.cfg 42 | *.egg 43 | MANIFEST 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .nox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *.cover 65 | *.py,cover 66 | .hypothesis/ 67 | .pytest_cache/ 68 | cover/ 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Django stuff: 75 | *.log 76 | local_settings.py 77 | db.sqlite3 78 | db.sqlite3-journal 79 | 80 | # Flask stuff: 81 | instance/ 82 | .webassets-cache 83 | 84 | # Scrapy stuff: 85 | .scrapy 86 | 87 | # Sphinx documentation 88 | docs/_build/ 89 | 90 | # PyBuilder 91 | .pybuilder/ 92 | target/ 93 | 94 | # Jupyter Notebook 95 | .ipynb_checkpoints 96 | 97 | # IPython 98 | profile_default/ 99 | ipython_config.py 100 | 101 | # pyenv 102 | # For a library or package, you might want to ignore these files since the code is 103 | # intended to run in multiple environments; otherwise, check them in: 104 | # .python-version 105 | 106 | # pipenv 107 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 108 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 109 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 110 | # install all needed dependencies. 111 | #Pipfile.lock 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "getrandom", 28 | "once_cell", 29 | "version_check", 30 | "zerocopy", 31 | ] 32 | 33 | [[package]] 34 | name = "aho-corasick" 35 | version = "1.1.3" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 38 | dependencies = [ 39 | "memchr", 40 | ] 41 | 42 | [[package]] 43 | name = "android-tzdata" 44 | version = "0.1.1" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 47 | 48 | [[package]] 49 | name = "android_system_properties" 50 | version = "0.1.5" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 53 | dependencies = [ 54 | "libc", 55 | ] 56 | 57 | [[package]] 58 | name = "ansi-str" 59 | version = "0.8.0" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "1cf4578926a981ab0ca955dc023541d19de37112bc24c1a197bd806d3d86ad1d" 62 | dependencies = [ 63 | "ansitok", 64 | ] 65 | 66 | [[package]] 67 | name = "ansitok" 68 | version = "0.2.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "220044e6a1bb31ddee4e3db724d29767f352de47445a6cd75e1a173142136c83" 71 | dependencies = [ 72 | "nom", 73 | "vte", 74 | ] 75 | 76 | [[package]] 77 | name = "anstream" 78 | version = "0.6.15" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 81 | dependencies = [ 82 | "anstyle", 83 | "anstyle-parse", 84 | "anstyle-query", 85 | "anstyle-wincon", 86 | "colorchoice", 87 | "is_terminal_polyfill", 88 | "utf8parse", 89 | ] 90 | 91 | [[package]] 92 | name = "anstyle" 93 | version = "1.0.8" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 96 | 97 | [[package]] 98 | name = "anstyle-parse" 99 | version = "0.2.5" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 102 | dependencies = [ 103 | "utf8parse", 104 | ] 105 | 106 | [[package]] 107 | name = "anstyle-query" 108 | version = "1.1.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 111 | dependencies = [ 112 | "windows-sys 0.52.0", 113 | ] 114 | 115 | [[package]] 116 | name = "anstyle-wincon" 117 | version = "3.0.4" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 120 | dependencies = [ 121 | "anstyle", 122 | "windows-sys 0.52.0", 123 | ] 124 | 125 | [[package]] 126 | name = "anyhow" 127 | version = "1.0.90" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" 130 | 131 | [[package]] 132 | name = "arrayvec" 133 | version = "0.5.2" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 136 | 137 | [[package]] 138 | name = "async-compression" 139 | version = "0.4.17" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" 142 | dependencies = [ 143 | "flate2", 144 | "futures-core", 145 | "memchr", 146 | "pin-project-lite", 147 | "tokio", 148 | ] 149 | 150 | [[package]] 151 | name = "atomic-waker" 152 | version = "1.1.2" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 155 | 156 | [[package]] 157 | name = "autocfg" 158 | version = "1.4.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 161 | 162 | [[package]] 163 | name = "backtrace" 164 | version = "0.3.74" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 167 | dependencies = [ 168 | "addr2line", 169 | "cfg-if", 170 | "libc", 171 | "miniz_oxide", 172 | "object", 173 | "rustc-demangle", 174 | "windows-targets 0.52.6", 175 | ] 176 | 177 | [[package]] 178 | name = "base64" 179 | version = "0.22.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 182 | 183 | [[package]] 184 | name = "bitflags" 185 | version = "2.6.0" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 188 | 189 | [[package]] 190 | name = "bumpalo" 191 | version = "3.16.0" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 194 | 195 | [[package]] 196 | name = "byteorder" 197 | version = "1.5.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 200 | 201 | [[package]] 202 | name = "bytes" 203 | version = "1.10.1" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 206 | 207 | [[package]] 208 | name = "cc" 209 | version = "1.2.16" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" 212 | dependencies = [ 213 | "shlex", 214 | ] 215 | 216 | [[package]] 217 | name = "cfg-if" 218 | version = "1.0.0" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 221 | 222 | [[package]] 223 | name = "chrono" 224 | version = "0.4.38" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 227 | dependencies = [ 228 | "android-tzdata", 229 | "iana-time-zone", 230 | "num-traits", 231 | "windows-targets 0.52.6", 232 | ] 233 | 234 | [[package]] 235 | name = "clap" 236 | version = "4.5.20" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" 239 | dependencies = [ 240 | "clap_builder", 241 | "clap_derive", 242 | ] 243 | 244 | [[package]] 245 | name = "clap_builder" 246 | version = "4.5.20" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" 249 | dependencies = [ 250 | "anstream", 251 | "anstyle", 252 | "clap_lex", 253 | "strsim", 254 | ] 255 | 256 | [[package]] 257 | name = "clap_complete" 258 | version = "4.5.37" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "11611dca53440593f38e6b25ec629de50b14cdfa63adc0fb856115a2c6d97595" 261 | dependencies = [ 262 | "clap", 263 | ] 264 | 265 | [[package]] 266 | name = "clap_derive" 267 | version = "4.5.18" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 270 | dependencies = [ 271 | "heck", 272 | "proc-macro2", 273 | "quote", 274 | "syn", 275 | ] 276 | 277 | [[package]] 278 | name = "clap_lex" 279 | version = "0.7.2" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 282 | 283 | [[package]] 284 | name = "colorchoice" 285 | version = "1.0.2" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 288 | 289 | [[package]] 290 | name = "colored" 291 | version = "2.1.0" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" 294 | dependencies = [ 295 | "lazy_static", 296 | "windows-sys 0.48.0", 297 | ] 298 | 299 | [[package]] 300 | name = "comfy-table" 301 | version = "7.1.1" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" 304 | dependencies = [ 305 | "ansi-str", 306 | "console", 307 | "crossterm", 308 | "strum", 309 | "strum_macros", 310 | "unicode-width", 311 | ] 312 | 313 | [[package]] 314 | name = "console" 315 | version = "0.15.8" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" 318 | dependencies = [ 319 | "encode_unicode", 320 | "lazy_static", 321 | "libc", 322 | "unicode-width", 323 | "windows-sys 0.52.0", 324 | ] 325 | 326 | [[package]] 327 | name = "cookie" 328 | version = "0.18.1" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" 331 | dependencies = [ 332 | "percent-encoding", 333 | "time", 334 | "version_check", 335 | ] 336 | 337 | [[package]] 338 | name = "cookie_store" 339 | version = "0.21.1" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" 342 | dependencies = [ 343 | "cookie", 344 | "document-features", 345 | "idna", 346 | "log", 347 | "publicsuffix", 348 | "serde", 349 | "serde_derive", 350 | "serde_json", 351 | "time", 352 | "url", 353 | ] 354 | 355 | [[package]] 356 | name = "core-foundation" 357 | version = "0.9.4" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 360 | dependencies = [ 361 | "core-foundation-sys", 362 | "libc", 363 | ] 364 | 365 | [[package]] 366 | name = "core-foundation-sys" 367 | version = "0.8.7" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 370 | 371 | [[package]] 372 | name = "crc32fast" 373 | version = "1.4.2" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 376 | dependencies = [ 377 | "cfg-if", 378 | ] 379 | 380 | [[package]] 381 | name = "crossterm" 382 | version = "0.27.0" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 385 | dependencies = [ 386 | "bitflags", 387 | "crossterm_winapi", 388 | "libc", 389 | "parking_lot", 390 | "winapi", 391 | ] 392 | 393 | [[package]] 394 | name = "crossterm_winapi" 395 | version = "0.9.1" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 398 | dependencies = [ 399 | "winapi", 400 | ] 401 | 402 | [[package]] 403 | name = "cssparser" 404 | version = "0.31.2" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" 407 | dependencies = [ 408 | "cssparser-macros", 409 | "dtoa-short", 410 | "itoa", 411 | "phf 0.11.2", 412 | "smallvec", 413 | ] 414 | 415 | [[package]] 416 | name = "cssparser-macros" 417 | version = "0.6.1" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" 420 | dependencies = [ 421 | "quote", 422 | "syn", 423 | ] 424 | 425 | [[package]] 426 | name = "darling" 427 | version = "0.20.10" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 430 | dependencies = [ 431 | "darling_core", 432 | "darling_macro", 433 | ] 434 | 435 | [[package]] 436 | name = "darling_core" 437 | version = "0.20.10" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 440 | dependencies = [ 441 | "fnv", 442 | "ident_case", 443 | "proc-macro2", 444 | "quote", 445 | "strsim", 446 | "syn", 447 | ] 448 | 449 | [[package]] 450 | name = "darling_macro" 451 | version = "0.20.10" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 454 | dependencies = [ 455 | "darling_core", 456 | "quote", 457 | "syn", 458 | ] 459 | 460 | [[package]] 461 | name = "deranged" 462 | version = "0.3.11" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 465 | dependencies = [ 466 | "powerfmt", 467 | ] 468 | 469 | [[package]] 470 | name = "derive_more" 471 | version = "0.99.18" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" 474 | dependencies = [ 475 | "proc-macro2", 476 | "quote", 477 | "syn", 478 | ] 479 | 480 | [[package]] 481 | name = "document-features" 482 | version = "0.2.10" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" 485 | dependencies = [ 486 | "litrs", 487 | ] 488 | 489 | [[package]] 490 | name = "dtoa" 491 | version = "1.0.9" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" 494 | 495 | [[package]] 496 | name = "dtoa-short" 497 | version = "0.3.5" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" 500 | dependencies = [ 501 | "dtoa", 502 | ] 503 | 504 | [[package]] 505 | name = "ego-tree" 506 | version = "0.6.3" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "12a0bb14ac04a9fcf170d0bbbef949b44cc492f4452bd20c095636956f653642" 509 | 510 | [[package]] 511 | name = "encode_unicode" 512 | version = "0.3.6" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 515 | 516 | [[package]] 517 | name = "encoding_rs" 518 | version = "0.8.34" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" 521 | dependencies = [ 522 | "cfg-if", 523 | ] 524 | 525 | [[package]] 526 | name = "equivalent" 527 | version = "1.0.1" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 530 | 531 | [[package]] 532 | name = "errno" 533 | version = "0.3.9" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 536 | dependencies = [ 537 | "libc", 538 | "windows-sys 0.52.0", 539 | ] 540 | 541 | [[package]] 542 | name = "fastrand" 543 | version = "2.1.1" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 546 | 547 | [[package]] 548 | name = "flate2" 549 | version = "1.0.34" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" 552 | dependencies = [ 553 | "crc32fast", 554 | "miniz_oxide", 555 | ] 556 | 557 | [[package]] 558 | name = "flexi_logger" 559 | version = "0.29.3" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "719236bdbcf6033a3395165f797076b31056018e6723ccff616eb25fc9c99de1" 562 | dependencies = [ 563 | "chrono", 564 | "log", 565 | "nu-ansi-term", 566 | "regex", 567 | "thiserror", 568 | ] 569 | 570 | [[package]] 571 | name = "fnv" 572 | version = "1.0.7" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 575 | 576 | [[package]] 577 | name = "foreign-types" 578 | version = "0.3.2" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 581 | dependencies = [ 582 | "foreign-types-shared", 583 | ] 584 | 585 | [[package]] 586 | name = "foreign-types-shared" 587 | version = "0.1.1" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 590 | 591 | [[package]] 592 | name = "form_urlencoded" 593 | version = "1.2.1" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 596 | dependencies = [ 597 | "percent-encoding", 598 | ] 599 | 600 | [[package]] 601 | name = "futf" 602 | version = "0.1.5" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" 605 | dependencies = [ 606 | "mac", 607 | "new_debug_unreachable", 608 | ] 609 | 610 | [[package]] 611 | name = "futures-channel" 612 | version = "0.3.31" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 615 | dependencies = [ 616 | "futures-core", 617 | "futures-sink", 618 | ] 619 | 620 | [[package]] 621 | name = "futures-core" 622 | version = "0.3.31" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 625 | 626 | [[package]] 627 | name = "futures-io" 628 | version = "0.3.31" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 631 | 632 | [[package]] 633 | name = "futures-sink" 634 | version = "0.3.31" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 637 | 638 | [[package]] 639 | name = "futures-task" 640 | version = "0.3.31" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 643 | 644 | [[package]] 645 | name = "futures-util" 646 | version = "0.3.31" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 649 | dependencies = [ 650 | "futures-core", 651 | "futures-io", 652 | "futures-sink", 653 | "futures-task", 654 | "memchr", 655 | "pin-project-lite", 656 | "pin-utils", 657 | "slab", 658 | ] 659 | 660 | [[package]] 661 | name = "fxhash" 662 | version = "0.2.1" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 665 | dependencies = [ 666 | "byteorder", 667 | ] 668 | 669 | [[package]] 670 | name = "getopts" 671 | version = "0.2.21" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" 674 | dependencies = [ 675 | "unicode-width", 676 | ] 677 | 678 | [[package]] 679 | name = "getrandom" 680 | version = "0.2.15" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 683 | dependencies = [ 684 | "cfg-if", 685 | "libc", 686 | "wasi", 687 | ] 688 | 689 | [[package]] 690 | name = "gimli" 691 | version = "0.31.1" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 694 | 695 | [[package]] 696 | name = "h2" 697 | version = "0.4.6" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" 700 | dependencies = [ 701 | "atomic-waker", 702 | "bytes", 703 | "fnv", 704 | "futures-core", 705 | "futures-sink", 706 | "http", 707 | "indexmap", 708 | "slab", 709 | "tokio", 710 | "tokio-util", 711 | "tracing", 712 | ] 713 | 714 | [[package]] 715 | name = "hashbrown" 716 | version = "0.15.2" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 719 | 720 | [[package]] 721 | name = "heck" 722 | version = "0.5.0" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 725 | 726 | [[package]] 727 | name = "html5ever" 728 | version = "0.27.0" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" 731 | dependencies = [ 732 | "log", 733 | "mac", 734 | "markup5ever", 735 | "proc-macro2", 736 | "quote", 737 | "syn", 738 | ] 739 | 740 | [[package]] 741 | name = "http" 742 | version = "1.1.0" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 745 | dependencies = [ 746 | "bytes", 747 | "fnv", 748 | "itoa", 749 | ] 750 | 751 | [[package]] 752 | name = "http-body" 753 | version = "1.0.1" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 756 | dependencies = [ 757 | "bytes", 758 | "http", 759 | ] 760 | 761 | [[package]] 762 | name = "http-body-util" 763 | version = "0.1.2" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 766 | dependencies = [ 767 | "bytes", 768 | "futures-util", 769 | "http", 770 | "http-body", 771 | "pin-project-lite", 772 | ] 773 | 774 | [[package]] 775 | name = "httparse" 776 | version = "1.9.5" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 779 | 780 | [[package]] 781 | name = "hydra-check" 782 | version = "2.0.4" 783 | dependencies = [ 784 | "anyhow", 785 | "clap", 786 | "clap_complete", 787 | "colored", 788 | "comfy-table", 789 | "flexi_logger", 790 | "indexmap", 791 | "insta", 792 | "log", 793 | "regex", 794 | "reqwest", 795 | "scraper", 796 | "serde", 797 | "serde_json", 798 | "serde_with", 799 | ] 800 | 801 | [[package]] 802 | name = "hyper" 803 | version = "1.5.0" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" 806 | dependencies = [ 807 | "bytes", 808 | "futures-channel", 809 | "futures-util", 810 | "h2", 811 | "http", 812 | "http-body", 813 | "httparse", 814 | "itoa", 815 | "pin-project-lite", 816 | "smallvec", 817 | "tokio", 818 | "want", 819 | ] 820 | 821 | [[package]] 822 | name = "hyper-rustls" 823 | version = "0.27.3" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" 826 | dependencies = [ 827 | "futures-util", 828 | "http", 829 | "hyper", 830 | "hyper-util", 831 | "rustls", 832 | "rustls-pki-types", 833 | "tokio", 834 | "tokio-rustls", 835 | "tower-service", 836 | ] 837 | 838 | [[package]] 839 | name = "hyper-tls" 840 | version = "0.6.0" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 843 | dependencies = [ 844 | "bytes", 845 | "http-body-util", 846 | "hyper", 847 | "hyper-util", 848 | "native-tls", 849 | "tokio", 850 | "tokio-native-tls", 851 | "tower-service", 852 | ] 853 | 854 | [[package]] 855 | name = "hyper-util" 856 | version = "0.1.9" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" 859 | dependencies = [ 860 | "bytes", 861 | "futures-channel", 862 | "futures-util", 863 | "http", 864 | "http-body", 865 | "hyper", 866 | "pin-project-lite", 867 | "socket2", 868 | "tokio", 869 | "tower-service", 870 | "tracing", 871 | ] 872 | 873 | [[package]] 874 | name = "iana-time-zone" 875 | version = "0.1.61" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 878 | dependencies = [ 879 | "android_system_properties", 880 | "core-foundation-sys", 881 | "iana-time-zone-haiku", 882 | "js-sys", 883 | "wasm-bindgen", 884 | "windows-core", 885 | ] 886 | 887 | [[package]] 888 | name = "iana-time-zone-haiku" 889 | version = "0.1.2" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 892 | dependencies = [ 893 | "cc", 894 | ] 895 | 896 | [[package]] 897 | name = "ident_case" 898 | version = "1.0.1" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 901 | 902 | [[package]] 903 | name = "idna" 904 | version = "1.0.3" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 907 | dependencies = [ 908 | "idna_adapter", 909 | "smallvec", 910 | "utf8_iter", 911 | ] 912 | 913 | [[package]] 914 | name = "idna_adapter" 915 | version = "1.1.0" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "279259b0ac81c89d11c290495fdcfa96ea3643b7df311c138b6fe8ca5237f0f8" 918 | dependencies = [ 919 | "idna_mapping", 920 | "unicode-bidi", 921 | "unicode-normalization", 922 | ] 923 | 924 | [[package]] 925 | name = "idna_mapping" 926 | version = "1.0.0" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "d5422cc5bc64289a77dbb45e970b86b5e9a04cb500abc7240505aedc1bf40f38" 929 | dependencies = [ 930 | "unicode-joining-type", 931 | ] 932 | 933 | [[package]] 934 | name = "indexmap" 935 | version = "2.7.0" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 938 | dependencies = [ 939 | "equivalent", 940 | "hashbrown", 941 | "serde", 942 | ] 943 | 944 | [[package]] 945 | name = "insta" 946 | version = "1.41.1" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" 949 | dependencies = [ 950 | "console", 951 | "lazy_static", 952 | "linked-hash-map", 953 | "similar", 954 | ] 955 | 956 | [[package]] 957 | name = "ipnet" 958 | version = "2.10.1" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" 961 | 962 | [[package]] 963 | name = "is_terminal_polyfill" 964 | version = "1.70.1" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 967 | 968 | [[package]] 969 | name = "itoa" 970 | version = "1.0.14" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 973 | 974 | [[package]] 975 | name = "js-sys" 976 | version = "0.3.72" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" 979 | dependencies = [ 980 | "wasm-bindgen", 981 | ] 982 | 983 | [[package]] 984 | name = "lazy_static" 985 | version = "1.5.0" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 988 | 989 | [[package]] 990 | name = "libc" 991 | version = "0.2.172" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 994 | 995 | [[package]] 996 | name = "linked-hash-map" 997 | version = "0.5.6" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 1000 | 1001 | [[package]] 1002 | name = "linux-raw-sys" 1003 | version = "0.4.14" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 1006 | 1007 | [[package]] 1008 | name = "litrs" 1009 | version = "0.4.1" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 1012 | 1013 | [[package]] 1014 | name = "lock_api" 1015 | version = "0.4.12" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 1018 | dependencies = [ 1019 | "autocfg", 1020 | "scopeguard", 1021 | ] 1022 | 1023 | [[package]] 1024 | name = "log" 1025 | version = "0.4.22" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 1028 | 1029 | [[package]] 1030 | name = "mac" 1031 | version = "0.1.1" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 1034 | 1035 | [[package]] 1036 | name = "markup5ever" 1037 | version = "0.12.1" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" 1040 | dependencies = [ 1041 | "log", 1042 | "phf 0.11.2", 1043 | "phf_codegen 0.11.2", 1044 | "string_cache", 1045 | "string_cache_codegen", 1046 | "tendril", 1047 | ] 1048 | 1049 | [[package]] 1050 | name = "memchr" 1051 | version = "2.7.4" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 1054 | 1055 | [[package]] 1056 | name = "mime" 1057 | version = "0.3.17" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1060 | 1061 | [[package]] 1062 | name = "minimal-lexical" 1063 | version = "0.2.1" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1066 | 1067 | [[package]] 1068 | name = "miniz_oxide" 1069 | version = "0.8.8" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 1072 | dependencies = [ 1073 | "adler2", 1074 | ] 1075 | 1076 | [[package]] 1077 | name = "mio" 1078 | version = "1.0.3" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 1081 | dependencies = [ 1082 | "libc", 1083 | "wasi", 1084 | "windows-sys 0.52.0", 1085 | ] 1086 | 1087 | [[package]] 1088 | name = "native-tls" 1089 | version = "0.2.12" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" 1092 | dependencies = [ 1093 | "libc", 1094 | "log", 1095 | "openssl", 1096 | "openssl-probe", 1097 | "openssl-sys", 1098 | "schannel", 1099 | "security-framework", 1100 | "security-framework-sys", 1101 | "tempfile", 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "new_debug_unreachable" 1106 | version = "1.0.6" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 1109 | 1110 | [[package]] 1111 | name = "nom" 1112 | version = "7.1.3" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1115 | dependencies = [ 1116 | "memchr", 1117 | "minimal-lexical", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "nu-ansi-term" 1122 | version = "0.50.1" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" 1125 | dependencies = [ 1126 | "windows-sys 0.52.0", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "num-conv" 1131 | version = "0.1.0" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1134 | 1135 | [[package]] 1136 | name = "num-traits" 1137 | version = "0.2.19" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1140 | dependencies = [ 1141 | "autocfg", 1142 | ] 1143 | 1144 | [[package]] 1145 | name = "object" 1146 | version = "0.36.7" 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" 1148 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1149 | dependencies = [ 1150 | "memchr", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "once_cell" 1155 | version = "1.20.2" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 1158 | 1159 | [[package]] 1160 | name = "openssl" 1161 | version = "0.10.72" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" 1164 | dependencies = [ 1165 | "bitflags", 1166 | "cfg-if", 1167 | "foreign-types", 1168 | "libc", 1169 | "once_cell", 1170 | "openssl-macros", 1171 | "openssl-sys", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "openssl-macros" 1176 | version = "0.1.1" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1179 | dependencies = [ 1180 | "proc-macro2", 1181 | "quote", 1182 | "syn", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "openssl-probe" 1187 | version = "0.1.5" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 1190 | 1191 | [[package]] 1192 | name = "openssl-sys" 1193 | version = "0.9.107" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" 1196 | dependencies = [ 1197 | "cc", 1198 | "libc", 1199 | "pkg-config", 1200 | "vcpkg", 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "parking_lot" 1205 | version = "0.12.3" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1208 | dependencies = [ 1209 | "lock_api", 1210 | "parking_lot_core", 1211 | ] 1212 | 1213 | [[package]] 1214 | name = "parking_lot_core" 1215 | version = "0.9.10" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1218 | dependencies = [ 1219 | "cfg-if", 1220 | "libc", 1221 | "redox_syscall", 1222 | "smallvec", 1223 | "windows-targets 0.52.6", 1224 | ] 1225 | 1226 | [[package]] 1227 | name = "percent-encoding" 1228 | version = "2.3.1" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1231 | 1232 | [[package]] 1233 | name = "phf" 1234 | version = "0.10.1" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" 1237 | dependencies = [ 1238 | "phf_shared 0.10.0", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "phf" 1243 | version = "0.11.2" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" 1246 | dependencies = [ 1247 | "phf_macros", 1248 | "phf_shared 0.11.2", 1249 | ] 1250 | 1251 | [[package]] 1252 | name = "phf_codegen" 1253 | version = "0.10.0" 1254 | source = "registry+https://github.com/rust-lang/crates.io-index" 1255 | checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" 1256 | dependencies = [ 1257 | "phf_generator 0.10.0", 1258 | "phf_shared 0.10.0", 1259 | ] 1260 | 1261 | [[package]] 1262 | name = "phf_codegen" 1263 | version = "0.11.2" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" 1266 | dependencies = [ 1267 | "phf_generator 0.11.2", 1268 | "phf_shared 0.11.2", 1269 | ] 1270 | 1271 | [[package]] 1272 | name = "phf_generator" 1273 | version = "0.10.0" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" 1276 | dependencies = [ 1277 | "phf_shared 0.10.0", 1278 | "rand", 1279 | ] 1280 | 1281 | [[package]] 1282 | name = "phf_generator" 1283 | version = "0.11.2" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" 1286 | dependencies = [ 1287 | "phf_shared 0.11.2", 1288 | "rand", 1289 | ] 1290 | 1291 | [[package]] 1292 | name = "phf_macros" 1293 | version = "0.11.2" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" 1296 | dependencies = [ 1297 | "phf_generator 0.11.2", 1298 | "phf_shared 0.11.2", 1299 | "proc-macro2", 1300 | "quote", 1301 | "syn", 1302 | ] 1303 | 1304 | [[package]] 1305 | name = "phf_shared" 1306 | version = "0.10.0" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" 1309 | dependencies = [ 1310 | "siphasher", 1311 | ] 1312 | 1313 | [[package]] 1314 | name = "phf_shared" 1315 | version = "0.11.2" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" 1318 | dependencies = [ 1319 | "siphasher", 1320 | ] 1321 | 1322 | [[package]] 1323 | name = "pin-project-lite" 1324 | version = "0.2.16" 1325 | source = "registry+https://github.com/rust-lang/crates.io-index" 1326 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1327 | 1328 | [[package]] 1329 | name = "pin-utils" 1330 | version = "0.1.0" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1333 | 1334 | [[package]] 1335 | name = "pkg-config" 1336 | version = "0.3.31" 1337 | source = "registry+https://github.com/rust-lang/crates.io-index" 1338 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 1339 | 1340 | [[package]] 1341 | name = "powerfmt" 1342 | version = "0.2.0" 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" 1344 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1345 | 1346 | [[package]] 1347 | name = "ppv-lite86" 1348 | version = "0.2.20" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 1351 | dependencies = [ 1352 | "zerocopy", 1353 | ] 1354 | 1355 | [[package]] 1356 | name = "precomputed-hash" 1357 | version = "0.1.1" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 1360 | 1361 | [[package]] 1362 | name = "proc-macro2" 1363 | version = "1.0.92" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 1366 | dependencies = [ 1367 | "unicode-ident", 1368 | ] 1369 | 1370 | [[package]] 1371 | name = "psl-types" 1372 | version = "2.0.11" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" 1375 | 1376 | [[package]] 1377 | name = "publicsuffix" 1378 | version = "2.3.0" 1379 | source = "registry+https://github.com/rust-lang/crates.io-index" 1380 | checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" 1381 | dependencies = [ 1382 | "idna", 1383 | "psl-types", 1384 | ] 1385 | 1386 | [[package]] 1387 | name = "quote" 1388 | version = "1.0.37" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 1391 | dependencies = [ 1392 | "proc-macro2", 1393 | ] 1394 | 1395 | [[package]] 1396 | name = "rand" 1397 | version = "0.8.5" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1400 | dependencies = [ 1401 | "libc", 1402 | "rand_chacha", 1403 | "rand_core", 1404 | ] 1405 | 1406 | [[package]] 1407 | name = "rand_chacha" 1408 | version = "0.3.1" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1411 | dependencies = [ 1412 | "ppv-lite86", 1413 | "rand_core", 1414 | ] 1415 | 1416 | [[package]] 1417 | name = "rand_core" 1418 | version = "0.6.4" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1421 | dependencies = [ 1422 | "getrandom", 1423 | ] 1424 | 1425 | [[package]] 1426 | name = "redox_syscall" 1427 | version = "0.5.7" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 1430 | dependencies = [ 1431 | "bitflags", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "regex" 1436 | version = "1.11.0" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" 1439 | dependencies = [ 1440 | "aho-corasick", 1441 | "memchr", 1442 | "regex-automata", 1443 | "regex-syntax", 1444 | ] 1445 | 1446 | [[package]] 1447 | name = "regex-automata" 1448 | version = "0.4.8" 1449 | source = "registry+https://github.com/rust-lang/crates.io-index" 1450 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" 1451 | dependencies = [ 1452 | "aho-corasick", 1453 | "memchr", 1454 | "regex-syntax", 1455 | ] 1456 | 1457 | [[package]] 1458 | name = "regex-syntax" 1459 | version = "0.8.5" 1460 | source = "registry+https://github.com/rust-lang/crates.io-index" 1461 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1462 | 1463 | [[package]] 1464 | name = "reqwest" 1465 | version = "0.12.8" 1466 | source = "registry+https://github.com/rust-lang/crates.io-index" 1467 | checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" 1468 | dependencies = [ 1469 | "async-compression", 1470 | "base64", 1471 | "bytes", 1472 | "cookie", 1473 | "cookie_store", 1474 | "encoding_rs", 1475 | "futures-channel", 1476 | "futures-core", 1477 | "futures-util", 1478 | "h2", 1479 | "http", 1480 | "http-body", 1481 | "http-body-util", 1482 | "hyper", 1483 | "hyper-rustls", 1484 | "hyper-tls", 1485 | "hyper-util", 1486 | "ipnet", 1487 | "js-sys", 1488 | "log", 1489 | "mime", 1490 | "native-tls", 1491 | "once_cell", 1492 | "percent-encoding", 1493 | "pin-project-lite", 1494 | "rustls-pemfile", 1495 | "serde", 1496 | "serde_json", 1497 | "serde_urlencoded", 1498 | "sync_wrapper", 1499 | "system-configuration", 1500 | "tokio", 1501 | "tokio-native-tls", 1502 | "tokio-util", 1503 | "tower-service", 1504 | "url", 1505 | "wasm-bindgen", 1506 | "wasm-bindgen-futures", 1507 | "web-sys", 1508 | "windows-registry", 1509 | ] 1510 | 1511 | [[package]] 1512 | name = "ring" 1513 | version = "0.17.14" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1516 | dependencies = [ 1517 | "cc", 1518 | "cfg-if", 1519 | "getrandom", 1520 | "libc", 1521 | "untrusted", 1522 | "windows-sys 0.52.0", 1523 | ] 1524 | 1525 | [[package]] 1526 | name = "rustc-demangle" 1527 | version = "0.1.24" 1528 | source = "registry+https://github.com/rust-lang/crates.io-index" 1529 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1530 | 1531 | [[package]] 1532 | name = "rustix" 1533 | version = "0.38.37" 1534 | source = "registry+https://github.com/rust-lang/crates.io-index" 1535 | checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" 1536 | dependencies = [ 1537 | "bitflags", 1538 | "errno", 1539 | "libc", 1540 | "linux-raw-sys", 1541 | "windows-sys 0.52.0", 1542 | ] 1543 | 1544 | [[package]] 1545 | name = "rustls" 1546 | version = "0.23.19" 1547 | source = "registry+https://github.com/rust-lang/crates.io-index" 1548 | checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" 1549 | dependencies = [ 1550 | "once_cell", 1551 | "rustls-pki-types", 1552 | "rustls-webpki", 1553 | "subtle", 1554 | "zeroize", 1555 | ] 1556 | 1557 | [[package]] 1558 | name = "rustls-pemfile" 1559 | version = "2.2.0" 1560 | source = "registry+https://github.com/rust-lang/crates.io-index" 1561 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 1562 | dependencies = [ 1563 | "rustls-pki-types", 1564 | ] 1565 | 1566 | [[package]] 1567 | name = "rustls-pki-types" 1568 | version = "1.10.0" 1569 | source = "registry+https://github.com/rust-lang/crates.io-index" 1570 | checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" 1571 | 1572 | [[package]] 1573 | name = "rustls-webpki" 1574 | version = "0.102.8" 1575 | source = "registry+https://github.com/rust-lang/crates.io-index" 1576 | checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" 1577 | dependencies = [ 1578 | "ring", 1579 | "rustls-pki-types", 1580 | "untrusted", 1581 | ] 1582 | 1583 | [[package]] 1584 | name = "rustversion" 1585 | version = "1.0.18" 1586 | source = "registry+https://github.com/rust-lang/crates.io-index" 1587 | checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" 1588 | 1589 | [[package]] 1590 | name = "ryu" 1591 | version = "1.0.18" 1592 | source = "registry+https://github.com/rust-lang/crates.io-index" 1593 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1594 | 1595 | [[package]] 1596 | name = "schannel" 1597 | version = "0.1.26" 1598 | source = "registry+https://github.com/rust-lang/crates.io-index" 1599 | checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" 1600 | dependencies = [ 1601 | "windows-sys 0.59.0", 1602 | ] 1603 | 1604 | [[package]] 1605 | name = "scopeguard" 1606 | version = "1.2.0" 1607 | source = "registry+https://github.com/rust-lang/crates.io-index" 1608 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1609 | 1610 | [[package]] 1611 | name = "scraper" 1612 | version = "0.20.0" 1613 | source = "registry+https://github.com/rust-lang/crates.io-index" 1614 | checksum = "b90460b31bfe1fc07be8262e42c665ad97118d4585869de9345a84d501a9eaf0" 1615 | dependencies = [ 1616 | "ahash", 1617 | "cssparser", 1618 | "ego-tree", 1619 | "getopts", 1620 | "html5ever", 1621 | "once_cell", 1622 | "selectors", 1623 | "tendril", 1624 | ] 1625 | 1626 | [[package]] 1627 | name = "security-framework" 1628 | version = "2.11.1" 1629 | source = "registry+https://github.com/rust-lang/crates.io-index" 1630 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1631 | dependencies = [ 1632 | "bitflags", 1633 | "core-foundation", 1634 | "core-foundation-sys", 1635 | "libc", 1636 | "security-framework-sys", 1637 | ] 1638 | 1639 | [[package]] 1640 | name = "security-framework-sys" 1641 | version = "2.12.0" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" 1644 | dependencies = [ 1645 | "core-foundation-sys", 1646 | "libc", 1647 | ] 1648 | 1649 | [[package]] 1650 | name = "selectors" 1651 | version = "0.25.0" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" 1654 | dependencies = [ 1655 | "bitflags", 1656 | "cssparser", 1657 | "derive_more", 1658 | "fxhash", 1659 | "log", 1660 | "new_debug_unreachable", 1661 | "phf 0.10.1", 1662 | "phf_codegen 0.10.0", 1663 | "precomputed-hash", 1664 | "servo_arc", 1665 | "smallvec", 1666 | ] 1667 | 1668 | [[package]] 1669 | name = "serde" 1670 | version = "1.0.216" 1671 | source = "registry+https://github.com/rust-lang/crates.io-index" 1672 | checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" 1673 | dependencies = [ 1674 | "serde_derive", 1675 | ] 1676 | 1677 | [[package]] 1678 | name = "serde_derive" 1679 | version = "1.0.216" 1680 | source = "registry+https://github.com/rust-lang/crates.io-index" 1681 | checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" 1682 | dependencies = [ 1683 | "proc-macro2", 1684 | "quote", 1685 | "syn", 1686 | ] 1687 | 1688 | [[package]] 1689 | name = "serde_json" 1690 | version = "1.0.133" 1691 | source = "registry+https://github.com/rust-lang/crates.io-index" 1692 | checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" 1693 | dependencies = [ 1694 | "itoa", 1695 | "memchr", 1696 | "ryu", 1697 | "serde", 1698 | ] 1699 | 1700 | [[package]] 1701 | name = "serde_urlencoded" 1702 | version = "0.7.1" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1705 | dependencies = [ 1706 | "form_urlencoded", 1707 | "itoa", 1708 | "ryu", 1709 | "serde", 1710 | ] 1711 | 1712 | [[package]] 1713 | name = "serde_with" 1714 | version = "3.11.0" 1715 | source = "registry+https://github.com/rust-lang/crates.io-index" 1716 | checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" 1717 | dependencies = [ 1718 | "serde", 1719 | "serde_derive", 1720 | "serde_with_macros", 1721 | ] 1722 | 1723 | [[package]] 1724 | name = "serde_with_macros" 1725 | version = "3.11.0" 1726 | source = "registry+https://github.com/rust-lang/crates.io-index" 1727 | checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" 1728 | dependencies = [ 1729 | "darling", 1730 | "proc-macro2", 1731 | "quote", 1732 | "syn", 1733 | ] 1734 | 1735 | [[package]] 1736 | name = "servo_arc" 1737 | version = "0.3.0" 1738 | source = "registry+https://github.com/rust-lang/crates.io-index" 1739 | checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" 1740 | dependencies = [ 1741 | "stable_deref_trait", 1742 | ] 1743 | 1744 | [[package]] 1745 | name = "shlex" 1746 | version = "1.3.0" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1749 | 1750 | [[package]] 1751 | name = "similar" 1752 | version = "2.6.0" 1753 | source = "registry+https://github.com/rust-lang/crates.io-index" 1754 | checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" 1755 | 1756 | [[package]] 1757 | name = "siphasher" 1758 | version = "0.3.11" 1759 | source = "registry+https://github.com/rust-lang/crates.io-index" 1760 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 1761 | 1762 | [[package]] 1763 | name = "slab" 1764 | version = "0.4.9" 1765 | source = "registry+https://github.com/rust-lang/crates.io-index" 1766 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1767 | dependencies = [ 1768 | "autocfg", 1769 | ] 1770 | 1771 | [[package]] 1772 | name = "smallvec" 1773 | version = "1.13.2" 1774 | source = "registry+https://github.com/rust-lang/crates.io-index" 1775 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1776 | 1777 | [[package]] 1778 | name = "socket2" 1779 | version = "0.5.9" 1780 | source = "registry+https://github.com/rust-lang/crates.io-index" 1781 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 1782 | dependencies = [ 1783 | "libc", 1784 | "windows-sys 0.52.0", 1785 | ] 1786 | 1787 | [[package]] 1788 | name = "stable_deref_trait" 1789 | version = "1.2.0" 1790 | source = "registry+https://github.com/rust-lang/crates.io-index" 1791 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1792 | 1793 | [[package]] 1794 | name = "string_cache" 1795 | version = "0.8.7" 1796 | source = "registry+https://github.com/rust-lang/crates.io-index" 1797 | checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" 1798 | dependencies = [ 1799 | "new_debug_unreachable", 1800 | "once_cell", 1801 | "parking_lot", 1802 | "phf_shared 0.10.0", 1803 | "precomputed-hash", 1804 | "serde", 1805 | ] 1806 | 1807 | [[package]] 1808 | name = "string_cache_codegen" 1809 | version = "0.5.2" 1810 | source = "registry+https://github.com/rust-lang/crates.io-index" 1811 | checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" 1812 | dependencies = [ 1813 | "phf_generator 0.10.0", 1814 | "phf_shared 0.10.0", 1815 | "proc-macro2", 1816 | "quote", 1817 | ] 1818 | 1819 | [[package]] 1820 | name = "strsim" 1821 | version = "0.11.1" 1822 | source = "registry+https://github.com/rust-lang/crates.io-index" 1823 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1824 | 1825 | [[package]] 1826 | name = "strum" 1827 | version = "0.26.3" 1828 | source = "registry+https://github.com/rust-lang/crates.io-index" 1829 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 1830 | 1831 | [[package]] 1832 | name = "strum_macros" 1833 | version = "0.26.4" 1834 | source = "registry+https://github.com/rust-lang/crates.io-index" 1835 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 1836 | dependencies = [ 1837 | "heck", 1838 | "proc-macro2", 1839 | "quote", 1840 | "rustversion", 1841 | "syn", 1842 | ] 1843 | 1844 | [[package]] 1845 | name = "subtle" 1846 | version = "2.6.1" 1847 | source = "registry+https://github.com/rust-lang/crates.io-index" 1848 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1849 | 1850 | [[package]] 1851 | name = "syn" 1852 | version = "2.0.90" 1853 | source = "registry+https://github.com/rust-lang/crates.io-index" 1854 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 1855 | dependencies = [ 1856 | "proc-macro2", 1857 | "quote", 1858 | "unicode-ident", 1859 | ] 1860 | 1861 | [[package]] 1862 | name = "sync_wrapper" 1863 | version = "1.0.1" 1864 | source = "registry+https://github.com/rust-lang/crates.io-index" 1865 | checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" 1866 | dependencies = [ 1867 | "futures-core", 1868 | ] 1869 | 1870 | [[package]] 1871 | name = "system-configuration" 1872 | version = "0.6.1" 1873 | source = "registry+https://github.com/rust-lang/crates.io-index" 1874 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 1875 | dependencies = [ 1876 | "bitflags", 1877 | "core-foundation", 1878 | "system-configuration-sys", 1879 | ] 1880 | 1881 | [[package]] 1882 | name = "system-configuration-sys" 1883 | version = "0.6.0" 1884 | source = "registry+https://github.com/rust-lang/crates.io-index" 1885 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1886 | dependencies = [ 1887 | "core-foundation-sys", 1888 | "libc", 1889 | ] 1890 | 1891 | [[package]] 1892 | name = "tempfile" 1893 | version = "3.13.0" 1894 | source = "registry+https://github.com/rust-lang/crates.io-index" 1895 | checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" 1896 | dependencies = [ 1897 | "cfg-if", 1898 | "fastrand", 1899 | "once_cell", 1900 | "rustix", 1901 | "windows-sys 0.59.0", 1902 | ] 1903 | 1904 | [[package]] 1905 | name = "tendril" 1906 | version = "0.4.3" 1907 | source = "registry+https://github.com/rust-lang/crates.io-index" 1908 | checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" 1909 | dependencies = [ 1910 | "futf", 1911 | "mac", 1912 | "utf-8", 1913 | ] 1914 | 1915 | [[package]] 1916 | name = "thiserror" 1917 | version = "1.0.64" 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" 1919 | checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" 1920 | dependencies = [ 1921 | "thiserror-impl", 1922 | ] 1923 | 1924 | [[package]] 1925 | name = "thiserror-impl" 1926 | version = "1.0.64" 1927 | source = "registry+https://github.com/rust-lang/crates.io-index" 1928 | checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" 1929 | dependencies = [ 1930 | "proc-macro2", 1931 | "quote", 1932 | "syn", 1933 | ] 1934 | 1935 | [[package]] 1936 | name = "time" 1937 | version = "0.3.37" 1938 | source = "registry+https://github.com/rust-lang/crates.io-index" 1939 | checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" 1940 | dependencies = [ 1941 | "deranged", 1942 | "itoa", 1943 | "num-conv", 1944 | "powerfmt", 1945 | "serde", 1946 | "time-core", 1947 | "time-macros", 1948 | ] 1949 | 1950 | [[package]] 1951 | name = "time-core" 1952 | version = "0.1.2" 1953 | source = "registry+https://github.com/rust-lang/crates.io-index" 1954 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1955 | 1956 | [[package]] 1957 | name = "time-macros" 1958 | version = "0.2.19" 1959 | source = "registry+https://github.com/rust-lang/crates.io-index" 1960 | checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" 1961 | dependencies = [ 1962 | "num-conv", 1963 | "time-core", 1964 | ] 1965 | 1966 | [[package]] 1967 | name = "tinyvec" 1968 | version = "1.8.0" 1969 | source = "registry+https://github.com/rust-lang/crates.io-index" 1970 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1971 | dependencies = [ 1972 | "tinyvec_macros", 1973 | ] 1974 | 1975 | [[package]] 1976 | name = "tinyvec_macros" 1977 | version = "0.1.1" 1978 | source = "registry+https://github.com/rust-lang/crates.io-index" 1979 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1980 | 1981 | [[package]] 1982 | name = "tokio" 1983 | version = "1.44.2" 1984 | source = "registry+https://github.com/rust-lang/crates.io-index" 1985 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" 1986 | dependencies = [ 1987 | "backtrace", 1988 | "bytes", 1989 | "libc", 1990 | "mio", 1991 | "pin-project-lite", 1992 | "socket2", 1993 | "windows-sys 0.52.0", 1994 | ] 1995 | 1996 | [[package]] 1997 | name = "tokio-native-tls" 1998 | version = "0.3.1" 1999 | source = "registry+https://github.com/rust-lang/crates.io-index" 2000 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2001 | dependencies = [ 2002 | "native-tls", 2003 | "tokio", 2004 | ] 2005 | 2006 | [[package]] 2007 | name = "tokio-rustls" 2008 | version = "0.26.0" 2009 | source = "registry+https://github.com/rust-lang/crates.io-index" 2010 | checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" 2011 | dependencies = [ 2012 | "rustls", 2013 | "rustls-pki-types", 2014 | "tokio", 2015 | ] 2016 | 2017 | [[package]] 2018 | name = "tokio-util" 2019 | version = "0.7.12" 2020 | source = "registry+https://github.com/rust-lang/crates.io-index" 2021 | checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" 2022 | dependencies = [ 2023 | "bytes", 2024 | "futures-core", 2025 | "futures-sink", 2026 | "pin-project-lite", 2027 | "tokio", 2028 | ] 2029 | 2030 | [[package]] 2031 | name = "tower-service" 2032 | version = "0.3.3" 2033 | source = "registry+https://github.com/rust-lang/crates.io-index" 2034 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2035 | 2036 | [[package]] 2037 | name = "tracing" 2038 | version = "0.1.40" 2039 | source = "registry+https://github.com/rust-lang/crates.io-index" 2040 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 2041 | dependencies = [ 2042 | "pin-project-lite", 2043 | "tracing-core", 2044 | ] 2045 | 2046 | [[package]] 2047 | name = "tracing-core" 2048 | version = "0.1.32" 2049 | source = "registry+https://github.com/rust-lang/crates.io-index" 2050 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 2051 | dependencies = [ 2052 | "once_cell", 2053 | ] 2054 | 2055 | [[package]] 2056 | name = "try-lock" 2057 | version = "0.2.5" 2058 | source = "registry+https://github.com/rust-lang/crates.io-index" 2059 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2060 | 2061 | [[package]] 2062 | name = "unicode-bidi" 2063 | version = "0.3.18" 2064 | source = "registry+https://github.com/rust-lang/crates.io-index" 2065 | checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 2066 | 2067 | [[package]] 2068 | name = "unicode-ident" 2069 | version = "1.0.14" 2070 | source = "registry+https://github.com/rust-lang/crates.io-index" 2071 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 2072 | 2073 | [[package]] 2074 | name = "unicode-joining-type" 2075 | version = "0.7.0" 2076 | source = "registry+https://github.com/rust-lang/crates.io-index" 2077 | checksum = "22f8cb47ccb8bc750808755af3071da4a10dcd147b68fc874b7ae4b12543f6f5" 2078 | 2079 | [[package]] 2080 | name = "unicode-normalization" 2081 | version = "0.1.24" 2082 | source = "registry+https://github.com/rust-lang/crates.io-index" 2083 | checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 2084 | dependencies = [ 2085 | "tinyvec", 2086 | ] 2087 | 2088 | [[package]] 2089 | name = "unicode-width" 2090 | version = "0.1.14" 2091 | source = "registry+https://github.com/rust-lang/crates.io-index" 2092 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 2093 | 2094 | [[package]] 2095 | name = "untrusted" 2096 | version = "0.9.0" 2097 | source = "registry+https://github.com/rust-lang/crates.io-index" 2098 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2099 | 2100 | [[package]] 2101 | name = "url" 2102 | version = "2.5.4" 2103 | source = "registry+https://github.com/rust-lang/crates.io-index" 2104 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 2105 | dependencies = [ 2106 | "form_urlencoded", 2107 | "idna", 2108 | "percent-encoding", 2109 | ] 2110 | 2111 | [[package]] 2112 | name = "utf-8" 2113 | version = "0.7.6" 2114 | source = "registry+https://github.com/rust-lang/crates.io-index" 2115 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 2116 | 2117 | [[package]] 2118 | name = "utf8_iter" 2119 | version = "1.0.4" 2120 | source = "registry+https://github.com/rust-lang/crates.io-index" 2121 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2122 | 2123 | [[package]] 2124 | name = "utf8parse" 2125 | version = "0.2.2" 2126 | source = "registry+https://github.com/rust-lang/crates.io-index" 2127 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2128 | 2129 | [[package]] 2130 | name = "vcpkg" 2131 | version = "0.2.15" 2132 | source = "registry+https://github.com/rust-lang/crates.io-index" 2133 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2134 | 2135 | [[package]] 2136 | name = "version_check" 2137 | version = "0.9.5" 2138 | source = "registry+https://github.com/rust-lang/crates.io-index" 2139 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2140 | 2141 | [[package]] 2142 | name = "vte" 2143 | version = "0.10.1" 2144 | source = "registry+https://github.com/rust-lang/crates.io-index" 2145 | checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" 2146 | dependencies = [ 2147 | "arrayvec", 2148 | "utf8parse", 2149 | "vte_generate_state_changes", 2150 | ] 2151 | 2152 | [[package]] 2153 | name = "vte_generate_state_changes" 2154 | version = "0.1.2" 2155 | source = "registry+https://github.com/rust-lang/crates.io-index" 2156 | checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" 2157 | dependencies = [ 2158 | "proc-macro2", 2159 | "quote", 2160 | ] 2161 | 2162 | [[package]] 2163 | name = "want" 2164 | version = "0.3.1" 2165 | source = "registry+https://github.com/rust-lang/crates.io-index" 2166 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2167 | dependencies = [ 2168 | "try-lock", 2169 | ] 2170 | 2171 | [[package]] 2172 | name = "wasi" 2173 | version = "0.11.0+wasi-snapshot-preview1" 2174 | source = "registry+https://github.com/rust-lang/crates.io-index" 2175 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2176 | 2177 | [[package]] 2178 | name = "wasm-bindgen" 2179 | version = "0.2.95" 2180 | source = "registry+https://github.com/rust-lang/crates.io-index" 2181 | checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" 2182 | dependencies = [ 2183 | "cfg-if", 2184 | "once_cell", 2185 | "wasm-bindgen-macro", 2186 | ] 2187 | 2188 | [[package]] 2189 | name = "wasm-bindgen-backend" 2190 | version = "0.2.95" 2191 | source = "registry+https://github.com/rust-lang/crates.io-index" 2192 | checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" 2193 | dependencies = [ 2194 | "bumpalo", 2195 | "log", 2196 | "once_cell", 2197 | "proc-macro2", 2198 | "quote", 2199 | "syn", 2200 | "wasm-bindgen-shared", 2201 | ] 2202 | 2203 | [[package]] 2204 | name = "wasm-bindgen-futures" 2205 | version = "0.4.45" 2206 | source = "registry+https://github.com/rust-lang/crates.io-index" 2207 | checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" 2208 | dependencies = [ 2209 | "cfg-if", 2210 | "js-sys", 2211 | "wasm-bindgen", 2212 | "web-sys", 2213 | ] 2214 | 2215 | [[package]] 2216 | name = "wasm-bindgen-macro" 2217 | version = "0.2.95" 2218 | source = "registry+https://github.com/rust-lang/crates.io-index" 2219 | checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" 2220 | dependencies = [ 2221 | "quote", 2222 | "wasm-bindgen-macro-support", 2223 | ] 2224 | 2225 | [[package]] 2226 | name = "wasm-bindgen-macro-support" 2227 | version = "0.2.95" 2228 | source = "registry+https://github.com/rust-lang/crates.io-index" 2229 | checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" 2230 | dependencies = [ 2231 | "proc-macro2", 2232 | "quote", 2233 | "syn", 2234 | "wasm-bindgen-backend", 2235 | "wasm-bindgen-shared", 2236 | ] 2237 | 2238 | [[package]] 2239 | name = "wasm-bindgen-shared" 2240 | version = "0.2.95" 2241 | source = "registry+https://github.com/rust-lang/crates.io-index" 2242 | checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" 2243 | 2244 | [[package]] 2245 | name = "web-sys" 2246 | version = "0.3.72" 2247 | source = "registry+https://github.com/rust-lang/crates.io-index" 2248 | checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" 2249 | dependencies = [ 2250 | "js-sys", 2251 | "wasm-bindgen", 2252 | ] 2253 | 2254 | [[package]] 2255 | name = "winapi" 2256 | version = "0.3.9" 2257 | source = "registry+https://github.com/rust-lang/crates.io-index" 2258 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2259 | dependencies = [ 2260 | "winapi-i686-pc-windows-gnu", 2261 | "winapi-x86_64-pc-windows-gnu", 2262 | ] 2263 | 2264 | [[package]] 2265 | name = "winapi-i686-pc-windows-gnu" 2266 | version = "0.4.0" 2267 | source = "registry+https://github.com/rust-lang/crates.io-index" 2268 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2269 | 2270 | [[package]] 2271 | name = "winapi-x86_64-pc-windows-gnu" 2272 | version = "0.4.0" 2273 | source = "registry+https://github.com/rust-lang/crates.io-index" 2274 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2275 | 2276 | [[package]] 2277 | name = "windows-core" 2278 | version = "0.52.0" 2279 | source = "registry+https://github.com/rust-lang/crates.io-index" 2280 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 2281 | dependencies = [ 2282 | "windows-targets 0.52.6", 2283 | ] 2284 | 2285 | [[package]] 2286 | name = "windows-registry" 2287 | version = "0.2.0" 2288 | source = "registry+https://github.com/rust-lang/crates.io-index" 2289 | checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" 2290 | dependencies = [ 2291 | "windows-result", 2292 | "windows-strings", 2293 | "windows-targets 0.52.6", 2294 | ] 2295 | 2296 | [[package]] 2297 | name = "windows-result" 2298 | version = "0.2.0" 2299 | source = "registry+https://github.com/rust-lang/crates.io-index" 2300 | checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 2301 | dependencies = [ 2302 | "windows-targets 0.52.6", 2303 | ] 2304 | 2305 | [[package]] 2306 | name = "windows-strings" 2307 | version = "0.1.0" 2308 | source = "registry+https://github.com/rust-lang/crates.io-index" 2309 | checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 2310 | dependencies = [ 2311 | "windows-result", 2312 | "windows-targets 0.52.6", 2313 | ] 2314 | 2315 | [[package]] 2316 | name = "windows-sys" 2317 | version = "0.48.0" 2318 | source = "registry+https://github.com/rust-lang/crates.io-index" 2319 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2320 | dependencies = [ 2321 | "windows-targets 0.48.5", 2322 | ] 2323 | 2324 | [[package]] 2325 | name = "windows-sys" 2326 | version = "0.52.0" 2327 | source = "registry+https://github.com/rust-lang/crates.io-index" 2328 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2329 | dependencies = [ 2330 | "windows-targets 0.52.6", 2331 | ] 2332 | 2333 | [[package]] 2334 | name = "windows-sys" 2335 | version = "0.59.0" 2336 | source = "registry+https://github.com/rust-lang/crates.io-index" 2337 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2338 | dependencies = [ 2339 | "windows-targets 0.52.6", 2340 | ] 2341 | 2342 | [[package]] 2343 | name = "windows-targets" 2344 | version = "0.48.5" 2345 | source = "registry+https://github.com/rust-lang/crates.io-index" 2346 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2347 | dependencies = [ 2348 | "windows_aarch64_gnullvm 0.48.5", 2349 | "windows_aarch64_msvc 0.48.5", 2350 | "windows_i686_gnu 0.48.5", 2351 | "windows_i686_msvc 0.48.5", 2352 | "windows_x86_64_gnu 0.48.5", 2353 | "windows_x86_64_gnullvm 0.48.5", 2354 | "windows_x86_64_msvc 0.48.5", 2355 | ] 2356 | 2357 | [[package]] 2358 | name = "windows-targets" 2359 | version = "0.52.6" 2360 | source = "registry+https://github.com/rust-lang/crates.io-index" 2361 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2362 | dependencies = [ 2363 | "windows_aarch64_gnullvm 0.52.6", 2364 | "windows_aarch64_msvc 0.52.6", 2365 | "windows_i686_gnu 0.52.6", 2366 | "windows_i686_gnullvm", 2367 | "windows_i686_msvc 0.52.6", 2368 | "windows_x86_64_gnu 0.52.6", 2369 | "windows_x86_64_gnullvm 0.52.6", 2370 | "windows_x86_64_msvc 0.52.6", 2371 | ] 2372 | 2373 | [[package]] 2374 | name = "windows_aarch64_gnullvm" 2375 | version = "0.48.5" 2376 | source = "registry+https://github.com/rust-lang/crates.io-index" 2377 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2378 | 2379 | [[package]] 2380 | name = "windows_aarch64_gnullvm" 2381 | version = "0.52.6" 2382 | source = "registry+https://github.com/rust-lang/crates.io-index" 2383 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2384 | 2385 | [[package]] 2386 | name = "windows_aarch64_msvc" 2387 | version = "0.48.5" 2388 | source = "registry+https://github.com/rust-lang/crates.io-index" 2389 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2390 | 2391 | [[package]] 2392 | name = "windows_aarch64_msvc" 2393 | version = "0.52.6" 2394 | source = "registry+https://github.com/rust-lang/crates.io-index" 2395 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2396 | 2397 | [[package]] 2398 | name = "windows_i686_gnu" 2399 | version = "0.48.5" 2400 | source = "registry+https://github.com/rust-lang/crates.io-index" 2401 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2402 | 2403 | [[package]] 2404 | name = "windows_i686_gnu" 2405 | version = "0.52.6" 2406 | source = "registry+https://github.com/rust-lang/crates.io-index" 2407 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2408 | 2409 | [[package]] 2410 | name = "windows_i686_gnullvm" 2411 | version = "0.52.6" 2412 | source = "registry+https://github.com/rust-lang/crates.io-index" 2413 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2414 | 2415 | [[package]] 2416 | name = "windows_i686_msvc" 2417 | version = "0.48.5" 2418 | source = "registry+https://github.com/rust-lang/crates.io-index" 2419 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2420 | 2421 | [[package]] 2422 | name = "windows_i686_msvc" 2423 | version = "0.52.6" 2424 | source = "registry+https://github.com/rust-lang/crates.io-index" 2425 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2426 | 2427 | [[package]] 2428 | name = "windows_x86_64_gnu" 2429 | version = "0.48.5" 2430 | source = "registry+https://github.com/rust-lang/crates.io-index" 2431 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2432 | 2433 | [[package]] 2434 | name = "windows_x86_64_gnu" 2435 | version = "0.52.6" 2436 | source = "registry+https://github.com/rust-lang/crates.io-index" 2437 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2438 | 2439 | [[package]] 2440 | name = "windows_x86_64_gnullvm" 2441 | version = "0.48.5" 2442 | source = "registry+https://github.com/rust-lang/crates.io-index" 2443 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2444 | 2445 | [[package]] 2446 | name = "windows_x86_64_gnullvm" 2447 | version = "0.52.6" 2448 | source = "registry+https://github.com/rust-lang/crates.io-index" 2449 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2450 | 2451 | [[package]] 2452 | name = "windows_x86_64_msvc" 2453 | version = "0.48.5" 2454 | source = "registry+https://github.com/rust-lang/crates.io-index" 2455 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2456 | 2457 | [[package]] 2458 | name = "windows_x86_64_msvc" 2459 | version = "0.52.6" 2460 | source = "registry+https://github.com/rust-lang/crates.io-index" 2461 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2462 | 2463 | [[package]] 2464 | name = "zerocopy" 2465 | version = "0.7.35" 2466 | source = "registry+https://github.com/rust-lang/crates.io-index" 2467 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 2468 | dependencies = [ 2469 | "byteorder", 2470 | "zerocopy-derive", 2471 | ] 2472 | 2473 | [[package]] 2474 | name = "zerocopy-derive" 2475 | version = "0.7.35" 2476 | source = "registry+https://github.com/rust-lang/crates.io-index" 2477 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 2478 | dependencies = [ 2479 | "proc-macro2", 2480 | "quote", 2481 | "syn", 2482 | ] 2483 | 2484 | [[package]] 2485 | name = "zeroize" 2486 | version = "1.8.1" 2487 | source = "registry+https://github.com/rust-lang/crates.io-index" 2488 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2489 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hydra-check" 3 | version = "2.0.4" 4 | description = "Check hydra for the build status of a package" 5 | authors = ["Felix Richter ", "Bryan Lai "] 6 | edition = "2021" 7 | license = "MIT" 8 | repository = "https://github.com/nix-community/hydra-check" 9 | keywords = ["cli"] 10 | categories = ["command-line-utilities"] 11 | 12 | [dependencies] 13 | anyhow = "1.0.89" 14 | clap = { version = "4.5.20", features = ["derive"] } 15 | clap_complete = "4.5.37" 16 | colored = "2.1.0" 17 | comfy-table = { version = "7.1.1", features = ["custom_styling"] } 18 | flexi_logger = "0.29.3" 19 | indexmap = { version = "2.6.0", features = ["serde"] } 20 | log = "0.4.22" 21 | regex = "1.11.0" 22 | reqwest = { version = "0.12.8", features = ["blocking", "cookies", "gzip"] } 23 | scraper = "0.20.0" 24 | serde = { version = "1.0.210", features = ["derive"] } 25 | serde_json = "1.0.132" 26 | serde_with = { version = "3.11.0", default-features = false, features = ["macros"] } 27 | 28 | [dev-dependencies] 29 | insta = "1.41.1" 30 | 31 | [build-dependencies] 32 | anyhow = "1.0.89" 33 | 34 | [profile.dev.package] 35 | insta.opt-level = 3 36 | similar.opt-level = 3 37 | 38 | [lints.clippy] 39 | pedantic = { level = "warn", priority = -1 } 40 | cargo = { level = "warn", priority = -1 } 41 | manual_string_new = "allow" 42 | match_bool = "allow" 43 | single_match = "allow" 44 | single_match_else = "allow" 45 | missing_errors_doc = "allow" 46 | multiple_crate_versions = "allow" 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Felix Richter 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 | # hydra-check 2 | 3 | ![Test](https://github.com/nix-community/hydra-check/workflows/Test/badge.svg) 4 | 5 | Check hydra for the build status of a package in a given channel. 6 | 7 | # Disclaimer 8 | Keep in mind that hydra is the NixOS build-farm orchestrator and has more important tasks to do than answering your puny requests. Response time may be in the seconds for each request. 9 | 10 | # Usage 11 | 12 | ```console 13 | $ nix-shell 14 | 15 | $ hydra-check --help 16 | usage: hydra-check [options] PACKAGES... 17 | ... 18 | 19 | $ hydra-check 20 | Evaluations of jobset nixpkgs/trunk @ https://hydra.nixos.org/jobset/nixpkgs/trunk/evals 21 | ⧖ nixpkgs → df76cd6 5h ago ✔ 194306 ✖ 3554 ⧖ 55364 Δ ? https://hydra.nixos.org/eval/1809865 22 | ⧖ nixpkgs → 7701a9e 14h ago ✔ 196874 ✖ 5123 ⧖ 51253 Δ ? https://hydra.nixos.org/eval/1809859 23 | ⧖ nixpkgs → 7150b43 23h ago ✔ 200232 ✖ 48388 ⧖ 4629 Δ ? https://hydra.nixos.org/eval/1809854 24 | ⧖ nixpkgs → ed1b999 1d ago ✔ 200402 ✖ 48482 ⧖ 4319 Δ ? https://hydra.nixos.org/eval/1809850 25 | ⧖ nixpkgs → b651050 1d ago ✔ 244082 ✖ 6187 ⧖ 2813 Δ ? https://hydra.nixos.org/eval/1809840 26 | ✔ nixpkgs → 85f7e66 2d ago ✔ 246713 ✖ 5963 ⧖ 0 Δ +67 https://hydra.nixos.org/eval/1809829 27 | ✔ nixpkgs → fe348e1 2d ago ✔ 246646 ✖ 6015 ⧖ 0 Δ +276 https://hydra.nixos.org/eval/1809822 28 | ... 29 | 30 | $ hydra-check hello 31 | Build Status for nixpkgs.hello.x86_64-linux on jobset nixos/trunk-combined 32 | ✔ hello-2.10 from 2020-03-14 - https://hydra.nixos.org/build/114752982 33 | 34 | $ hydra-check hello --arch x86_64-darwin 35 | Build Status for hello.x86_64-darwin on jobset nixpkgs/trunk 36 | ✔ hello-2.12.1 from 2023-09-28 - https://hydra.nixos.org/build/236635446 37 | 38 | $ hydra-check hello python --channel 19.03 39 | Build Status for nixpkgs.hello.x86_64-linux on jobset nixos/19.03 40 | ✔ hello-2.10 from 2019-10-14 - https://hydra.nixos.org/build/103243113 41 | Build Status for nixpkgs.python.x86_64-linux on jobset nixos/19.03 42 | ✔ python-2.7.17 from 2020-01-14 - https://hydra.nixos.org/build/110523905 43 | 44 | $ hydra-check nixos.tests.installer.simpleUefiGrub --channel 19.09 --arch aarch64-linux 45 | Build Status for nixos.tests.installer.simpleUefiGrub.aarch64-linux on jobset nixos/19.09 46 | ✖ (Dependency failed) vm-test-run-installer-simpleUefiGrub from 2020-03-19 - https://hydra.nixos.org/build/115139363 47 | 48 | Last Builds: 49 | ✖ (Dependency failed) vm-test-run-installer-simpleUefiGrub from 2020-03-18 - https://hydra.nixos.org/build/115135183 50 | ✖ (Dependency failed) vm-test-run-installer-simpleUefiGrub from 2020-03-18 - https://hydra.nixos.org/build/115093440 51 | ✔ vm-test-run-installer-simpleUefiGrub from 2020-03-18 - https://hydra.nixos.org/build/115073926 52 | ✔ vm-test-run-installer-simpleUefiGrub from 2020-03-17 - https://hydra.nixos.org/build/115013869 53 | ✖ (Cancelled) vm-test-run-installer-simpleUefiGrub from 2020-03-17 - https://hydra.nixos.org/build/114921818 54 | ✔ vm-test-run-installer-simpleUefiGrub from 2020-03-17 - https://hydra.nixos.org/build/114887664 55 | ✖ (Timed out) vm-test-run-installer-simpleUefiGrub from 2020-03-16 - https://hydra.nixos.org/build/114881668 56 | ... 57 | 58 | 59 | $ hydra-check ugarit --channel 19.09 --short 60 | Build Status for nixpkgs.ugarit.x86_64-linux on jobset nixos/19.09 61 | ✖ (Dependency failed) chicken-ugarit-2.0 from 2020-02-23 - https://hydra.nixos.org/build/108216732 62 | 63 | 64 | $ hydra-check nixos.containerTarball hello --channel 19.09 --arch i686-linux --json | jq . 65 | { 66 | "nixos.containerTarball": [ 67 | { 68 | "icon": "✖", 69 | "success": false, 70 | "status": "Failed", 71 | "timestamp": "2020-03-18T22:02:59Z", 72 | "build_id": "115099119", 73 | "build_url": "https://hydra.nixos.org/build/115099119", 74 | "name": "tarball", 75 | "arch": "i686-linux" 76 | }, 77 | { 78 | "icon": "✖", 79 | "success": false, 80 | "status": "Failed", 81 | "timestamp": "2020-03-17T18:10:09Z", 82 | "build_id": "115073178", 83 | "build_url": "https://hydra.nixos.org/build/115073178", 84 | "name": "tarball", 85 | "arch": "i686-linux" 86 | }, 87 | ... 88 | ], 89 | "hello": [ 90 | { 91 | "icon": "✔", 92 | "success": true, 93 | "status": "Succeeded", 94 | "timestamp": "2017-07-31T13:28:03Z", 95 | "build_id": "57619684", 96 | "build_url": "https://hydra.nixos.org/build/57619684", 97 | "name": "hello-2.10", 98 | "arch": "i686-linux" 99 | }, 100 | { 101 | "icon": "✔", 102 | "success": true, 103 | "status": "Succeeded", 104 | "timestamp": "2017-07-25T03:36:27Z", 105 | "build_id": "56997384", 106 | "build_url": "https://hydra.nixos.org/build/56997384", 107 | "name": "hello-2.10", 108 | "arch": "i686-linux" 109 | }, 110 | ... 111 | ] 112 | } 113 | 114 | $ hydra-check --channel=staging-next --eval 115 | info: querying the latest evaluation of --jobset 'nixpkgs/staging-next' 116 | 117 | Evaluations of jobset nixpkgs/staging-next @ https://hydra.nixos.org/jobset/nixpkgs/staging-next/evals 118 | ⧖ nixpkgs → b2a0e31 1d ago ✔ 42294 ✖ 2447 ⧖ 208458 Δ +18066 https://hydra.nixos.org/eval/1809845 119 | 120 | info: no package filter has been specified, so the default filter '/nixVersions.stable' is used for better performance 121 | info: specify another filter with --eval '1809845/', or force an empty filter with a trailing slash: '1809845/' 122 | 123 | Evaluation 1809845 filtered by 'nixVersions.stable' @ https://hydra.nixos.org/eval/1809845?filter=nixVersions.stable 124 | 125 | input: nixpkgs 126 | type: Git checkout 127 | value: https://github.com/NixOS/nixpkgs.git 128 | revision: b2a0e3125e8b373ee2d6480ebd3b8f5c20080796 129 | store_path: /nix/store/54adh6vxi8zf1vpxj2gagwajk3hcrd0x-source 130 | 131 | input: officialRelease 132 | type: Boolean 133 | value: false 134 | 135 | input: supportedSystems 136 | type: Nix expression 137 | value: [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ] 138 | 139 | changed_input: nixpkgs 140 | changes: 20741966793c to b2a0e3125e8b 141 | url: https://hydra.nixos.org/api/scmdiff?type=git&rev2=b2a0e3125e8b373ee2d6480ebd3b8f5c20080796&branch=&rev1=20741966793c81f2322645d30bce95d37f63f545&uri=https%3A%2F%2Fgithub.com%2FNixOS%2Fnixpkgs.git 142 | revs: 20741966793c81f2322645d30bce95d37f63f545 -> b2a0e3125e8b373ee2d6480ebd3b8f5c20080796 143 | 144 | Queued Jobs: 145 | ⧖ (Queued) nixVersions.stable.aarch64-darwin nix-2.24.10 https://hydra.nixos.org/build/277949183 146 | ⧖ (Queued) nixVersions.stable.aarch64-linux nix-2.24.10 2024-11-07 https://hydra.nixos.org/build/277629888 147 | ⧖ (Queued) nixVersions.stable.x86_64-darwin nix-2.24.10 https://hydra.nixos.org/build/277948312 148 | ⧖ (Queued) nixVersions.stable.x86_64-linux nix-2.24.10 2024-11-07 https://hydra.nixos.org/build/277560645 149 | 150 | ``` 151 | 152 | # Changelog 153 | 154 | ## 2.0.0 Breaking changes 155 | - Rewritten in Rust 156 | - Always prints long outputs with all recent builds unless `--short` is explicitly specified 157 | - `--arch` defaults to the target architecture (instead of `x86_64-linux` all the time) 158 | - `--jobset` explicitly conflicts with `--channel` to avoid confusion, as channels are just aliases for jobsets 159 | - The `staging` channel / alias is removed as `nixos/staging` is no longer active; instead we add `staging-next` as an alias for `nixpkgs/staging-next` 160 | - The default `unstable` channel points to `nixpkgs/trunk` on non-NixOS systems 161 | 162 | ### Features 163 | - Print recent evaluations of the jobset if no package is specified 164 | - Add an `--eval` flag for information about a specific evaluation 165 | - Infer the current stable Nixpkgs release (e.g. `24.05`) with a hack 166 | - Support standard channel names (e.g. `nixos-unstable`) 167 | - Generate shell completions with `--shell-completion SHELL` 168 | - Print nicely formatted, colored and aligned tables 169 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use anyhow::bail; 2 | 3 | const WARN: &str = "cargo::warning="; 4 | 5 | fn main() -> anyhow::Result<()> { 6 | check_and_refine_package_versions() 7 | } 8 | 9 | /// - Checks that the package version is consistent bewteen: 10 | /// 11 | /// - `Cargo.toml` (i.e. `$CARGO_PKG_VERSION`), and 12 | /// - `package.nix` (i.e. `$version`). 13 | /// 14 | /// - Extends the package version, i.e. `$CARGO_PKG_VERSION` with a commit 15 | /// hash suffix generated in package.nix. This is useful for 16 | /// identifying development builds. 17 | /// 18 | fn check_and_refine_package_versions() -> anyhow::Result<()> { 19 | println!("cargo::rerun-if-env-changed=version"); 20 | 21 | let set_version = |version: &str| { 22 | println!("{WARN}setting version to: {version}"); 23 | println!("cargo::rustc-env=CARGO_PKG_VERSION={version}"); 24 | }; 25 | 26 | let version_from_cargo = env!("CARGO_PKG_VERSION"); 27 | let version_from_nix = if let Some(version_from_nix) = option_env!("version") { 28 | if !version_from_nix.starts_with(version_from_cargo) { 29 | bail!( 30 | "inconsistent versioning: Cargo.toml={} vs package.nix={}\n{}\n{}", 31 | version_from_cargo, 32 | version_from_nix, 33 | "either update the versions or refresh the development environment", 34 | "if you are using direnv, prepend `watch_file Cargo.lock` to `.envrc`", 35 | ); 36 | } 37 | version_from_nix 38 | } else { 39 | version_from_cargo 40 | }; 41 | 42 | set_version(version_from_nix); 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let lock = builtins.fromJSON (builtins.readFile ./flake.lock); 4 | in 5 | fetchTarball { 6 | url = 7 | "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 8 | sha256 = lock.nodes.flake-compat.locked.narHash; 9 | } 10 | ) 11 | { src = ./.; }).defaultNix 12 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1733328505, 7 | "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-utils": { 20 | "inputs": { 21 | "systems": "systems" 22 | }, 23 | "locked": { 24 | "lastModified": 1731533236, 25 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 26 | "owner": "numtide", 27 | "repo": "flake-utils", 28 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "numtide", 33 | "repo": "flake-utils", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs": { 38 | "locked": { 39 | "lastModified": 1744502386, 40 | "narHash": "sha256-QAd1L37eU7ktL2WeLLLTmI6P9moz9+a/ONO8qNBYJgM=", 41 | "owner": "NixOS", 42 | "repo": "nixpkgs", 43 | "rev": "f6db44a8daa59c40ae41ba6e5823ec77fe0d2124", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "NixOS", 48 | "ref": "nixos-unstable", 49 | "repo": "nixpkgs", 50 | "type": "github" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "flake-compat": "flake-compat", 56 | "flake-utils": "flake-utils", 57 | "nixpkgs": "nixpkgs" 58 | } 59 | }, 60 | "systems": { 61 | "locked": { 62 | "lastModified": 1681028828, 63 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 64 | "owner": "nix-systems", 65 | "repo": "default", 66 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "nix-systems", 71 | "repo": "default", 72 | "type": "github" 73 | } 74 | } 75 | }, 76 | "root": "root", 77 | "version": 7 78 | } 79 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "scrape hydra for the build status of a package"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | flake-compat = { 8 | url = "github:edolstra/flake-compat"; 9 | flake = false; 10 | }; 11 | }; 12 | 13 | outputs = { self, nixpkgs, flake-utils, ... }: 14 | flake-utils.lib.eachDefaultSystem (system: 15 | let 16 | pkgs = nixpkgs.legacyPackages.${system}; 17 | in 18 | { 19 | packages = { 20 | hydra-check = pkgs.callPackage ./package.nix { source = self; }; 21 | default = self.packages.${system}.hydra-check; 22 | }; 23 | 24 | devShells.default = self.packages.${system}.hydra-check.overrideAttrs ({ nativeBuildInputs, ... }: { 25 | nativeBuildInputs = with pkgs.buildPackages; [ 26 | git 27 | cargo # with shell completions, instead of cargo-auditable 28 | cargo-insta # for updating insta snapshots 29 | clippy # more lints for better rust code 30 | nixfmt-rfc-style # for formatting nix code 31 | ] ++ nativeBuildInputs; 32 | 33 | # use cached crates in "$HOME/.cargo" 34 | cargoDeps = pkgs.emptyDirectory; 35 | 36 | env = with pkgs.buildPackages; { 37 | # for developments, e.g. symbol lookup in std library 38 | RUST_SRC_PATH = "${rustPlatform.rustLibSrc}"; 39 | # for debugging 40 | RUST_LIB_BACKTRACE = "1"; 41 | }; 42 | }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | hydra-check, 4 | rustPlatform, 5 | source ? builtins.path { 6 | # `builtins.path` works well with lazy trees 7 | name = "hydra-check-source"; 8 | path = ./.; 9 | }, 10 | }: 11 | 12 | let 13 | 14 | packageVersion = with builtins; (fromTOML (readFile "${source}/Cargo.toml")).package.version; 15 | 16 | # append git revision to the version string, if available 17 | versionSuffix = 18 | if (source ? dirtyShortRev || source ? shortRev) then 19 | "-g${source.dirtyShortRev or source.shortRev}" 20 | else 21 | ""; 22 | 23 | newVersion = "${packageVersion}${versionSuffix}"; 24 | 25 | in 26 | 27 | hydra-check.overrideAttrs ( 28 | { 29 | version, 30 | meta ? { }, 31 | nativeBuildInputs ? [ ], 32 | nativeInstallCheckInputs ? [ ], 33 | ... 34 | }: 35 | { 36 | version = 37 | assert lib.assertMsg (lib.versionAtLeast newVersion version) '' 38 | hydra-check provided here (${newVersion}) failed to be newer 39 | than the one provided in nixpkgs (${version}). 40 | ''; 41 | newVersion; 42 | 43 | src = source; 44 | 45 | cargoDeps = rustPlatform.importCargoLock { 46 | lockFile = "${source}/Cargo.lock"; 47 | }; 48 | 49 | meta = meta // { 50 | maintainers = with lib.maintainers; [ 51 | makefu 52 | artturin 53 | bryango 54 | ]; 55 | # to correctly generate meta.position for backtrace: 56 | inherit (meta) description; 57 | }; 58 | } 59 | ) 60 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #! nix-shell -p bash cargo cargo-edit -i bash 3 | # shellcheck shell=bash 4 | 5 | set -euo pipefail 6 | 7 | NEWVERSION="$1" 8 | REPO=nix-community/hydra-check 9 | 10 | if [[ $NEWVERSION == "" ]]; then 11 | echo "No version specified!" 12 | exit 1 13 | fi 14 | 15 | cargo set-version "$NEWVERSION" 16 | cargo build 17 | 18 | # commit the update 19 | git add --patch Cargo.toml Cargo.lock 20 | git commit -m "build(release): v${NEWVERSION}" 21 | 22 | # push & tag 23 | set -x 24 | CURRENT_BRANCH="$(git branch --show-current)" 25 | REMOTE=$(git remote --verbose | grep "REPO" | uniq) 26 | git push "$REMOTE" "$CURRENT_BRANCH" 27 | gh release --repo "$REPO" create "v${NEWVERSION}" -t "v${NEWVERSION}" --prerelease --target "$CURRENT_BRANCH" --generate-notes 28 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let lock = builtins.fromJSON (builtins.readFile ./flake.lock); 4 | in 5 | fetchTarball { 6 | url = 7 | "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 8 | sha256 = lock.nodes.flake-compat.locked.narHash; 9 | } 10 | ) 11 | { src = ./.; }).shellNix 12 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, command, value_parser, CommandFactory, Parser}; 2 | use clap_complete::Shell; 3 | use flexi_logger::Logger; 4 | use log::{debug, error, warn}; 5 | use regex::Regex; 6 | use std::{ 7 | env::consts::{ARCH, OS}, 8 | path::Path, 9 | }; 10 | 11 | use crate::{constants, log_format, Evaluation, NixpkgsChannelVersion}; 12 | 13 | #[derive(Debug, Clone)] 14 | pub(crate) enum Queries { 15 | Jobset, 16 | Packages(Vec), 17 | Evals(Vec), 18 | } 19 | 20 | #[derive(Parser, Debug, Default)] 21 | #[command(author, version, verbatim_doc_comment)] 22 | #[allow( 23 | rustdoc::bare_urls, 24 | clippy::doc_markdown, 25 | clippy::struct_excessive_bools 26 | )] 27 | #[deny(missing_docs)] 28 | /// 29 | /// Check hydra.nixos.org for build status of packages 30 | /// 31 | /// Other channels can be: 32 | /// - unstable - alias for nixos/trunk-combined (for NixOS) or nixpkgs/trunk 33 | /// - master - alias for nixpkgs/trunk (Default for other architectures) 34 | /// - staging-next - alias for nixpkgs/staging-next 35 | /// - 24.05 - alias for nixos/release-24.05 36 | /// 37 | /// Usually using the above as --channel arguments, should fit most usages. 38 | /// However, you can use a verbatim jobset name such as: 39 | /// 40 | /// nixpkgs/nixpkgs-24.05-darwin 41 | /// 42 | /// Jobset names can be constructed with the project name (e.g. `nixos/` or `nixpkgs/`) 43 | /// followed by a branch name. The available jobsets can be found at: 44 | /// - https://hydra.nixos.org/project/nixos 45 | /// - https://hydra.nixos.org/project/nixpkgs 46 | /// 47 | pub struct HydraCheckCli { 48 | #[arg(id = "PACKAGES")] 49 | queries: Vec, 50 | 51 | /// Only print the hydra build url, then exit 52 | #[arg(long)] 53 | url: bool, 54 | 55 | /// Output json 56 | #[arg(long)] 57 | json: bool, 58 | 59 | /// Write only the latest build even if last build failed 60 | #[arg(short, long)] 61 | short: bool, 62 | 63 | /// System architecture to check 64 | #[arg(short, long)] 65 | arch: Option, 66 | 67 | /// Channel to check packages for 68 | #[arg(short, long, default_value = "unstable")] 69 | channel: String, 70 | 71 | /// Specify jobset to check packages for 72 | #[arg(long, conflicts_with = "channel")] 73 | jobset: Option, 74 | 75 | /// Print details about specific evaluations instead of packages 76 | #[arg(short, long)] 77 | eval: bool, 78 | 79 | /// Print more debugging information 80 | #[arg(short, long)] 81 | verbose: bool, 82 | 83 | /// Print generated completions for a given shell 84 | #[arg(long = "shell-completion", exclusive = true, value_parser = value_parser!(Shell))] 85 | shell: Option, 86 | } 87 | 88 | /// Resolved command line arguments, with all options normalized and unwrapped 89 | #[derive(Debug)] 90 | pub(crate) struct ResolvedArgs { 91 | /// List of packages or evals to query 92 | pub(crate) queries: Queries, 93 | pub(crate) url: bool, 94 | pub(crate) json: bool, 95 | pub(crate) short: bool, 96 | pub(crate) jobset: String, 97 | } 98 | 99 | impl HydraCheckCli { 100 | fn guess_arch(self) -> Self { 101 | let warn_if_unknown = |arch: &str| { 102 | if !Vec::from(constants::KNOWN_ARCHITECTURES).contains(&arch) { 103 | warn!( 104 | "unknown --arch '{arch}', {}: {:#?}", 105 | "consider specify one of the following known architectures", 106 | constants::KNOWN_ARCHITECTURES 107 | ); 108 | } 109 | }; 110 | if let Some(arch) = self.arch.clone() { 111 | // allow empty `--arch` as it may be the user's intention to 112 | // specify architectures explicitly for each package 113 | if !arch.is_empty() { 114 | warn_if_unknown(&arch); 115 | } 116 | return self; 117 | } 118 | let arch = format!( 119 | "{}-{}", 120 | ARCH, 121 | match OS { 122 | "macos" => "darwin", // hack to produce e.g. `aarch64-darwin` 123 | x => x, 124 | } 125 | ); 126 | debug!("assuming --arch '{arch}'"); 127 | warn_if_unknown(&arch); 128 | Self { 129 | arch: Some(arch), 130 | ..self 131 | } 132 | } 133 | 134 | /// Guesses the hydra jobset based on system information from build time, 135 | /// run time, and the provided command line arguments. 136 | /// Note that this method is inherently non-deterministic as it depends on 137 | /// the current build target & runtime systems. 138 | /// See the source code for the detailed heuristics. 139 | #[must_use] 140 | #[allow(clippy::missing_panics_doc)] 141 | pub fn guess_jobset(self) -> Self { 142 | if self.jobset.is_some() { 143 | return self; 144 | } 145 | // https://wiki.nixos.org/wiki/Channel_branches 146 | // https://github.com/NixOS/infra/blob/master/channels.nix 147 | let (nixpkgs, nixos) = ("nixpkgs/trunk", "nixos/trunk-combined"); 148 | let jobset: String = match self.channel.as_str() { 149 | "master" | "nixpkgs-unstable" => nixpkgs.into(), 150 | "nixos-unstable" => nixos.into(), 151 | "nixos-unstable-small" => "nixos/unstable-small".into(), 152 | "unstable" => match (Path::new("/etc/NIXOS").exists(), &self.arch) { 153 | (true, Some(arch)) 154 | if Vec::from(constants::NIXOS_ARCHITECTURES).contains(&arch.as_str()) => 155 | { 156 | // only returns the NixOS jobset if the current system is NixOS 157 | // and the --arch is a NixOS supported system. 158 | nixos.into() 159 | } 160 | _ => nixpkgs.into(), 161 | }, 162 | "stable" => { 163 | let ver = match NixpkgsChannelVersion::stable() { 164 | Ok(version) => version, 165 | Err(err) => { 166 | error!( 167 | "{}, {}.\n\n{}", 168 | "could not fetch the stable release version number", 169 | "please specify '--channel' or '--jobset' explicitly", 170 | err 171 | ); 172 | std::process::exit(1); 173 | } 174 | }; 175 | match self.arch.clone() { 176 | // darwin 177 | Some(x) if x.ends_with("darwin") => format!("nixpkgs/nixpkgs-{ver}-darwin"), 178 | // others 179 | _ => format!("nixos/release-{ver}"), 180 | } 181 | } 182 | x if x.starts_with("staging-next") => format!("nixpkgs/{x}"), 183 | x if Regex::new(r"^[0-9]+\.[0-9]+$").unwrap().is_match(x) => { 184 | format!("nixos/release-{x}") 185 | } 186 | x if Regex::new(r"^nixos-[0-9]+\.[0-9]+").unwrap().is_match(x) => { 187 | x.replacen("nixos", "nixos/release", 1) 188 | } 189 | x if Regex::new(r"^nixpkgs-[0-9]+\.[0-9]+").unwrap().is_match(x) => { 190 | x.replacen("nixpkgs", "nixpkgs/nixpkgs", 1) 191 | } 192 | _ => self.channel.clone(), 193 | }; 194 | debug!("--channel '{}' implies --jobset '{}'", self.channel, jobset); 195 | Self { 196 | jobset: Some(jobset), 197 | ..self 198 | } 199 | } 200 | 201 | /// Guesses the full package name spec (e.g. `nixpkgs.gimp.x86_64-linux`) 202 | /// for hydra, given the command line inputs. 203 | /// See the source code for the detailed heuristics. 204 | #[must_use] 205 | pub fn guess_package_name(&self, package: &str) -> String { 206 | let has_known_arch_suffix = constants::KNOWN_ARCHITECTURES 207 | .iter() 208 | .any(|known_arch| package.ends_with(format!(".{known_arch}").as_str())); 209 | 210 | let warn_unknown_arch = || -> String { 211 | warn!( 212 | "unknown architecture for package {package}, {}, {}, {}.", 213 | "consider specifying an arch suffix explicitly", 214 | "such as 'gimp.x86_64-linux'", 215 | "or provide a non-empty '--arch'" 216 | ); 217 | "".into() 218 | }; 219 | 220 | let arch_suffix = match self.arch.clone() { 221 | _ if has_known_arch_suffix => "".into(), 222 | None => warn_unknown_arch(), 223 | // empty --arch is useful for aggregate job such as the channel tests 224 | // e.g. https://hydra.nixos.org/job/nixpkgs/trunk/unstable 225 | Some(arch) if arch.is_empty() => "".into(), 226 | Some(arch) => format!(".{arch}"), 227 | }; 228 | 229 | if package.starts_with("nixpkgs.") || package.starts_with("nixos.") { 230 | // we assume the user knows the full package name 231 | return format!("{package}{arch_suffix}"); 232 | } 233 | 234 | if self.jobset.clone().is_some_and(|x| x.starts_with("nixos/")) { 235 | // we assume that the user searches for a package and not a test 236 | return format!("nixpkgs.{package}{arch_suffix}"); 237 | } 238 | 239 | format!("{package}{arch_suffix}") 240 | } 241 | 242 | fn guess_packages(&self) -> Vec { 243 | self.queries 244 | .iter() 245 | .filter_map(|package| { 246 | if package.starts_with("python3Packages") || package.starts_with("python3.pkgs") { 247 | error!( 248 | "instead of '{package}', you want {}", 249 | "python3xPackages... (e.g. python311Packages)" 250 | ); 251 | None 252 | } else { 253 | Some(self.guess_package_name(package)) 254 | } 255 | }) 256 | .collect() 257 | } 258 | 259 | fn guess_evals(&self) -> Vec { 260 | let mut evals = Vec::new(); 261 | for spec in &self.queries { 262 | evals.push(Evaluation::guess_from_spec(spec)); 263 | } 264 | evals 265 | } 266 | 267 | /// Parses the command line flags and calls [`Self::guess_all_args()`]. 268 | /// Also prints shell completions if asked for. 269 | pub(crate) fn parse_and_guess() -> anyhow::Result { 270 | let args = Self::parse(); 271 | if let Some(shell) = args.shell { 272 | // generate shell completions 273 | let mut cmd = Self::command(); 274 | let bin_name = cmd.get_name().to_string(); 275 | let mut buf = Vec::new(); 276 | clap_complete::generate(shell, &mut cmd, bin_name, &mut buf); 277 | let completion_text = String::from_utf8(buf)?; 278 | print!( 279 | "{}", 280 | match shell { 281 | // hack to provide channel completions for zsh 282 | Shell::Zsh => { 283 | let channel_options = format!( 284 | "CHANNEL:({})", 285 | [ 286 | "nixpkgs-unstable", 287 | "nixos-unstable", 288 | "nixos-unstable-small", 289 | "staging-next", 290 | "stable" 291 | ] 292 | .join(" ") 293 | ); 294 | let arch_options = 295 | format!("ARCH:({})", constants::KNOWN_ARCHITECTURES.join(" ")); 296 | completion_text 297 | .replace("CHANNEL:_default", &channel_options) 298 | .replace("ARCH:_default", &arch_options) 299 | } 300 | _ => completion_text, 301 | } 302 | ); 303 | std::process::exit(0); 304 | } 305 | args.guess_all_args() 306 | } 307 | 308 | /// Guesses all relevant command line arguments and sets the log level. 309 | pub(crate) fn guess_all_args(self) -> anyhow::Result { 310 | let args = self; 311 | let log_level = match args.verbose { 312 | false => log::LevelFilter::Info, 313 | true => log::LevelFilter::Trace, 314 | }; 315 | Logger::with(log_level).format(log_format).start()?; 316 | let args = args.guess_arch(); 317 | let args = args.guess_jobset(); 318 | let queries = match (args.queries.is_empty(), args.eval) { 319 | (true, false) => Queries::Jobset, 320 | // this would resolve to the latest eval of a jobset: 321 | (true, true) => Queries::Evals(vec![Evaluation::guess_from_spec("")]), 322 | (false, true) => Queries::Evals(args.guess_evals()), 323 | (false, false) => Queries::Packages(args.guess_packages()), 324 | }; 325 | Ok(ResolvedArgs { 326 | queries, 327 | url: args.url, 328 | json: args.json, 329 | short: args.short, 330 | jobset: args 331 | .jobset 332 | .expect("jobset should be resolved by `guess_jobset()`"), 333 | }) 334 | } 335 | 336 | /// Runs the program and provides an exit code (with possible errors). 337 | pub fn execute() -> anyhow::Result { 338 | Self::parse_and_guess()?.fetch_and_print() 339 | } 340 | } 341 | 342 | impl ResolvedArgs { 343 | /// Fetches build or evaluation status from hydra.nixos.org 344 | /// and prints the result according to the command line specs. 345 | pub(crate) fn fetch_and_print(&self) -> anyhow::Result { 346 | match &self.queries { 347 | Queries::Jobset => { 348 | self.fetch_and_print_jobset(false)?; 349 | Ok(true) 350 | } 351 | Queries::Packages(packages) => self.fetch_and_print_packages(packages), 352 | Queries::Evals(evals) => self.fetch_and_print_evaluations(evals), 353 | } 354 | } 355 | } 356 | 357 | #[test] 358 | fn guess_jobset() { 359 | let aliases = [ 360 | ("24.05", "nixos/release-24.05"), 361 | ("nixos-23.05", "nixos/release-23.05"), 362 | ("nixos-23.11-small", "nixos/release-23.11-small"), 363 | ]; 364 | for (channel, jobset) in aliases { 365 | eprintln!("{channel} => {jobset}"); 366 | let args = HydraCheckCli::parse_from(["hydra-check", "--channel", channel]).guess_jobset(); 367 | debug_assert_eq!(args.jobset, Some(jobset.into())); 368 | } 369 | } 370 | 371 | #[test] 372 | fn guess_darwin() { 373 | let apple_silicon = "aarch64-darwin"; 374 | if Vec::from(constants::NIXOS_ARCHITECTURES).contains(&apple_silicon) { 375 | // if one day NixOS gains support for the darwin kernel 376 | // (however unlikely), abort this test 377 | return; 378 | } 379 | let args = HydraCheckCli::parse_from(["hydra-check", "--arch", apple_silicon]).guess_jobset(); 380 | // always follow nixpkgs-unstable 381 | debug_assert_eq!(args.jobset, Some("nixpkgs/trunk".into())); 382 | } 383 | 384 | #[test] 385 | #[ignore = "require internet connection"] 386 | fn guess_stable() { 387 | let args = HydraCheckCli::parse_from(["hydra-check", "--channel", "stable"]).guess_jobset(); 388 | eprintln!("{:?}", args.jobset); 389 | } 390 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | //! Useful constants shared across the program 2 | 3 | /// Currently supported systems (`supportedSystems`) on [hydra.nixos.org](https://hydra.nixos.org). 4 | /// 5 | /// This is invoked in the jobset: [nixpkgs/trunk](https://hydra.nixos.org/jobset/nixpkgs/trunk#tabs-configuration), 6 | /// and defined by the following expressions in nixpkgs: 7 | /// - [pkgs/top-level/release.nix](https://github.com/NixOS/nixpkgs/blob/master/pkgs/top-level/release.nix) 8 | /// - [ci/supportedSystems.nix](https://github.com/NixOS/nixpkgs/blob/master/ci/supportedSystems.nix). 9 | /// 10 | /// This may change in the future. 11 | /// 12 | /// ``` 13 | /// assert_eq!(hydra_check::constants::KNOWN_ARCHITECTURES, [ 14 | /// "x86_64-linux", 15 | /// "aarch64-linux", 16 | /// "x86_64-darwin", 17 | /// "aarch64-darwin", 18 | /// ]); 19 | /// ``` 20 | /// 21 | pub const KNOWN_ARCHITECTURES: [&str; 4] = [ 22 | "x86_64-linux", 23 | "aarch64-linux", 24 | "x86_64-darwin", 25 | "aarch64-darwin", 26 | ]; 27 | 28 | /// Currently supported systems (`supportedSystems`) for NixOS. 29 | /// 30 | /// This is invoked in the jobset: [nixos/trunk-combined](https://hydra.nixos.org/jobset/nixos/trunk-combined#tabs-configuration), 31 | /// and defined by the expression: [nixpkgs: nixos/release-combined.nix](https://github.com/NixOS/nixpkgs/blob/master/nixos/release-combined.nix). 32 | /// 33 | /// This may change in the future. 34 | /// 35 | /// ``` 36 | /// assert_eq!(hydra_check::constants::NIXOS_ARCHITECTURES, [ 37 | /// "x86_64-linux", 38 | /// "aarch64-linux", 39 | /// ]); 40 | /// ``` 41 | /// 42 | pub const NIXOS_ARCHITECTURES: [&str; 2] = ["x86_64-linux", "aarch64-linux"]; 43 | 44 | /// Default package filter for the details of a specific evaluation. 45 | pub const DEFAULT_EVALUATION_FILTER: &str = "nixVersions.stable"; 46 | 47 | /// User agent header that we send along to hydra for identifying this app. 48 | /// 49 | /// ``` 50 | /// assert!( 51 | /// hydra_check::constants::APP_USER_AGENT.starts_with("hydra-check/") 52 | /// ); 53 | /// ``` 54 | /// 55 | pub const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); 56 | -------------------------------------------------------------------------------- /src/fetch_stable.rs: -------------------------------------------------------------------------------- 1 | use anyhow::bail; 2 | use log::debug; 3 | use scraper::Html; 4 | use serde::Deserialize; 5 | 6 | use crate::{SoupFind, TryAttr}; 7 | 8 | /// Currently supported Nixpkgs channel version 9 | /// 10 | /// This provides a extremely hacky way of obtaining the latest release 11 | /// number (e.g. 24.05) of Nixpkgs, by parsing the manual on nixos.org. 12 | /// 13 | #[derive(Deserialize, Debug, Clone)] 14 | pub struct NixpkgsChannelVersion { 15 | #[serde(rename = "channel")] 16 | status: String, 17 | version: String, 18 | } 19 | 20 | impl NixpkgsChannelVersion { 21 | fn fetch() -> anyhow::Result> { 22 | debug!("fetching the latest channel version from nixos.org/manual"); 23 | let document = reqwest::blocking::get("https://nixos.org/manual/nixpkgs/stable/")? 24 | .error_for_status()? 25 | .text()?; 26 | let html = Html::parse_document(&document); 27 | let channels_spec = html.find("body")?.try_attr("data-nixpkgs-channels")?; 28 | Ok(serde_json::from_str(channels_spec)?) 29 | } 30 | 31 | fn fetch_channel(spec: &str) -> anyhow::Result { 32 | let channels = Self::fetch()?; 33 | for channel in channels.clone() { 34 | if channel.status == spec { 35 | return Ok(channel.version); 36 | } 37 | } 38 | bail!( 39 | "could not find '{spec}' from supported channels: {:?}", 40 | channels 41 | ) 42 | } 43 | 44 | /// Fetches the current stable version number of Nixpkgs 45 | pub fn stable() -> anyhow::Result { 46 | Self::fetch_channel("stable") 47 | } 48 | } 49 | 50 | #[test] 51 | #[ignore = "require internet connection"] 52 | fn fetch_stable() { 53 | let ver = NixpkgsChannelVersion::stable().unwrap(); 54 | println!("latest stable version: {ver}"); 55 | debug_assert!(regex::Regex::new(r"^[0-9]+\.[0-9]+") 56 | .unwrap() 57 | .is_match(&ver)); 58 | } 59 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | 3 | //!
4 | //! 5 | //! Currently hydra-check is only provided as a CLI application, _not_ as a library. 6 | //! We exposed the library documentation here for internal reference. 7 | //! We do _not_ provide any guarantee about the stability of these interfaces. 8 | //! 9 | //!
10 | //! 11 | #![allow(clippy::doc_markdown)] 12 | #![doc = include_str!("../README.md")] 13 | 14 | mod args; 15 | mod fetch_stable; 16 | mod queries; 17 | mod structs; 18 | 19 | pub mod constants; 20 | pub mod soup; 21 | 22 | pub use args::HydraCheckCli; 23 | pub use fetch_stable::NixpkgsChannelVersion; 24 | 25 | use args::ResolvedArgs; 26 | use soup::{SoupFind, TryAttr}; 27 | use structs::{BuildStatus, EvalInput, Evaluation, StatusIcon}; 28 | 29 | use colored::{ColoredString, Colorize}; 30 | use comfy_table::Table; 31 | use scraper::{ElementRef, Html}; 32 | use std::time::Duration; 33 | 34 | /// Trait for a single `Status` entry from a Hydra report. 35 | /// This usually corresponds to a single line in the tables from Hydra's 36 | /// web interface, such as a single [`BuildStatus`]. 37 | trait ShowHydraStatus { 38 | fn format_as_vec(&self) -> Vec; 39 | } 40 | 41 | /// Trait for all kinds of Hydra `Report`. 42 | /// This usually corresponds to a summary page from Hydra's web interface, 43 | /// such as . 44 | trait FetchHydraReport: Clone { 45 | fn get_url(&self) -> &str; 46 | fn fetch_document(&self) -> anyhow::Result { 47 | let document = reqwest::blocking::Client::builder() 48 | .timeout(Duration::from_secs(30)) 49 | .user_agent(constants::APP_USER_AGENT) 50 | .build()? 51 | .get(self.get_url()) 52 | .send()? 53 | .error_for_status()? 54 | .text()?; 55 | Ok(Html::parse_document(&document)) 56 | } 57 | 58 | fn finish_with_error(self, status: String) -> Self; 59 | 60 | /// Checks if the fetched [Html] contains a `tbody` tag (table body). 61 | /// If not, returns the alert text. If yes, returns the found element. 62 | fn find_tbody<'a>(&self, doc: &'a Html, selector: &str) -> Result, Self> { 63 | let selectors = format!("{selector} tbody"); 64 | match doc.find(selectors.trim()) { 65 | Err(_) => { 66 | // either the package was not evaluated (due to being e.g. unfree) 67 | // or the package does not exist 68 | let status = if let Ok(alert) = doc.find("div.alert") { 69 | alert.text().collect() 70 | } else { 71 | format!( 72 | "Unknown Hydra Error with '{}' found at {}", 73 | selectors, 74 | self.get_url() 75 | ) 76 | }; 77 | // sanitize the text a little bit 78 | let status: Vec<&str> = status.lines().map(str::trim).collect(); 79 | let status: String = status.join(" "); 80 | Err(self.clone().finish_with_error(status)) 81 | } 82 | Ok(tbody) => Ok(tbody), 83 | } 84 | } 85 | 86 | fn format_table(&self, short: bool, entries: &Vec) -> String { 87 | let mut table = Table::new(); 88 | table.load_preset(comfy_table::presets::NOTHING); 89 | // .set_content_arrangement(comfy_table::ContentArrangement::Dynamic); 90 | for entry in entries { 91 | table.add_row(entry.format_as_vec()); 92 | if short { 93 | break; 94 | } 95 | } 96 | for (idx, column) in table.column_iter_mut().enumerate() { 97 | if idx == 0 { 98 | column.set_padding((0, 1)); 99 | } 100 | // column.set_constraint(comfy_table::ColumnConstraint::ContentWidth); 101 | } 102 | table.trim_fmt() 103 | } 104 | } 105 | 106 | fn is_skipable_row(row: ElementRef<'_>) -> anyhow::Result { 107 | let link = row.find("td")?.find("a")?.try_attr("href")?; 108 | let skipable = link.ends_with("/all") || link.contains("full=1"); 109 | Ok(skipable) 110 | } 111 | 112 | fn log_format( 113 | w: &mut dyn std::io::Write, 114 | _now: &mut flexi_logger::DeferredNow, 115 | record: &log::Record, 116 | ) -> Result<(), std::io::Error> { 117 | let level = record.level(); 118 | let color = match level { 119 | log::Level::Error => "red", 120 | log::Level::Warn => "yellow", 121 | _ => "", 122 | }; 123 | let level = format!("{level}:").to_lowercase().color(color).bold(); 124 | write!(w, "{} {}", level, &record.args()) 125 | } 126 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use hydra_check::HydraCheckCli; 2 | 3 | fn main() -> anyhow::Result<()> { 4 | let success = HydraCheckCli::execute()?; 5 | if !success { 6 | std::process::exit(1); 7 | } 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /src/queries/builds.rs: -------------------------------------------------------------------------------- 1 | //! A module that formats the details of one (or multiple) build(s), 2 | //! from urls such as . 3 | //! 4 | //! This module is adapted from the `evals` module as the two are similar 5 | //! in structure. The module is currently only used by the `builds` module, 6 | //! hence the relevant interfaces are marked as `pub(super)`. 7 | 8 | use serde::Serialize; 9 | 10 | use crate::{EvalInput, FetchHydraReport, StatusIcon}; 11 | 12 | #[non_exhaustive] 13 | #[derive(Serialize, Clone)] 14 | pub(super) struct BuildReport { 15 | url: String, 16 | pub(super) inputs: Vec, 17 | } 18 | 19 | impl FetchHydraReport for BuildReport { 20 | fn get_url(&self) -> &str { 21 | &self.url 22 | } 23 | 24 | fn finish_with_error(self, status: String) -> Self { 25 | Self { 26 | inputs: vec![EvalInput { 27 | name: Some(StatusIcon::Warning.to_string()), 28 | value: Some(status), 29 | ..Default::default() 30 | }], 31 | ..self 32 | } 33 | } 34 | } 35 | 36 | impl BuildReport { 37 | pub(super) fn from_url(url: &str) -> Self { 38 | Self { 39 | url: url.to_string(), 40 | inputs: vec![], 41 | } 42 | } 43 | 44 | pub(super) fn fetch_and_read(self) -> anyhow::Result { 45 | let doc = self.fetch_document()?; 46 | let tbody = match self.find_tbody(&doc, "div#tabs-buildinputs") { 47 | // inputs are essential information, so exit early if this fails: 48 | Err(stat) => return Ok(stat), 49 | Ok(tbody) => tbody, 50 | }; 51 | let inputs = EvalInput::from_tbody(tbody, &self.url)?; 52 | Ok(Self { inputs, ..self }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/queries/evals.rs: -------------------------------------------------------------------------------- 1 | //! A module that formats the details of one (or multiple) evaluation(s), 2 | //! from urls such as . 3 | //! 4 | //! This is a long pile of spaghetti that serves a single purpose. 5 | //! Splitting it into separate modules would also be clumsy as we then need 6 | //! to add a bunch of `pub(super)` identifiers to the various data structures. 7 | 8 | use anyhow::{anyhow, bail}; 9 | use colored::Colorize; 10 | use indexmap::IndexMap; 11 | use log::{info, warn}; 12 | use regex::Regex; 13 | use scraper::Html; 14 | use serde::Serialize; 15 | use serde_json::Value; 16 | use serde_with::skip_serializing_none; 17 | use std::fmt::Display; 18 | 19 | #[cfg(test)] 20 | use insta::assert_snapshot; 21 | 22 | use crate::{ 23 | BuildStatus, EvalInput, Evaluation, FetchHydraReport, ResolvedArgs, SoupFind, StatusIcon, 24 | }; 25 | 26 | #[skip_serializing_none] 27 | #[derive(Serialize, Clone)] 28 | struct EvalInputChanges { 29 | input: String, 30 | description: String, 31 | url: Option, 32 | revs: Option<(String, String)>, 33 | short_revs: Option<(String, String)>, 34 | } 35 | 36 | impl Display for EvalInputChanges { 37 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 | let json = serde_json::to_string(&self).expect("EvalInput should be serialized into json"); 39 | let json: Value = serde_json::from_str(&json).unwrap(); 40 | let strings: Vec<_> = ["input", "description", "url", "revs"] 41 | .iter() 42 | .filter_map(|key| match &json[key] { 43 | Value::Null => None, 44 | // unquote the string: 45 | Value::String(value) => { 46 | let key = match *key { 47 | "input" => "changed_input", 48 | "description" => "changes", 49 | k => k, 50 | }; 51 | Some(format!("{}: {}", key.bold(), value)) 52 | } 53 | Value::Array(vec) => { 54 | let texts: Vec<_> = vec.iter().filter_map(|x| x.as_str()).collect(); 55 | Some(format!("{}: {}", key.bold(), texts.join(" -> "))) 56 | } 57 | value => Some(format!("{}: {}", key.bold(), value)), 58 | }) 59 | .collect(); 60 | write!(f, "{}", strings.join("\n")) 61 | } 62 | } 63 | 64 | #[test] 65 | fn format_input_changes() { 66 | let input_changes = EvalInputChanges { 67 | input: "nixpkgs".into(), 68 | description: "8c4dc69b9732 to 1e9e641a3fc1".into(), 69 | url: Some("https://hydra.nixos.org/api/scmdiff?blah/blah/blah".into()), 70 | revs: Some(( 71 | "8c4dc69b9732f6bbe826b5fbb32184987520ff26".into(), 72 | "1e9e641a3fc1b22fbdb823a99d8ff96692cc4fba".into(), 73 | )), 74 | short_revs: Some(("8c4dc69b9732".into(), "1e9e641a3fc1".into())), 75 | }; 76 | assert_snapshot!(input_changes.to_string(), @r#" 77 | changed_input: nixpkgs 78 | changes: 8c4dc69b9732 to 1e9e641a3fc1 79 | url: https://hydra.nixos.org/api/scmdiff?blah/blah/blah 80 | revs: 8c4dc69b9732f6bbe826b5fbb32184987520ff26 -> 1e9e641a3fc1b22fbdb823a99d8ff96692cc4fba 81 | "#); 82 | } 83 | 84 | impl EvalInputChanges { 85 | #[allow(clippy::similar_names)] 86 | fn from_html(doc: &Html) -> anyhow::Result> { 87 | let tables = doc.find_all("div#tabs-inputs table"); 88 | #[allow(clippy::redundant_closure_for_method_calls)] 89 | let err = || { 90 | anyhow!( 91 | "could not parse the table of changed inputs in {:?}", 92 | tables.iter().map(|x| x.html()).collect::>() 93 | ) 94 | }; 95 | // table of input changes: 96 | let table = tables.get(1).ok_or_else(err)?; 97 | let thead: Vec = table 98 | .find("tr")? 99 | .find_all("th") 100 | .iter() 101 | .map(|x| x.text().collect()) 102 | .collect(); 103 | if !thead 104 | .iter() 105 | .all(|x| x.trim().contains("Input") || x.trim().contains("Changes")) 106 | { 107 | bail!(err()); 108 | } 109 | let tbody = table.find_all("tr"); 110 | let rows = tbody.get(1..).ok_or_else(err)?; 111 | let mut input_changes = Vec::new(); 112 | 113 | let regex_short_revs = Regex::new("^([0-9a-z]+) to ([0-9a-z]+)$").unwrap(); 114 | let [regex_rev1, regex_rev2] = ["rev1", "rev2"].map(|rev_id| { 115 | let re = format!("^.*{rev_id}=([0-9a-z]+).*$"); 116 | Regex::new(&re).unwrap() 117 | }); 118 | 119 | for row in rows { 120 | let columns = row.find_all("td"); 121 | let mut columns = columns.iter(); 122 | let input: String = columns.next().ok_or_else(err)?.text().collect(); 123 | let input = input.trim().to_string(); 124 | 125 | let changes = columns.next().ok_or_else(err)?; 126 | let description: String = changes.text().collect(); 127 | let description = description.trim().to_string(); 128 | 129 | // the following entries are non-essential, 130 | // so we avoid using `?` for premature exits 131 | let url = changes 132 | .find("a") 133 | .ok() 134 | .and_then(|x| x.attr("href")) 135 | .map(str::to_string); 136 | 137 | let revs = if let Some(url) = &url { 138 | // note that the returned url is not deterministic: 139 | // the position of the query parameters may float around 140 | let [rev1, rev2] = [®ex_rev1, ®ex_rev2].map(|regex_rev| { 141 | match regex_rev.captures(url).map(|x| x.extract()) { 142 | Some((_, [rev])) if !rev.is_empty() => Some(rev.to_string()), 143 | _ => None, 144 | } 145 | }); 146 | 147 | match (rev1, rev2) { 148 | (Some(rev1), Some(rev2)) => Some((rev1, rev2)), 149 | _ => None, 150 | } 151 | } else { 152 | None 153 | }; 154 | 155 | let short_revs = if description.is_empty() { 156 | None 157 | } else { 158 | match regex_short_revs.captures(&description).map(|x| x.extract()) { 159 | Some((_, [rev1, rev2])) if (!rev1.is_empty()) && (!rev2.is_empty()) => { 160 | Some((rev1.to_string(), rev2.to_string())) 161 | } 162 | _ => None, 163 | } 164 | }; 165 | 166 | input_changes.push(EvalInputChanges { 167 | input, 168 | description, 169 | url, 170 | revs, 171 | short_revs, 172 | }); 173 | } 174 | Ok(input_changes) 175 | } 176 | } 177 | 178 | #[derive(Serialize, Clone)] 179 | struct EvalReport<'a> { 180 | #[serde(flatten)] 181 | eval: &'a Evaluation, 182 | url: String, 183 | inputs: Vec, 184 | changes: Vec, 185 | aborted: Vec, 186 | now_fail: Vec, 187 | now_succeed: Vec, 188 | new: Vec, 189 | removed: Vec, 190 | still_fail: Vec, 191 | still_succeed: Vec, 192 | unfinished: Vec, 193 | } 194 | 195 | impl FetchHydraReport for EvalReport<'_> { 196 | fn get_url(&self) -> &str { 197 | &self.url 198 | } 199 | 200 | fn finish_with_error(self, status: String) -> Self { 201 | Self { 202 | inputs: vec![EvalInput { 203 | name: Some(StatusIcon::Warning.to_string()), 204 | value: Some(status), 205 | ..Default::default() 206 | }], 207 | ..self 208 | } 209 | } 210 | } 211 | 212 | impl<'a> From<&'a Evaluation> for EvalReport<'a> { 213 | fn from(eval: &'a Evaluation) -> Self { 214 | let url = format!("https://hydra.nixos.org/eval/{}", eval.id); 215 | let url = match &eval.filter { 216 | Some(x) => format!("{url}?filter={x}"), 217 | None => url, 218 | }; 219 | Self { 220 | eval, 221 | url, 222 | inputs: vec![], 223 | changes: vec![], 224 | aborted: vec![], 225 | now_fail: vec![], 226 | now_succeed: vec![], 227 | new: vec![], 228 | removed: vec![], 229 | still_fail: vec![], 230 | still_succeed: vec![], 231 | unfinished: vec![], 232 | } 233 | } 234 | } 235 | 236 | impl EvalReport<'_> { 237 | fn parse_build_stats(&self, doc: &Html, selector: &str) -> anyhow::Result> { 238 | let err = || { 239 | anyhow!( 240 | "could not parse the table of build stats '{:?}' in {}", 241 | selector, 242 | doc.html() 243 | ) 244 | }; 245 | let tbody = match self.find_tbody(doc, selector) { 246 | Err(stat) => bail!("{:?}", stat.inputs.first().ok_or_else(err)?.value), 247 | Ok(tbody) => tbody, 248 | }; 249 | BuildStatus::from_tbody(tbody) 250 | } 251 | 252 | fn fetch_and_read(self) -> anyhow::Result { 253 | let doc = self.fetch_document()?; 254 | let tbody = match self.find_tbody(&doc, "div#tabs-inputs") { 255 | // inputs are essential information, so exit early if this fails: 256 | Err(stat) => return Ok(stat), 257 | Ok(tbody) => tbody, 258 | }; 259 | let inputs = EvalInput::from_tbody(tbody, self.eval.id.to_string().as_str())?; 260 | let changes = EvalInputChanges::from_html(&doc).unwrap_or_else(|err| { 261 | warn!("{}\n{}", err, err.backtrace()); 262 | vec![] 263 | }); 264 | 265 | let [aborted, now_fail, now_succeed, new, removed, still_fail, still_succeed, unfinished] = 266 | [ 267 | "aborted", 268 | "now-fail", 269 | "now-succeed", 270 | "new", 271 | "removed", 272 | "still-fail", 273 | "still-succeed", 274 | "unfinished", 275 | ] 276 | .map(|selector| { 277 | let selector = format!("div#tabs-{selector}"); 278 | self.parse_build_stats(&doc, &selector) 279 | .unwrap_or_else(|err| { 280 | warn!("{}\n{}", err, err.backtrace()); 281 | vec![] 282 | }) 283 | }); 284 | 285 | Ok(Self { 286 | inputs, 287 | changes, 288 | aborted, 289 | now_fail, 290 | now_succeed, 291 | new, 292 | removed, 293 | still_fail, 294 | still_succeed, 295 | unfinished, 296 | ..self 297 | }) 298 | } 299 | } 300 | 301 | impl ResolvedArgs { 302 | pub(crate) fn fetch_and_print_evaluations(&self, evals: &[Evaluation]) -> anyhow::Result { 303 | let mut indexmap = IndexMap::new(); 304 | let evals: Vec<_> = match evals.iter().any(|eval| eval.id == 0) { 305 | false => evals.to_owned(), 306 | true => { 307 | info!( 308 | "querying the latest evaluation of --jobset '{}'", 309 | self.jobset 310 | ); 311 | let err = || { 312 | anyhow!( 313 | "could not find the latest evaluation for --jobset '{}'", 314 | self.jobset 315 | ) 316 | }; 317 | eprintln!(); 318 | let id = self.fetch_and_print_jobset(false)?.ok_or_else(err)?; 319 | println!(); 320 | evals 321 | .iter() 322 | .map(|eval| match &eval.id { 323 | 0 => Evaluation { id, ..eval.clone() }, 324 | _ => eval.clone(), 325 | }) 326 | .collect() 327 | } 328 | }; 329 | for (idx, eval) in evals.iter().enumerate() { 330 | let stat = EvalReport::from(eval); 331 | if self.url { 332 | println!("{}", stat.get_url()); 333 | continue; 334 | } 335 | if !self.json { 336 | // print title first, then fetch 337 | if idx > 0 && !self.short { 338 | println!(); // vertical whitespace 339 | } 340 | println!( 341 | "Evaluation {}{} {}", 342 | stat.eval.id.to_string().bold(), 343 | match &stat.eval.filter { 344 | Some(x) => format!(" filtered by '{}'", x.bold()), 345 | None => "".into(), 346 | }, 347 | format!("@ {}", stat.get_url()).dimmed(), 348 | ); 349 | } 350 | let stat = stat.fetch_and_read()?; 351 | if self.json { 352 | indexmap.insert(&stat.eval.spec, stat); 353 | continue; 354 | } 355 | for entry in &stat.inputs { 356 | println!(); // vertical separation 357 | println!("{entry}"); 358 | } 359 | for entry in &stat.changes { 360 | println!(); // vertical separation 361 | println!("{entry}"); 362 | } 363 | if self.short { 364 | continue; 365 | } 366 | for (build_stats, prompt) in [ 367 | (&stat.aborted, "Aborted / Timed out:".bold()), 368 | (&stat.now_fail, "Newly Failing:".bold()), 369 | (&stat.now_succeed, "Newly Succeeding:".bold()), 370 | (&stat.new, "New Jobs:".bold()), 371 | (&stat.removed, "Removed Jobs:".bold()), 372 | (&stat.still_fail, "Still Failing:".bold()), 373 | (&stat.still_succeed, "Still Succeeding:".bold()), 374 | (&stat.unfinished, "Queued Jobs:".bold()), 375 | ] { 376 | #[allow(clippy::uninlined_format_args)] 377 | if !build_stats.is_empty() { 378 | println!(); 379 | println!("{}", prompt); 380 | println!("{}", stat.format_table(false, build_stats)); 381 | } 382 | } 383 | } 384 | if self.json { 385 | println!("{}", serde_json::to_string_pretty(&indexmap)?); 386 | } 387 | Ok(true) 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/queries/jobset.rs: -------------------------------------------------------------------------------- 1 | //! A module that formats the details of the specified (or inferred) jobset, 2 | //! from an url like: . 3 | 4 | use anyhow::bail; 5 | use colored::{ColoredString, Colorize}; 6 | use indexmap::IndexMap; 7 | use serde::Serialize; 8 | use serde_with::skip_serializing_none; 9 | 10 | use crate::{ 11 | is_skipable_row, FetchHydraReport, ResolvedArgs, ShowHydraStatus, SoupFind, StatusIcon, TryAttr, 12 | }; 13 | 14 | #[skip_serializing_none] 15 | #[derive(Serialize, Debug, Default, Clone)] 16 | /// Status of a single evaluation, can be serialized to a JSON entry 17 | struct EvalStatus { 18 | icon: StatusIcon, 19 | finished: Option, 20 | id: Option, 21 | url: Option, 22 | datetime: Option, 23 | relative: Option, 24 | timestamp: Option, 25 | status: String, 26 | short_rev: Option, 27 | input_changes: Option, 28 | succeeded: Option, 29 | failed: Option, 30 | queued: Option, 31 | delta: Option, 32 | } 33 | 34 | impl ShowHydraStatus for EvalStatus { 35 | fn format_as_vec(&self) -> Vec { 36 | let mut row = Vec::new(); 37 | let icon = ColoredString::from(&self.icon); 38 | let description = match &self.input_changes { 39 | Some(x) => x, 40 | None => &self.status, 41 | }; 42 | row.push(format!("{icon} {description}").into()); 43 | let details = if self.url.is_some() { 44 | let relative = self.relative.clone().unwrap_or_default().into(); 45 | let statistics = [ 46 | (StatusIcon::Succeeded, self.succeeded), 47 | (StatusIcon::Failed, self.failed), 48 | (StatusIcon::Queued, self.queued), 49 | ]; 50 | let [suceeded, failed, queued] = statistics.map(|(icon, text)| -> ColoredString { 51 | format!( 52 | "{} {}", 53 | ColoredString::from(&icon), 54 | text.unwrap_or_default() 55 | ) 56 | .into() 57 | }); 58 | let queued = match self.queued.unwrap_or_default() { 59 | x if x != 0 => queued.bold(), 60 | _ => queued.normal(), 61 | }; 62 | let delta = format!( 63 | "Δ {}", 64 | match self.delta.clone().unwrap_or("~".into()).trim() { 65 | x if x.starts_with('+') => x.green(), 66 | x if x.starts_with('-') => x.red(), 67 | x => x.into(), 68 | } 69 | ) 70 | .into(); 71 | let url = self.url.clone().unwrap_or_default().dimmed(); 72 | &[relative, suceeded, failed, queued, delta, url] 73 | } else { 74 | &Default::default() 75 | }; 76 | row.extend_from_slice(details); 77 | row 78 | } 79 | } 80 | 81 | #[derive(Clone)] 82 | /// Container for the eval status and metadata of a jobset 83 | struct JobsetReport<'a> { 84 | jobset: &'a str, 85 | url: String, 86 | /// Status of recent evaluations of the jobset 87 | evals: Vec, 88 | } 89 | 90 | impl FetchHydraReport for JobsetReport<'_> { 91 | fn get_url(&self) -> &str { 92 | &self.url 93 | } 94 | 95 | fn finish_with_error(self, status: String) -> Self { 96 | Self { 97 | evals: vec![EvalStatus { 98 | icon: StatusIcon::Warning, 99 | status, 100 | ..Default::default() 101 | }], 102 | ..self 103 | } 104 | } 105 | } 106 | 107 | impl<'a> From<&'a ResolvedArgs> for JobsetReport<'a> { 108 | fn from(args: &'a ResolvedArgs) -> Self { 109 | // 110 | // https://hydra.nixos.org/jobset/nixpkgs/trunk/evals 111 | // 112 | let url = format!("https://hydra.nixos.org/jobset/{}/evals", args.jobset); 113 | Self { 114 | jobset: &args.jobset, 115 | url, 116 | evals: vec![], 117 | } 118 | } 119 | } 120 | 121 | impl JobsetReport<'_> { 122 | fn fetch_and_read(self) -> anyhow::Result { 123 | let doc = self.fetch_document()?; 124 | let tbody = match self.find_tbody(&doc, "") { 125 | Err(stat) => return Ok(stat), 126 | Ok(tbody) => tbody, 127 | }; 128 | let mut evals: Vec = Vec::new(); 129 | for row in tbody.find_all("tr") { 130 | let columns = row.find_all("td"); 131 | let [eval_id, timestamp, input_changes, succeeded, failed, queued, delta] = 132 | columns.as_slice() 133 | else { 134 | #[allow(clippy::redundant_else)] 135 | if is_skipable_row(row)? { 136 | continue; 137 | } else { 138 | bail!( 139 | "error while parsing Hydra status for jobset '{}': {:?}", 140 | self.jobset, 141 | row 142 | ); 143 | } 144 | }; 145 | 146 | let url = eval_id.find("a")?.try_attr("href")?; 147 | let eval_id: String = eval_id.text().collect(); 148 | let id: u64 = eval_id.parse()?; 149 | 150 | let time = timestamp.find("time")?; 151 | let date = time.try_attr("datetime")?; 152 | let relative = time.text().collect(); 153 | let timestamp = time.try_attr("data-timestamp")?; 154 | let timestamp: u64 = timestamp.parse()?; 155 | 156 | let status: String = input_changes 157 | .find("span") 158 | .map(|x| x.text().collect()) 159 | .unwrap_or_default(); 160 | 161 | let short_rev = input_changes.find("tt")?.text().collect(); 162 | let input_changes = { 163 | let text: String = input_changes.text().collect(); 164 | let text = text.replace(&status, ""); 165 | let texts: Vec<_> = text.split_whitespace().collect(); 166 | texts.join(" ") 167 | }; 168 | 169 | let [succeeded, failed, queued, delta] = [succeeded, failed, queued, delta].map(|x| { 170 | let text: String = x.text().collect(); 171 | text.trim().to_string() 172 | }); 173 | 174 | let [succeeded, failed, queued]: [Result; 3] = 175 | [succeeded, failed, queued].map(|text| match text.is_empty() { 176 | true => Ok(0), 177 | false => text.parse(), 178 | }); 179 | let delta = match delta { 180 | x if x.is_empty() => None, 181 | x => Some(x), 182 | }; 183 | 184 | let finished = queued == Ok(0); 185 | let icon = match finished { 186 | true => StatusIcon::Succeeded, 187 | false => StatusIcon::Queued, 188 | }; 189 | 190 | evals.push(EvalStatus { 191 | icon, 192 | finished: Some(finished), 193 | id: Some(id), 194 | url: Some(url.into()), 195 | datetime: Some(date.into()), 196 | relative: Some(relative), 197 | timestamp: Some(timestamp), 198 | status, 199 | short_rev: Some(short_rev), 200 | input_changes: Some(input_changes), 201 | succeeded: Some(succeeded?), 202 | failed: Some(failed?), 203 | queued: Some(queued?), 204 | delta, 205 | }); 206 | } 207 | Ok(Self { evals, ..self }) 208 | } 209 | } 210 | 211 | impl ResolvedArgs { 212 | pub(crate) fn fetch_and_print_jobset( 213 | &self, 214 | force_summary: bool, 215 | ) -> anyhow::Result> { 216 | let stat = JobsetReport::from(self); 217 | let (short, json) = match force_summary { 218 | true => (true, false), 219 | false => (self.short, self.json), 220 | }; 221 | if self.url { 222 | println!("{}", stat.get_url()); 223 | return Ok(None); 224 | } 225 | if !json { 226 | // print title first, then fetch 227 | println!( 228 | "Evaluations of jobset {} {}", 229 | self.jobset.bold(), 230 | format!("@ {}", stat.get_url()).dimmed() 231 | ); 232 | } 233 | let stat = stat.fetch_and_read()?; 234 | let first_stat = stat.evals.first(); 235 | let latest_id = first_stat.and_then(|x| x.id); 236 | if json { 237 | let mut indexmap = IndexMap::new(); 238 | match short { 239 | true => indexmap.insert( 240 | &stat.jobset, 241 | match first_stat { 242 | Some(x) => vec![x.to_owned()], 243 | None => vec![], 244 | }, 245 | ), 246 | false => indexmap.insert(&stat.jobset, stat.evals), 247 | }; 248 | println!("{}", serde_json::to_string_pretty(&indexmap)?); 249 | return Ok(latest_id); 250 | } 251 | println!("{}", stat.format_table(short, &stat.evals)); 252 | Ok(latest_id) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/queries/mod.rs: -------------------------------------------------------------------------------- 1 | mod builds; 2 | mod evals; 3 | mod jobset; 4 | mod packages; 5 | -------------------------------------------------------------------------------- /src/queries/packages.rs: -------------------------------------------------------------------------------- 1 | //! A module that formats the details of the specified (or inferred) packages, 2 | //! e.g. from . 3 | 4 | use colored::Colorize; 5 | use indexmap::IndexMap; 6 | use log::info; 7 | 8 | use super::builds::BuildReport; 9 | use crate::{structs::BuildStatus, FetchHydraReport, ResolvedArgs, StatusIcon}; 10 | 11 | #[derive(Clone)] 12 | /// Container for the build status and metadata of a package 13 | struct PackageReport<'a> { 14 | package: &'a str, 15 | url: String, 16 | /// Status of recent builds of the package 17 | builds: Vec, 18 | } 19 | 20 | impl FetchHydraReport for PackageReport<'_> { 21 | fn get_url(&self) -> &str { 22 | &self.url 23 | } 24 | 25 | fn finish_with_error(self, status: String) -> Self { 26 | Self { 27 | builds: vec![BuildStatus { 28 | icon: StatusIcon::Warning, 29 | status, 30 | ..Default::default() 31 | }], 32 | ..self 33 | } 34 | } 35 | } 36 | 37 | impl<'a> PackageReport<'a> { 38 | /// Initializes the status container with the resolved package name 39 | /// and the resolved command line arguments. 40 | fn from_package_with_args(package: &'a str, args: &'a ResolvedArgs) -> Self { 41 | // 42 | // Examples: 43 | // - https://hydra.nixos.org/job/nixos/release-19.09/nixpkgs.hello.x86_64-linux/latest 44 | // - https://hydra.nixos.org/job/nixos/release-19.09/nixos.tests.installer.simpleUefiGrub.aarch64-linux 45 | // - https://hydra.nixos.org/job/nixpkgs/trunk/hello.x86_64-linux/all 46 | // 47 | // There is also {url}/all which is a lot slower. 48 | // 49 | let url = format!("https://hydra.nixos.org/job/{}/{}", args.jobset, package); 50 | Self { 51 | package, 52 | url, 53 | builds: vec![], 54 | } 55 | } 56 | 57 | fn fetch_and_read(self) -> anyhow::Result { 58 | let doc = self.fetch_document()?; 59 | let tbody = match self.find_tbody(&doc, "") { 60 | Err(stat) => return Ok(stat), 61 | Ok(tbody) => tbody, 62 | }; 63 | let builds = BuildStatus::from_tbody(tbody)?; 64 | Ok(Self { builds, ..self }) 65 | } 66 | } 67 | 68 | impl ResolvedArgs { 69 | pub(crate) fn fetch_and_print_packages(&self, packages: &[String]) -> anyhow::Result { 70 | let mut status = true; 71 | let mut indexmap = IndexMap::new(); 72 | for (idx, package) in packages.iter().enumerate() { 73 | let stat = PackageReport::from_package_with_args(package, self); 74 | if self.url { 75 | println!("{}", stat.get_url()); 76 | continue; 77 | } 78 | let url_dimmed = stat.get_url().dimmed(); 79 | if !self.json { 80 | // print title first, then fetch 81 | if idx > 0 && !self.short { 82 | println!(); // vertical whitespace 83 | } 84 | println!( 85 | "Build Status for {} on jobset {}", 86 | stat.package.bold(), 87 | self.jobset.bold(), 88 | ); 89 | if !self.short { 90 | println!("{url_dimmed}"); 91 | } 92 | } 93 | let stat = stat.fetch_and_read()?; 94 | let first_stat = stat.builds.first(); 95 | let success = first_stat.is_some_and(|build| build.success); 96 | if !success { 97 | status = false; 98 | } 99 | if self.json { 100 | match self.short { 101 | true => indexmap.insert( 102 | stat.package, 103 | match first_stat { 104 | Some(x) => vec![x.to_owned()], 105 | None => vec![], 106 | }, 107 | ), 108 | false => indexmap.insert(stat.package, stat.builds), 109 | }; 110 | continue; // print later 111 | } 112 | println!("{}", stat.format_table(self.short, &stat.builds)); 113 | if !success { 114 | if self.short { 115 | info!("latest build failed, check out: {url_dimmed}"); 116 | } else { 117 | eprintln!("\n{}", "Links:".bold()); 118 | #[rustfmt::skip] 119 | eprintln!( 120 | "{} (all builds)", 121 | format!("🔗 {url_dimmed}/all").dimmed() 122 | ); 123 | eprintln!( 124 | "{} (latest successful build)", 125 | format!("🔗 {url_dimmed}/latest").dimmed() 126 | ); 127 | eprintln!( 128 | "{} (latest success from a finished eval)", 129 | format!("🔗 {url_dimmed}/latest-finished").dimmed() 130 | ); 131 | 132 | eprintln!(); 133 | } 134 | info!("showing inputs for the latest success from a finished eval..."); 135 | 136 | let url = format!("{}/latest-finished", stat.get_url()); 137 | let build_report = BuildReport::from_url(&url); 138 | let build_report = build_report.fetch_and_read()?; 139 | for entry in &build_report.inputs { 140 | if self.short { 141 | if let (Some(name), Some(rev)) = (&entry.name, &entry.revision) { 142 | println!("{name}: {rev}"); 143 | } 144 | } else { 145 | println!(); // vertical separation 146 | println!("{entry}"); 147 | } 148 | } 149 | } 150 | } 151 | if self.json { 152 | println!("{}", serde_json::to_string_pretty(&indexmap)?); 153 | } 154 | Ok(status) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/soup.rs: -------------------------------------------------------------------------------- 1 | //! Convenient extensions for the querying interface of [`scraper`] 2 | 3 | #[doc(inline)] 4 | pub use scraper; 5 | 6 | use anyhow::anyhow; 7 | use scraper::{selectable::Selectable, ElementRef, Html, Selector}; 8 | use std::fmt::Debug; 9 | 10 | #[allow(clippy::module_name_repetitions)] 11 | /// A simple wrapper trait that provides the `find` and `find_all` methods 12 | /// to [`scraper`]'s [`Selectable`] elements, inspired by the interface of 13 | /// Python's `BeautifulSoup`. 14 | pub trait SoupFind<'a> { 15 | /// Finds all child elements matching the CSS selectors 16 | /// and collect them into a [`Vec`]. 17 | fn find_all(self, selectors: &str) -> Vec>; 18 | 19 | /// Finds the first element that matches the CSS selectors, 20 | /// returning [`anyhow::Error`] if not found. 21 | fn find(self, selectors: &str) -> anyhow::Result>; 22 | } 23 | 24 | trait AsHtml { 25 | fn as_html(&self) -> String; 26 | } 27 | 28 | impl<'a, T: Selectable<'a> + Debug + AsHtml> SoupFind<'a> for T { 29 | fn find_all(self, selectors: &str) -> Vec> { 30 | let selector = Selector::parse(selectors).expect("the selector should be valid"); 31 | self.select(&selector).collect() 32 | } 33 | 34 | fn find(self, selectors: &str) -> anyhow::Result> { 35 | let selector = Selector::parse(selectors).expect("the selector should be valid"); 36 | let err = anyhow!( 37 | "could not select '{:?}' in '{:?}'", 38 | selector, 39 | self.as_html() 40 | ); 41 | let element = self.select(&selector).next().ok_or(err)?; 42 | Ok(element) 43 | } 44 | } 45 | 46 | impl AsHtml for &Html { 47 | fn as_html(&self) -> String { 48 | self.html() 49 | } 50 | } 51 | 52 | impl AsHtml for ElementRef<'_> { 53 | fn as_html(&self) -> String { 54 | self.html() 55 | } 56 | } 57 | 58 | /// A trivial wrapper trait for [`scraper`]'s [`.attr()`][ElementRef::attr] 59 | /// that returns an [`anyhow::Result`] instead of an [`Option`]. 60 | pub trait TryAttr<'a> { 61 | /// Calls [`.attr`][ElementRef::attr] and errors out if there is [`None`]. 62 | fn try_attr(&self, attr: &str) -> anyhow::Result<&'a str>; 63 | } 64 | 65 | impl<'a> TryAttr<'a> for ElementRef<'a> { 66 | fn try_attr(&self, attr: &str) -> anyhow::Result<&'a str> { 67 | let err = anyhow!("could not find attribute '{attr}' in '{}'", self.html()); 68 | self.attr(attr).ok_or(err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/structs/build.rs: -------------------------------------------------------------------------------- 1 | use anyhow::bail; 2 | use colored::{ColoredString, Colorize}; 3 | use scraper::ElementRef; 4 | use serde::Serialize; 5 | use serde_with::skip_serializing_none; 6 | 7 | use crate::{is_skipable_row, ShowHydraStatus, SoupFind, StatusIcon, TryAttr}; 8 | 9 | #[skip_serializing_none] 10 | #[derive(Serialize, Debug, Default, Clone)] 11 | /// Status of a single build attempt, can be serialized to a JSON entry 12 | pub(crate) struct BuildStatus { 13 | pub(crate) icon: StatusIcon, 14 | pub(crate) success: bool, 15 | pub(crate) status: String, 16 | pub(crate) timestamp: Option, 17 | pub(crate) build_id: Option, 18 | pub(crate) build_url: Option, 19 | pub(crate) name: Option, 20 | pub(crate) arch: Option, 21 | pub(crate) evals: bool, 22 | pub(crate) job_name: Option, 23 | } 24 | 25 | impl ShowHydraStatus for BuildStatus { 26 | fn format_as_vec(&self) -> Vec { 27 | let mut row = Vec::new(); 28 | let icon = ColoredString::from(&self.icon); 29 | let status = match (self.evals, self.success) { 30 | (false, _) => format!("{icon} {}", self.status), 31 | (true, false) => format!("{icon} ({})", self.status), 32 | (true, true) => format!("{icon}"), 33 | }; 34 | row.push(status.into()); 35 | match &self.job_name { 36 | Some(job_name) => row.push(job_name.as_str().into()), 37 | None => {} 38 | }; 39 | let details = if self.evals { 40 | let name = self.name.clone().unwrap_or_default().into(); 41 | let timestamp = self 42 | .timestamp 43 | .clone() 44 | .unwrap_or_default() 45 | .split_once('T') 46 | .unwrap_or_default() 47 | .0 48 | .into(); 49 | &[name, timestamp] 50 | } else { 51 | &Default::default() 52 | }; 53 | row.extend_from_slice(details); 54 | let build_url = self.build_url.clone().unwrap_or_default().dimmed(); 55 | row.push(build_url); 56 | row 57 | } 58 | } 59 | 60 | impl BuildStatus { 61 | pub(crate) fn from_tbody(tbody: ElementRef<'_>) -> anyhow::Result> { 62 | let mut builds = Vec::new(); 63 | for row in tbody.find_all("tr") { 64 | let columns = row.find_all("td"); 65 | let (status, build, job_name, timestamp, name, arch) = 66 | // case I: the job is removed: 67 | if let [job_name, arch] = 68 | columns.as_slice() 69 | { 70 | let build_url = job_name.find("a")?.attr("href"); 71 | let job_name: String = job_name.text().collect(); 72 | let arch = arch.find("tt")?.text().collect(); 73 | builds.push(BuildStatus { 74 | icon: StatusIcon::Warning, 75 | status: "Removed".into(), 76 | build_url: build_url.map(str::to_string), 77 | arch: Some(arch), 78 | job_name: Some(job_name.trim().into()), 79 | ..Default::default() 80 | }); 81 | continue; 82 | } else 83 | // case II: there is no `job_name` column 84 | if let [status, build, timestamp, name, arch] = columns.as_slice() { 85 | let job_name = None; 86 | (status, build, job_name, timestamp, name, arch) 87 | } else 88 | // case III: there is a `job_name` column (e.g. in eval details page) 89 | if let [status, build, job_name, timestamp, name, arch] = columns.as_slice() { 90 | (status, build, Some(job_name), timestamp, name, arch) 91 | } else { 92 | #[allow(clippy::redundant_else)] 93 | if is_skipable_row(row)? { 94 | continue; 95 | } else { 96 | bail!("error while parsing build status from: {}", row.html()); 97 | } 98 | }; 99 | if let Ok(span_status) = status.find("span") { 100 | let span_status: String = span_status.text().collect(); 101 | let status = if span_status.trim() == "Queued" { 102 | "Queued: no build has been attempted for this package yet (still queued)" 103 | .to_string() 104 | } else { 105 | format!("Unknown Hydra status: {span_status}") 106 | }; 107 | builds.push(BuildStatus { 108 | icon: StatusIcon::Queued, 109 | status, 110 | ..Default::default() 111 | }); 112 | continue; 113 | } 114 | let status = status.find("img")?.try_attr("title")?; 115 | let build_id = build.find("a")?.text().collect(); 116 | let build_url = build.find("a")?.attr("href"); 117 | let timestamp = timestamp.find("time").ok().and_then(|x| x.attr("datetime")); 118 | let name = name.text().collect(); 119 | let job_name = job_name.map(|x| x.text().collect::().trim().into()); 120 | let arch = arch.find("tt")?.text().collect(); 121 | let success = status == "Succeeded"; 122 | let icon = match (success, status) { 123 | (true, _) => StatusIcon::Succeeded, 124 | (false, "Cancelled") => StatusIcon::Cancelled, 125 | (false, "Queued") => StatusIcon::Queued, 126 | (false, _) => StatusIcon::Failed, 127 | }; 128 | let evals = true; 129 | builds.push(BuildStatus { 130 | icon, 131 | success, 132 | status: status.into(), 133 | timestamp: timestamp.map(str::to_string), 134 | build_id: Some(build_id), 135 | build_url: build_url.map(str::to_string), 136 | name: Some(name), 137 | arch: Some(arch), 138 | evals, 139 | job_name, 140 | }); 141 | } 142 | Ok(builds) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/structs/eval.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use serde::Serialize; 3 | 4 | use crate::constants; 5 | 6 | /// Specification for a single Hydra evaluation, with an optional filter. 7 | /// Should only be constructed with [`Evaluation::guess_from_spec`] 8 | /// to ensure a correct [`Evaluation::spec`], thus marked [`non_exhaustive`]. 9 | #[non_exhaustive] 10 | #[derive(Debug, Clone, Serialize)] 11 | pub(crate) struct Evaluation { 12 | #[serde(skip)] 13 | pub(crate) spec: String, 14 | pub(crate) id: u64, 15 | pub(crate) filter: Option, 16 | } 17 | 18 | impl Evaluation { 19 | /// Parses an evaluation from a plain text specification. 20 | pub(crate) fn guess_from_spec(spec: &str) -> Self { 21 | let spec = spec.trim(); 22 | 23 | let mut split_spec = spec.splitn(2, '/'); 24 | let id = split_spec.next().unwrap().trim(); 25 | let filter = split_spec.next(); 26 | 27 | let (id, filter) = match id.parse() { 28 | Ok(x) => (x, filter), 29 | Err(_) => ( 30 | 0u64, // for the latest eval 31 | match id.is_empty() { 32 | true => filter, 33 | false => Some(spec), 34 | }, 35 | ), 36 | }; 37 | let filter = match filter { 38 | None => { 39 | let default = constants::DEFAULT_EVALUATION_FILTER.to_string(); 40 | info!( 41 | "{}, so the default filter '/{default}' is used {}", 42 | "no package filter has been specified", "for better performance" 43 | ); 44 | info!( 45 | "specify another filter with --eval '{}', {}\n", 46 | format!( 47 | "{}/", 48 | match id { 49 | 0 => "".into(), 50 | x => x.to_string(), 51 | } 52 | ), 53 | "or force an empty filter with a trailing slash '/'", 54 | ); 55 | Some(default) 56 | } 57 | Some(x) if x.trim().is_empty() => None, 58 | Some(x) => Some(x.into()), 59 | }; 60 | Self { 61 | spec: format!( 62 | "{id}{}", 63 | match &filter { 64 | Some(x) => format!("/{x}"), 65 | None => "".into(), 66 | } 67 | ), 68 | id, 69 | filter, 70 | } 71 | } 72 | } 73 | 74 | #[test] 75 | fn guess_eval_from_spec() { 76 | let default_filter = constants::DEFAULT_EVALUATION_FILTER; 77 | #[allow(clippy::unreadable_literal)] 78 | for (spec, id, filter) in [ 79 | ("123456", 123456, Some(default_filter.into())), 80 | ("123456/", 123456, None), 81 | ("123456/rustc", 123456, Some("rustc".into())), 82 | ("", 0, Some(default_filter.into())), 83 | ("/", 0, None), 84 | ("/rustc", 0, Some("rustc".into())), 85 | ("rustc", 0, Some("rustc".into())), 86 | ("weird/filter", 0, Some("weird/filter".into())), 87 | ] { 88 | let eval = Evaluation::guess_from_spec(spec); 89 | println!("{eval:?}"); 90 | assert!(eval.id == id && eval.filter == filter); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/structs/icons.rs: -------------------------------------------------------------------------------- 1 | use colored::{ColoredString, Colorize}; 2 | use serde_with::SerializeDisplay; 3 | 4 | #[derive(SerializeDisplay, Debug, Clone, Default)] 5 | pub(crate) enum StatusIcon { 6 | Succeeded, 7 | Failed, 8 | Cancelled, 9 | Queued, 10 | #[default] 11 | Warning, 12 | } 13 | 14 | impl From<&StatusIcon> for ColoredString { 15 | fn from(icon: &StatusIcon) -> Self { 16 | match icon { 17 | StatusIcon::Succeeded => "✔".green(), 18 | StatusIcon::Failed => "✖".red(), 19 | StatusIcon::Cancelled => "⏹".red(), 20 | StatusIcon::Queued => "⧖".yellow(), 21 | StatusIcon::Warning => "⚠".yellow(), 22 | } 23 | } 24 | } 25 | 26 | impl std::fmt::Display for StatusIcon { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | let icon = ColoredString::from(self).normal(); 29 | write!(f, "{icon}") 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | use insta::assert_snapshot; 35 | 36 | #[test] 37 | fn serialize_and_colorize_icons() { 38 | let success_icon = serde_json::to_string(&StatusIcon::Succeeded).unwrap(); 39 | debug_assert_eq!(success_icon, r#""✔""#); 40 | let colored_icon = ColoredString::from(&StatusIcon::Queued); 41 | assert_snapshot!(colored_icon.to_string(), @"⧖"); 42 | } 43 | -------------------------------------------------------------------------------- /src/structs/inputs.rs: -------------------------------------------------------------------------------- 1 | use anyhow::bail; 2 | use colored::Colorize; 3 | use scraper::ElementRef; 4 | use serde::Serialize; 5 | use serde_json::Value; 6 | use serde_with::skip_serializing_none; 7 | use std::fmt::Display; 8 | 9 | #[cfg(test)] 10 | use insta::assert_snapshot; 11 | 12 | use crate::{is_skipable_row, SoupFind}; 13 | 14 | #[skip_serializing_none] 15 | #[derive(Serialize, Clone, Default, Debug)] 16 | /// Inputs of a given evaluation (which is also the inputs of a package build) 17 | pub(crate) struct EvalInput { 18 | pub(crate) name: Option, 19 | #[serde(rename = "type")] 20 | pub(crate) input_type: Option, 21 | pub(crate) value: Option, 22 | pub(crate) revision: Option, 23 | pub(crate) store_path: Option, 24 | } 25 | 26 | impl Display for EvalInput { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | let json = serde_json::to_string(&self).expect("EvalInput should be serialized into json"); 29 | let json: Value = serde_json::from_str(&json).unwrap(); 30 | let strings: Vec<_> = ["name", "type", "value", "revision", "store_path"] 31 | .iter() 32 | .filter_map(|key| match &json[key] { 33 | Value::Null => None, 34 | // unquote the string: 35 | Value::String(value) => { 36 | let key = match *key { 37 | "name" => "input", 38 | k => k, 39 | }; 40 | Some(format!("{}: {}", key.bold(), value)) 41 | } 42 | value => Some(format!("{}: {}", key.bold(), value)), 43 | }) 44 | .collect(); 45 | write!(f, "{}", strings.join("\n")) 46 | } 47 | } 48 | 49 | impl EvalInput { 50 | pub(crate) fn from_tbody(tbody: ElementRef<'_>, caller_id: &str) -> anyhow::Result> { 51 | let mut inputs: Vec = Vec::new(); 52 | for row in tbody.find_all("tr") { 53 | let columns = row.find_all("td"); 54 | let columns: Vec<_> = columns 55 | .iter() 56 | .map(|x| { 57 | let text: String = x.text().collect(); 58 | match text.trim() { 59 | "" => None, 60 | x => Some(x.to_string()), 61 | } 62 | }) 63 | .collect(); 64 | let [name, input_type, value, revision, store_path] = columns.as_slice() else { 65 | #[allow(clippy::redundant_else)] 66 | if let Ok(true) = is_skipable_row(row) { 67 | continue; 68 | } else { 69 | bail!( 70 | "error while parsing inputs for {}: {:?}", 71 | caller_id, 72 | row.html() 73 | ); 74 | } 75 | }; 76 | inputs.push(EvalInput { 77 | name: name.to_owned(), 78 | input_type: input_type.to_owned(), 79 | value: value.to_owned(), 80 | revision: revision.to_owned(), 81 | store_path: store_path.to_owned(), 82 | }); 83 | } 84 | Ok(inputs) 85 | } 86 | } 87 | 88 | #[test] 89 | fn format_eval_input() { 90 | let eval_input = EvalInput { 91 | name: Some("nixpkgs".into()), 92 | input_type: Some("Git checkout".into()), 93 | value: Some("https://github.com/nixos/nixpkgs.git".into()), 94 | revision: Some("1e9e641a3fc1b22fbdb823a99d8ff96692cc4fba".into()), 95 | store_path: Some("/nix/store/ln479gq56q3kyzyl0mm00xglpmfpzqx4-source".into()), 96 | }; 97 | assert_snapshot!(eval_input.to_string(), @r#" 98 | input: nixpkgs 99 | type: Git checkout 100 | value: https://github.com/nixos/nixpkgs.git 101 | revision: 1e9e641a3fc1b22fbdb823a99d8ff96692cc4fba 102 | store_path: /nix/store/ln479gq56q3kyzyl0mm00xglpmfpzqx4-source 103 | "#); 104 | } 105 | -------------------------------------------------------------------------------- /src/structs/mod.rs: -------------------------------------------------------------------------------- 1 | mod build; 2 | mod eval; 3 | mod icons; 4 | mod inputs; 5 | 6 | pub(crate) use build::BuildStatus; 7 | pub(crate) use eval::Evaluation; 8 | pub(crate) use icons::StatusIcon; 9 | pub(crate) use inputs::EvalInput; 10 | --------------------------------------------------------------------------------