├── .envrc ├── .github ├── pull_request_template.md └── workflows │ ├── build_nix.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── README.md ├── flake.lock ├── flake.nix ├── src ├── error.rs ├── lib.rs ├── main.rs ├── message.rs └── nix │ ├── describe_derivation.nix │ ├── describe_derivation.rs │ ├── find_attribute_paths.nix │ ├── find_attribute_paths.rs │ ├── lib.nix │ ├── lib.rs │ ├── mod.rs │ ├── narinfo.rs │ └── substituters.rs └── tests ├── fixtures ├── flake-direct-buildInput │ ├── flake.lock │ ├── flake.nix │ ├── pkg1 │ │ └── pkg1 │ └── pkg2 │ │ └── pkg2 ├── flake-direct-nativeBuildInput │ ├── flake.lock │ ├── flake.nix │ ├── pkg1 │ │ └── pkg1 │ └── pkg2 │ │ └── pkg2 ├── flake-three-levels │ ├── flake.lock │ ├── flake.nix │ ├── pkg1 │ │ └── pkg1 │ ├── pkg2 │ │ └── pkg2 │ └── pkg3 │ │ └── pkg3 ├── flake-trivial-rust │ ├── flake.lock │ └── flake.nix └── flake-trivial │ ├── flake.lock │ └── flake.nix └── test_cli.py /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | Issue: # 3 | 4 | ## Description 5 | 6 | ## Motivation 7 | 8 | ## Checklist 9 | Checklist before merging: 10 | - [ ] CHANGELOG.md updated 11 | - [ ] README.md up-to-date 12 | -------------------------------------------------------------------------------- /.github/workflows/build_nix.yml: -------------------------------------------------------------------------------- 1 | name: "Build Nix package on Ubuntu" 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: cachix/install-nix-action@v12 12 | - name: Building package 13 | run: nix build . --extra-experimental-features "flakes nix-command" 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2023, axodotdev 2 | # SPDX-License-Identifier: MIT or Apache-2.0 3 | # 4 | # CI that: 5 | # 6 | # * checks for a Git Tag that looks like a release 7 | # * builds artifacts with cargo-dist (archives, installers, hashes) 8 | # * uploads those artifacts to temporary workflow zip 9 | # * on success, uploads the artifacts to a Github Release 10 | # 11 | # Note that the Github Release will be created with a generated 12 | # title/body based on your changelogs. 13 | 14 | name: Release 15 | 16 | permissions: 17 | contents: write 18 | 19 | # This task will run whenever you push a git tag that looks like a version 20 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 21 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 22 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 23 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 24 | # 25 | # If PACKAGE_NAME is specified, then the announcement will be for that 26 | # package (erroring out if it doesn't have the given version or isn't cargo-dist-able). 27 | # 28 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 29 | # (cargo-dist-able) packages in the workspace with that version (this mode is 30 | # intended for workspaces with only one dist-able package, or with all dist-able 31 | # packages versioned/released in lockstep). 32 | # 33 | # If you push multiple tags at once, separate instances of this workflow will 34 | # spin up, creating an independent announcement for each one. However Github 35 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 36 | # mistake. 37 | # 38 | # If there's a prerelease-style suffix to the version, then the release(s) 39 | # will be marked as a prerelease. 40 | on: 41 | push: 42 | tags: 43 | - '**[0-9]+.[0-9]+.[0-9]+*' 44 | pull_request: 45 | 46 | jobs: 47 | # Run 'cargo dist plan' (or host) to determine what tasks we need to do 48 | plan: 49 | runs-on: ubuntu-latest 50 | outputs: 51 | val: ${{ steps.plan.outputs.manifest }} 52 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 53 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 54 | publishing: ${{ !github.event.pull_request }} 55 | env: 56 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | steps: 58 | - uses: actions/checkout@v4 59 | with: 60 | submodules: recursive 61 | - name: Install cargo-dist 62 | # we specify bash to get pipefail; it guards against the `curl` command 63 | # failing. otherwise `sh` won't catch that `curl` returned non-0 64 | shell: bash 65 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.8.2/cargo-dist-installer.sh | sh" 66 | # sure would be cool if github gave us proper conditionals... 67 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 68 | # functionality based on whether this is a pull_request, and whether it's from a fork. 69 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 70 | # but also really annoying to build CI around when it needs secrets to work right.) 71 | - id: plan 72 | run: | 73 | cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > dist-manifest.json 74 | echo "cargo dist ran successfully" 75 | cat dist-manifest.json 76 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 77 | - name: "Upload dist-manifest.json" 78 | uses: actions/upload-artifact@v3 79 | with: 80 | name: artifacts 81 | path: dist-manifest.json 82 | 83 | # Build and packages all the platform-specific things 84 | build-local-artifacts: 85 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 86 | # Let the initial task tell us to not run (currently very blunt) 87 | needs: 88 | - plan 89 | if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 90 | strategy: 91 | fail-fast: false 92 | # Target platforms/runners are computed by cargo-dist in create-release. 93 | # Each member of the matrix has the following arguments: 94 | # 95 | # - runner: the github runner 96 | # - dist-args: cli flags to pass to cargo dist 97 | # - install-dist: expression to run to install cargo-dist on the runner 98 | # 99 | # Typically there will be: 100 | # - 1 "global" task that builds universal installers 101 | # - N "local" tasks that build each platform's binaries and platform-specific installers 102 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 103 | runs-on: ${{ matrix.runner }} 104 | env: 105 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 106 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 107 | steps: 108 | - uses: actions/checkout@v4 109 | with: 110 | submodules: recursive 111 | - uses: swatinem/rust-cache@v2 112 | - name: Install cargo-dist 113 | run: ${{ matrix.install_dist }} 114 | # Get the dist-manifest 115 | - name: Fetch local artifacts 116 | uses: actions/download-artifact@v4.1.7 117 | with: 118 | name: artifacts 119 | path: target/distrib/ 120 | - name: Install dependencies 121 | run: | 122 | ${{ matrix.packages_install }} 123 | - name: Build artifacts 124 | run: | 125 | # Actually do builds and make zips and whatnot 126 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 127 | echo "cargo dist ran successfully" 128 | - id: cargo-dist 129 | name: Post-build 130 | # We force bash here just because github makes it really hard to get values up 131 | # to "real" actions without writing to env-vars, and writing to env-vars has 132 | # inconsistent syntax between shell and powershell. 133 | shell: bash 134 | run: | 135 | # Parse out what we just built and upload it to scratch storage 136 | echo "paths<> "$GITHUB_OUTPUT" 137 | jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" 138 | echo "EOF" >> "$GITHUB_OUTPUT" 139 | 140 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 141 | - name: "Upload artifacts" 142 | uses: actions/upload-artifact@v3 143 | with: 144 | name: artifacts 145 | path: | 146 | ${{ steps.cargo-dist.outputs.paths }} 147 | ${{ env.BUILD_MANIFEST_NAME }} 148 | 149 | # Build and package all the platform-agnostic(ish) things 150 | build-global-artifacts: 151 | needs: 152 | - plan 153 | - build-local-artifacts 154 | runs-on: "ubuntu-20.04" 155 | env: 156 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 157 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 158 | steps: 159 | - uses: actions/checkout@v4 160 | with: 161 | submodules: recursive 162 | - name: Install cargo-dist 163 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.8.2/cargo-dist-installer.sh | sh" 164 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 165 | - name: Fetch local artifacts 166 | uses: actions/download-artifact@v4.1.7 167 | with: 168 | name: artifacts 169 | path: target/distrib/ 170 | - id: cargo-dist 171 | shell: bash 172 | run: | 173 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 174 | echo "cargo dist ran successfully" 175 | 176 | # Parse out what we just built and upload it to scratch storage 177 | echo "paths<> "$GITHUB_OUTPUT" 178 | jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" 179 | echo "EOF" >> "$GITHUB_OUTPUT" 180 | 181 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 182 | - name: "Upload artifacts" 183 | uses: actions/upload-artifact@v3 184 | with: 185 | name: artifacts 186 | path: | 187 | ${{ steps.cargo-dist.outputs.paths }} 188 | ${{ env.BUILD_MANIFEST_NAME }} 189 | # Determines if we should publish/announce 190 | host: 191 | needs: 192 | - plan 193 | - build-local-artifacts 194 | - build-global-artifacts 195 | # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) 196 | if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} 197 | env: 198 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 199 | runs-on: "ubuntu-20.04" 200 | outputs: 201 | val: ${{ steps.host.outputs.manifest }} 202 | steps: 203 | - uses: actions/checkout@v4 204 | with: 205 | submodules: recursive 206 | - name: Install cargo-dist 207 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.8.2/cargo-dist-installer.sh | sh" 208 | # Fetch artifacts from scratch-storage 209 | - name: Fetch artifacts 210 | uses: actions/download-artifact@v4.1.7 211 | with: 212 | name: artifacts 213 | path: target/distrib/ 214 | # This is a harmless no-op for Github Releases, hosting for that happens in "announce" 215 | - id: host 216 | shell: bash 217 | run: | 218 | cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 219 | echo "artifacts uploaded and released successfully" 220 | cat dist-manifest.json 221 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 222 | - name: "Upload dist-manifest.json" 223 | uses: actions/upload-artifact@v3 224 | with: 225 | name: artifacts 226 | path: dist-manifest.json 227 | 228 | # Create a Github Release while uploading all files to it 229 | announce: 230 | needs: 231 | - plan 232 | - host 233 | # use "always() && ..." to allow us to wait for all publish jobs while 234 | # still allowing individual publish jobs to skip themselves (for prereleases). 235 | # "host" however must run to completion, no skipping allowed! 236 | if: ${{ always() && needs.host.result == 'success' }} 237 | runs-on: "ubuntu-20.04" 238 | env: 239 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 240 | steps: 241 | - uses: actions/checkout@v4 242 | with: 243 | submodules: recursive 244 | - name: "Download Github Artifacts" 245 | uses: actions/download-artifact@v4.1.7 246 | with: 247 | name: artifacts 248 | path: artifacts 249 | - name: Cleanup 250 | run: | 251 | # Remove the granular manifests 252 | rm -f artifacts/*-dist-manifest.json 253 | - name: Create Github Release 254 | uses: ncipollo/release-action@v1 255 | with: 256 | tag: ${{ needs.plan.outputs.tag }} 257 | name: ${{ fromJson(needs.host.outputs.val).announcement_title }} 258 | body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }} 259 | prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }} 260 | artifacts: "artifacts/*" 261 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | debug 3 | result 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## [Unreleased] 5 | ### Added 6 | - [#49](https://github.com/tweag/nixtract/pull/49) rewrite describe_derivation to include all found derivations (but actively skip bootstrap packages) 7 | 8 | ### Removed 9 | - [#50](https://github.com/tweag/nixtract/pull/50) excludes fixed output derivations in nixtract output 10 | 11 | ### Fixed 12 | - [#53](https://github.com/tweag/nixtract/pull/53) resolve an issue where some derivations were analyzed multiple times 13 | 14 | ## [0.3.0] - 2024-04-17 15 | ### Added 16 | - [#34](https://github.com/tweag/nixtract/pull/34) add option to provide nixtract with a status communication channel 17 | - [#36](https://github.com/tweag/nixtract/pull/36) add option to only extract runtime dependencies 18 | - [#40](https://github.com/tweag/nixtract/pull/40) log warning when narinfo fetching fails 19 | 20 | ### Fixed 21 | - [#38](https://github.com/tweag/nixtract/pull/38) fixed bug where found derivations were parsed incorrectly 22 | - [#42](https://github.com/tweag/nixtract/pull/42) reintroduced the src field in the derivation description 23 | - [#43](https://github.com/tweag/nixtract/pull/43) enables the `flakes` and `nix-command` features for nix invocations, this avoids users having to have them enabled manually 24 | 25 | ### Changed 26 | - [#36](https://github.com/tweag/nixtract/pull/36) moved all nixtract configuration options into a single struct passed to the `nixtract` function 27 | 28 | [0.3.0]: https://github.com/tweag/nixtract/compare/v0.2.0...v0.3.0 29 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.13" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle" 45 | version = "1.0.6" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 48 | 49 | [[package]] 50 | name = "anstyle-parse" 51 | version = "0.2.3" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 54 | dependencies = [ 55 | "utf8parse", 56 | ] 57 | 58 | [[package]] 59 | name = "anstyle-query" 60 | version = "1.0.2" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 63 | dependencies = [ 64 | "windows-sys 0.52.0", 65 | ] 66 | 67 | [[package]] 68 | name = "anstyle-wincon" 69 | version = "3.0.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 72 | dependencies = [ 73 | "anstyle", 74 | "windows-sys 0.52.0", 75 | ] 76 | 77 | [[package]] 78 | name = "autocfg" 79 | version = "1.2.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" 82 | 83 | [[package]] 84 | name = "backtrace" 85 | version = "0.3.71" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 88 | dependencies = [ 89 | "addr2line", 90 | "cc", 91 | "cfg-if", 92 | "libc", 93 | "miniz_oxide", 94 | "object", 95 | "rustc-demangle", 96 | ] 97 | 98 | [[package]] 99 | name = "base64" 100 | version = "0.21.7" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 103 | 104 | [[package]] 105 | name = "bitflags" 106 | version = "1.3.2" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 109 | 110 | [[package]] 111 | name = "bitflags" 112 | version = "2.5.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 115 | 116 | [[package]] 117 | name = "bumpalo" 118 | version = "3.16.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 121 | 122 | [[package]] 123 | name = "bytes" 124 | version = "1.6.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" 127 | 128 | [[package]] 129 | name = "cc" 130 | version = "1.0.94" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" 133 | 134 | [[package]] 135 | name = "cfg-if" 136 | version = "1.0.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 139 | 140 | [[package]] 141 | name = "clap" 142 | version = "4.5.4" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" 145 | dependencies = [ 146 | "clap_builder", 147 | "clap_derive", 148 | ] 149 | 150 | [[package]] 151 | name = "clap-verbosity-flag" 152 | version = "2.2.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "bb9b20c0dd58e4c2e991c8d203bbeb76c11304d1011659686b5b644bc29aa478" 155 | dependencies = [ 156 | "clap", 157 | "log", 158 | ] 159 | 160 | [[package]] 161 | name = "clap_builder" 162 | version = "4.5.2" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 165 | dependencies = [ 166 | "anstream", 167 | "anstyle", 168 | "clap_lex", 169 | "strsim", 170 | ] 171 | 172 | [[package]] 173 | name = "clap_derive" 174 | version = "4.5.4" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" 177 | dependencies = [ 178 | "heck", 179 | "proc-macro2", 180 | "quote", 181 | "syn 2.0.59", 182 | ] 183 | 184 | [[package]] 185 | name = "clap_lex" 186 | version = "0.7.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 189 | 190 | [[package]] 191 | name = "colorchoice" 192 | version = "1.0.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 195 | 196 | [[package]] 197 | name = "console" 198 | version = "0.15.8" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" 201 | dependencies = [ 202 | "encode_unicode", 203 | "lazy_static", 204 | "libc", 205 | "unicode-width", 206 | "windows-sys 0.52.0", 207 | ] 208 | 209 | [[package]] 210 | name = "core-foundation" 211 | version = "0.9.4" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 214 | dependencies = [ 215 | "core-foundation-sys", 216 | "libc", 217 | ] 218 | 219 | [[package]] 220 | name = "core-foundation-sys" 221 | version = "0.8.6" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 224 | 225 | [[package]] 226 | name = "crossbeam-deque" 227 | version = "0.8.5" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 230 | dependencies = [ 231 | "crossbeam-epoch", 232 | "crossbeam-utils", 233 | ] 234 | 235 | [[package]] 236 | name = "crossbeam-epoch" 237 | version = "0.9.18" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 240 | dependencies = [ 241 | "crossbeam-utils", 242 | ] 243 | 244 | [[package]] 245 | name = "crossbeam-utils" 246 | version = "0.8.19" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 249 | 250 | [[package]] 251 | name = "diff" 252 | version = "0.1.13" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 255 | 256 | [[package]] 257 | name = "dyn-clone" 258 | version = "1.0.17" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" 261 | 262 | [[package]] 263 | name = "either" 264 | version = "1.11.0" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" 267 | 268 | [[package]] 269 | name = "encode_unicode" 270 | version = "0.3.6" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 273 | 274 | [[package]] 275 | name = "encoding_rs" 276 | version = "0.8.34" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" 279 | dependencies = [ 280 | "cfg-if", 281 | ] 282 | 283 | [[package]] 284 | name = "env_filter" 285 | version = "0.1.0" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" 288 | dependencies = [ 289 | "log", 290 | "regex", 291 | ] 292 | 293 | [[package]] 294 | name = "env_logger" 295 | version = "0.11.3" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" 298 | dependencies = [ 299 | "anstream", 300 | "anstyle", 301 | "env_filter", 302 | "humantime", 303 | "log", 304 | ] 305 | 306 | [[package]] 307 | name = "equivalent" 308 | version = "1.0.1" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 311 | 312 | [[package]] 313 | name = "errno" 314 | version = "0.3.8" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 317 | dependencies = [ 318 | "libc", 319 | "windows-sys 0.52.0", 320 | ] 321 | 322 | [[package]] 323 | name = "fastrand" 324 | version = "2.0.2" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" 327 | 328 | [[package]] 329 | name = "flume" 330 | version = "0.11.0" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" 333 | dependencies = [ 334 | "futures-core", 335 | "futures-sink", 336 | "nanorand", 337 | "spin", 338 | ] 339 | 340 | [[package]] 341 | name = "fnv" 342 | version = "1.0.7" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 345 | 346 | [[package]] 347 | name = "foreign-types" 348 | version = "0.3.2" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 351 | dependencies = [ 352 | "foreign-types-shared", 353 | ] 354 | 355 | [[package]] 356 | name = "foreign-types-shared" 357 | version = "0.1.1" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 360 | 361 | [[package]] 362 | name = "form_urlencoded" 363 | version = "1.2.1" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 366 | dependencies = [ 367 | "percent-encoding", 368 | ] 369 | 370 | [[package]] 371 | name = "futures-channel" 372 | version = "0.3.30" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 375 | dependencies = [ 376 | "futures-core", 377 | ] 378 | 379 | [[package]] 380 | name = "futures-core" 381 | version = "0.3.30" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 384 | 385 | [[package]] 386 | name = "futures-io" 387 | version = "0.3.30" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 390 | 391 | [[package]] 392 | name = "futures-sink" 393 | version = "0.3.30" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 396 | 397 | [[package]] 398 | name = "futures-task" 399 | version = "0.3.30" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 402 | 403 | [[package]] 404 | name = "futures-util" 405 | version = "0.3.30" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 408 | dependencies = [ 409 | "futures-core", 410 | "futures-io", 411 | "futures-task", 412 | "memchr", 413 | "pin-project-lite", 414 | "pin-utils", 415 | "slab", 416 | ] 417 | 418 | [[package]] 419 | name = "getrandom" 420 | version = "0.2.14" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" 423 | dependencies = [ 424 | "cfg-if", 425 | "js-sys", 426 | "libc", 427 | "wasi", 428 | "wasm-bindgen", 429 | ] 430 | 431 | [[package]] 432 | name = "gimli" 433 | version = "0.28.1" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 436 | 437 | [[package]] 438 | name = "h2" 439 | version = "0.3.26" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" 442 | dependencies = [ 443 | "bytes", 444 | "fnv", 445 | "futures-core", 446 | "futures-sink", 447 | "futures-util", 448 | "http", 449 | "indexmap", 450 | "slab", 451 | "tokio", 452 | "tokio-util", 453 | "tracing", 454 | ] 455 | 456 | [[package]] 457 | name = "hashbrown" 458 | version = "0.14.3" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 461 | 462 | [[package]] 463 | name = "heck" 464 | version = "0.5.0" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 467 | 468 | [[package]] 469 | name = "hermit-abi" 470 | version = "0.3.9" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 473 | 474 | [[package]] 475 | name = "http" 476 | version = "0.2.12" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 479 | dependencies = [ 480 | "bytes", 481 | "fnv", 482 | "itoa", 483 | ] 484 | 485 | [[package]] 486 | name = "http-body" 487 | version = "0.4.6" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 490 | dependencies = [ 491 | "bytes", 492 | "http", 493 | "pin-project-lite", 494 | ] 495 | 496 | [[package]] 497 | name = "httparse" 498 | version = "1.8.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 501 | 502 | [[package]] 503 | name = "httpdate" 504 | version = "1.0.3" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 507 | 508 | [[package]] 509 | name = "humantime" 510 | version = "2.1.0" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 513 | 514 | [[package]] 515 | name = "hyper" 516 | version = "0.14.28" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" 519 | dependencies = [ 520 | "bytes", 521 | "futures-channel", 522 | "futures-core", 523 | "futures-util", 524 | "h2", 525 | "http", 526 | "http-body", 527 | "httparse", 528 | "httpdate", 529 | "itoa", 530 | "pin-project-lite", 531 | "socket2", 532 | "tokio", 533 | "tower-service", 534 | "tracing", 535 | "want", 536 | ] 537 | 538 | [[package]] 539 | name = "hyper-tls" 540 | version = "0.5.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 543 | dependencies = [ 544 | "bytes", 545 | "hyper", 546 | "native-tls", 547 | "tokio", 548 | "tokio-native-tls", 549 | ] 550 | 551 | [[package]] 552 | name = "idna" 553 | version = "0.5.0" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 556 | dependencies = [ 557 | "unicode-bidi", 558 | "unicode-normalization", 559 | ] 560 | 561 | [[package]] 562 | name = "indexmap" 563 | version = "2.2.6" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 566 | dependencies = [ 567 | "equivalent", 568 | "hashbrown", 569 | ] 570 | 571 | [[package]] 572 | name = "indicatif" 573 | version = "0.17.8" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" 576 | dependencies = [ 577 | "console", 578 | "instant", 579 | "number_prefix", 580 | "portable-atomic", 581 | "unicode-width", 582 | ] 583 | 584 | [[package]] 585 | name = "indicatif-log-bridge" 586 | version = "0.2.2" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "2963046f28a204e3e3fd7e754fd90a6235da05b5378f24707ff0ec9513725ce3" 589 | dependencies = [ 590 | "indicatif", 591 | "log", 592 | ] 593 | 594 | [[package]] 595 | name = "instant" 596 | version = "0.1.12" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 599 | dependencies = [ 600 | "cfg-if", 601 | ] 602 | 603 | [[package]] 604 | name = "ipnet" 605 | version = "2.9.0" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" 608 | 609 | [[package]] 610 | name = "itoa" 611 | version = "1.0.11" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 614 | 615 | [[package]] 616 | name = "js-sys" 617 | version = "0.3.69" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 620 | dependencies = [ 621 | "wasm-bindgen", 622 | ] 623 | 624 | [[package]] 625 | name = "lazy_static" 626 | version = "1.4.0" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 629 | 630 | [[package]] 631 | name = "libc" 632 | version = "0.2.153" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 635 | 636 | [[package]] 637 | name = "linux-raw-sys" 638 | version = "0.4.13" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 641 | 642 | [[package]] 643 | name = "lock_api" 644 | version = "0.4.11" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 647 | dependencies = [ 648 | "autocfg", 649 | "scopeguard", 650 | ] 651 | 652 | [[package]] 653 | name = "log" 654 | version = "0.4.21" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 657 | 658 | [[package]] 659 | name = "memchr" 660 | version = "2.7.2" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 663 | 664 | [[package]] 665 | name = "mime" 666 | version = "0.3.17" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 669 | 670 | [[package]] 671 | name = "miniz_oxide" 672 | version = "0.7.2" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 675 | dependencies = [ 676 | "adler", 677 | ] 678 | 679 | [[package]] 680 | name = "mio" 681 | version = "0.8.11" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 684 | dependencies = [ 685 | "libc", 686 | "wasi", 687 | "windows-sys 0.48.0", 688 | ] 689 | 690 | [[package]] 691 | name = "nanorand" 692 | version = "0.7.0" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" 695 | dependencies = [ 696 | "getrandom", 697 | ] 698 | 699 | [[package]] 700 | name = "native-tls" 701 | version = "0.2.11" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 704 | dependencies = [ 705 | "lazy_static", 706 | "libc", 707 | "log", 708 | "openssl", 709 | "openssl-probe", 710 | "openssl-sys", 711 | "schannel", 712 | "security-framework", 713 | "security-framework-sys", 714 | "tempfile", 715 | ] 716 | 717 | [[package]] 718 | name = "nixtract" 719 | version = "0.3.0" 720 | dependencies = [ 721 | "clap", 722 | "clap-verbosity-flag", 723 | "env_logger", 724 | "flume", 725 | "indicatif", 726 | "indicatif-log-bridge", 727 | "log", 728 | "num_cpus", 729 | "pretty_assertions", 730 | "rayon", 731 | "reqwest", 732 | "schemars", 733 | "serde", 734 | "serde_json", 735 | "shell-escape", 736 | "tempfile", 737 | "thiserror", 738 | ] 739 | 740 | [[package]] 741 | name = "num_cpus" 742 | version = "1.16.0" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 745 | dependencies = [ 746 | "hermit-abi", 747 | "libc", 748 | ] 749 | 750 | [[package]] 751 | name = "number_prefix" 752 | version = "0.4.0" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 755 | 756 | [[package]] 757 | name = "object" 758 | version = "0.32.2" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 761 | dependencies = [ 762 | "memchr", 763 | ] 764 | 765 | [[package]] 766 | name = "once_cell" 767 | version = "1.19.0" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 770 | 771 | [[package]] 772 | name = "openssl" 773 | version = "0.10.66" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" 776 | dependencies = [ 777 | "bitflags 2.5.0", 778 | "cfg-if", 779 | "foreign-types", 780 | "libc", 781 | "once_cell", 782 | "openssl-macros", 783 | "openssl-sys", 784 | ] 785 | 786 | [[package]] 787 | name = "openssl-macros" 788 | version = "0.1.1" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 791 | dependencies = [ 792 | "proc-macro2", 793 | "quote", 794 | "syn 2.0.59", 795 | ] 796 | 797 | [[package]] 798 | name = "openssl-probe" 799 | version = "0.1.5" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 802 | 803 | [[package]] 804 | name = "openssl-sys" 805 | version = "0.9.103" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" 808 | dependencies = [ 809 | "cc", 810 | "libc", 811 | "pkg-config", 812 | "vcpkg", 813 | ] 814 | 815 | [[package]] 816 | name = "percent-encoding" 817 | version = "2.3.1" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 820 | 821 | [[package]] 822 | name = "pin-project-lite" 823 | version = "0.2.14" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 826 | 827 | [[package]] 828 | name = "pin-utils" 829 | version = "0.1.0" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 832 | 833 | [[package]] 834 | name = "pkg-config" 835 | version = "0.3.30" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 838 | 839 | [[package]] 840 | name = "portable-atomic" 841 | version = "1.6.0" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" 844 | 845 | [[package]] 846 | name = "pretty_assertions" 847 | version = "1.4.0" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" 850 | dependencies = [ 851 | "diff", 852 | "yansi", 853 | ] 854 | 855 | [[package]] 856 | name = "proc-macro2" 857 | version = "1.0.81" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" 860 | dependencies = [ 861 | "unicode-ident", 862 | ] 863 | 864 | [[package]] 865 | name = "quote" 866 | version = "1.0.36" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 869 | dependencies = [ 870 | "proc-macro2", 871 | ] 872 | 873 | [[package]] 874 | name = "rayon" 875 | version = "1.10.0" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 878 | dependencies = [ 879 | "either", 880 | "rayon-core", 881 | ] 882 | 883 | [[package]] 884 | name = "rayon-core" 885 | version = "1.12.1" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 888 | dependencies = [ 889 | "crossbeam-deque", 890 | "crossbeam-utils", 891 | ] 892 | 893 | [[package]] 894 | name = "regex" 895 | version = "1.10.4" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 898 | dependencies = [ 899 | "aho-corasick", 900 | "memchr", 901 | "regex-automata", 902 | "regex-syntax", 903 | ] 904 | 905 | [[package]] 906 | name = "regex-automata" 907 | version = "0.4.6" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 910 | dependencies = [ 911 | "aho-corasick", 912 | "memchr", 913 | "regex-syntax", 914 | ] 915 | 916 | [[package]] 917 | name = "regex-syntax" 918 | version = "0.8.3" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 921 | 922 | [[package]] 923 | name = "reqwest" 924 | version = "0.11.27" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" 927 | dependencies = [ 928 | "base64", 929 | "bytes", 930 | "encoding_rs", 931 | "futures-core", 932 | "futures-util", 933 | "h2", 934 | "http", 935 | "http-body", 936 | "hyper", 937 | "hyper-tls", 938 | "ipnet", 939 | "js-sys", 940 | "log", 941 | "mime", 942 | "native-tls", 943 | "once_cell", 944 | "percent-encoding", 945 | "pin-project-lite", 946 | "rustls-pemfile", 947 | "serde", 948 | "serde_json", 949 | "serde_urlencoded", 950 | "sync_wrapper", 951 | "system-configuration", 952 | "tokio", 953 | "tokio-native-tls", 954 | "tower-service", 955 | "url", 956 | "wasm-bindgen", 957 | "wasm-bindgen-futures", 958 | "web-sys", 959 | "winreg", 960 | ] 961 | 962 | [[package]] 963 | name = "rustc-demangle" 964 | version = "0.1.23" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 967 | 968 | [[package]] 969 | name = "rustix" 970 | version = "0.38.32" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" 973 | dependencies = [ 974 | "bitflags 2.5.0", 975 | "errno", 976 | "libc", 977 | "linux-raw-sys", 978 | "windows-sys 0.52.0", 979 | ] 980 | 981 | [[package]] 982 | name = "rustls-pemfile" 983 | version = "1.0.4" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 986 | dependencies = [ 987 | "base64", 988 | ] 989 | 990 | [[package]] 991 | name = "ryu" 992 | version = "1.0.17" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 995 | 996 | [[package]] 997 | name = "schannel" 998 | version = "0.1.23" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" 1001 | dependencies = [ 1002 | "windows-sys 0.52.0", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "schemars" 1007 | version = "0.8.16" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" 1010 | dependencies = [ 1011 | "dyn-clone", 1012 | "schemars_derive", 1013 | "serde", 1014 | "serde_json", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "schemars_derive" 1019 | version = "0.8.16" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" 1022 | dependencies = [ 1023 | "proc-macro2", 1024 | "quote", 1025 | "serde_derive_internals", 1026 | "syn 1.0.109", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "scopeguard" 1031 | version = "1.2.0" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1034 | 1035 | [[package]] 1036 | name = "security-framework" 1037 | version = "2.10.0" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" 1040 | dependencies = [ 1041 | "bitflags 1.3.2", 1042 | "core-foundation", 1043 | "core-foundation-sys", 1044 | "libc", 1045 | "security-framework-sys", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "security-framework-sys" 1050 | version = "2.10.0" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" 1053 | dependencies = [ 1054 | "core-foundation-sys", 1055 | "libc", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "serde" 1060 | version = "1.0.198" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" 1063 | dependencies = [ 1064 | "serde_derive", 1065 | ] 1066 | 1067 | [[package]] 1068 | name = "serde_derive" 1069 | version = "1.0.198" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" 1072 | dependencies = [ 1073 | "proc-macro2", 1074 | "quote", 1075 | "syn 2.0.59", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "serde_derive_internals" 1080 | version = "0.26.0" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" 1083 | dependencies = [ 1084 | "proc-macro2", 1085 | "quote", 1086 | "syn 1.0.109", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "serde_json" 1091 | version = "1.0.116" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" 1094 | dependencies = [ 1095 | "itoa", 1096 | "ryu", 1097 | "serde", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "serde_urlencoded" 1102 | version = "0.7.1" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1105 | dependencies = [ 1106 | "form_urlencoded", 1107 | "itoa", 1108 | "ryu", 1109 | "serde", 1110 | ] 1111 | 1112 | [[package]] 1113 | name = "shell-escape" 1114 | version = "0.1.5" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" 1117 | 1118 | [[package]] 1119 | name = "slab" 1120 | version = "0.4.9" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1123 | dependencies = [ 1124 | "autocfg", 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "socket2" 1129 | version = "0.5.6" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" 1132 | dependencies = [ 1133 | "libc", 1134 | "windows-sys 0.52.0", 1135 | ] 1136 | 1137 | [[package]] 1138 | name = "spin" 1139 | version = "0.9.8" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1142 | dependencies = [ 1143 | "lock_api", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "strsim" 1148 | version = "0.11.1" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1151 | 1152 | [[package]] 1153 | name = "syn" 1154 | version = "1.0.109" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1157 | dependencies = [ 1158 | "proc-macro2", 1159 | "quote", 1160 | "unicode-ident", 1161 | ] 1162 | 1163 | [[package]] 1164 | name = "syn" 1165 | version = "2.0.59" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" 1168 | dependencies = [ 1169 | "proc-macro2", 1170 | "quote", 1171 | "unicode-ident", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "sync_wrapper" 1176 | version = "0.1.2" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1179 | 1180 | [[package]] 1181 | name = "system-configuration" 1182 | version = "0.5.1" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 1185 | dependencies = [ 1186 | "bitflags 1.3.2", 1187 | "core-foundation", 1188 | "system-configuration-sys", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "system-configuration-sys" 1193 | version = "0.5.0" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 1196 | dependencies = [ 1197 | "core-foundation-sys", 1198 | "libc", 1199 | ] 1200 | 1201 | [[package]] 1202 | name = "tempfile" 1203 | version = "3.10.1" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 1206 | dependencies = [ 1207 | "cfg-if", 1208 | "fastrand", 1209 | "rustix", 1210 | "windows-sys 0.52.0", 1211 | ] 1212 | 1213 | [[package]] 1214 | name = "thiserror" 1215 | version = "1.0.58" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" 1218 | dependencies = [ 1219 | "thiserror-impl", 1220 | ] 1221 | 1222 | [[package]] 1223 | name = "thiserror-impl" 1224 | version = "1.0.58" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" 1227 | dependencies = [ 1228 | "proc-macro2", 1229 | "quote", 1230 | "syn 2.0.59", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "tinyvec" 1235 | version = "1.6.0" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1238 | dependencies = [ 1239 | "tinyvec_macros", 1240 | ] 1241 | 1242 | [[package]] 1243 | name = "tinyvec_macros" 1244 | version = "0.1.1" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1247 | 1248 | [[package]] 1249 | name = "tokio" 1250 | version = "1.37.0" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" 1253 | dependencies = [ 1254 | "backtrace", 1255 | "bytes", 1256 | "libc", 1257 | "mio", 1258 | "pin-project-lite", 1259 | "socket2", 1260 | "windows-sys 0.48.0", 1261 | ] 1262 | 1263 | [[package]] 1264 | name = "tokio-native-tls" 1265 | version = "0.3.1" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1268 | dependencies = [ 1269 | "native-tls", 1270 | "tokio", 1271 | ] 1272 | 1273 | [[package]] 1274 | name = "tokio-util" 1275 | version = "0.7.10" 1276 | source = "registry+https://github.com/rust-lang/crates.io-index" 1277 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" 1278 | dependencies = [ 1279 | "bytes", 1280 | "futures-core", 1281 | "futures-sink", 1282 | "pin-project-lite", 1283 | "tokio", 1284 | "tracing", 1285 | ] 1286 | 1287 | [[package]] 1288 | name = "tower-service" 1289 | version = "0.3.2" 1290 | source = "registry+https://github.com/rust-lang/crates.io-index" 1291 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1292 | 1293 | [[package]] 1294 | name = "tracing" 1295 | version = "0.1.40" 1296 | source = "registry+https://github.com/rust-lang/crates.io-index" 1297 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1298 | dependencies = [ 1299 | "pin-project-lite", 1300 | "tracing-core", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "tracing-core" 1305 | version = "0.1.32" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1308 | dependencies = [ 1309 | "once_cell", 1310 | ] 1311 | 1312 | [[package]] 1313 | name = "try-lock" 1314 | version = "0.2.5" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1317 | 1318 | [[package]] 1319 | name = "unicode-bidi" 1320 | version = "0.3.15" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1323 | 1324 | [[package]] 1325 | name = "unicode-ident" 1326 | version = "1.0.12" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1329 | 1330 | [[package]] 1331 | name = "unicode-normalization" 1332 | version = "0.1.23" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1335 | dependencies = [ 1336 | "tinyvec", 1337 | ] 1338 | 1339 | [[package]] 1340 | name = "unicode-width" 1341 | version = "0.1.11" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 1344 | 1345 | [[package]] 1346 | name = "url" 1347 | version = "2.5.0" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 1350 | dependencies = [ 1351 | "form_urlencoded", 1352 | "idna", 1353 | "percent-encoding", 1354 | ] 1355 | 1356 | [[package]] 1357 | name = "utf8parse" 1358 | version = "0.2.1" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1361 | 1362 | [[package]] 1363 | name = "vcpkg" 1364 | version = "0.2.15" 1365 | source = "registry+https://github.com/rust-lang/crates.io-index" 1366 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1367 | 1368 | [[package]] 1369 | name = "want" 1370 | version = "0.3.1" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1373 | dependencies = [ 1374 | "try-lock", 1375 | ] 1376 | 1377 | [[package]] 1378 | name = "wasi" 1379 | version = "0.11.0+wasi-snapshot-preview1" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1382 | 1383 | [[package]] 1384 | name = "wasm-bindgen" 1385 | version = "0.2.92" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 1388 | dependencies = [ 1389 | "cfg-if", 1390 | "wasm-bindgen-macro", 1391 | ] 1392 | 1393 | [[package]] 1394 | name = "wasm-bindgen-backend" 1395 | version = "0.2.92" 1396 | source = "registry+https://github.com/rust-lang/crates.io-index" 1397 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 1398 | dependencies = [ 1399 | "bumpalo", 1400 | "log", 1401 | "once_cell", 1402 | "proc-macro2", 1403 | "quote", 1404 | "syn 2.0.59", 1405 | "wasm-bindgen-shared", 1406 | ] 1407 | 1408 | [[package]] 1409 | name = "wasm-bindgen-futures" 1410 | version = "0.4.42" 1411 | source = "registry+https://github.com/rust-lang/crates.io-index" 1412 | checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" 1413 | dependencies = [ 1414 | "cfg-if", 1415 | "js-sys", 1416 | "wasm-bindgen", 1417 | "web-sys", 1418 | ] 1419 | 1420 | [[package]] 1421 | name = "wasm-bindgen-macro" 1422 | version = "0.2.92" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 1425 | dependencies = [ 1426 | "quote", 1427 | "wasm-bindgen-macro-support", 1428 | ] 1429 | 1430 | [[package]] 1431 | name = "wasm-bindgen-macro-support" 1432 | version = "0.2.92" 1433 | source = "registry+https://github.com/rust-lang/crates.io-index" 1434 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 1435 | dependencies = [ 1436 | "proc-macro2", 1437 | "quote", 1438 | "syn 2.0.59", 1439 | "wasm-bindgen-backend", 1440 | "wasm-bindgen-shared", 1441 | ] 1442 | 1443 | [[package]] 1444 | name = "wasm-bindgen-shared" 1445 | version = "0.2.92" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 1448 | 1449 | [[package]] 1450 | name = "web-sys" 1451 | version = "0.3.69" 1452 | source = "registry+https://github.com/rust-lang/crates.io-index" 1453 | checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" 1454 | dependencies = [ 1455 | "js-sys", 1456 | "wasm-bindgen", 1457 | ] 1458 | 1459 | [[package]] 1460 | name = "windows-sys" 1461 | version = "0.48.0" 1462 | source = "registry+https://github.com/rust-lang/crates.io-index" 1463 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1464 | dependencies = [ 1465 | "windows-targets 0.48.5", 1466 | ] 1467 | 1468 | [[package]] 1469 | name = "windows-sys" 1470 | version = "0.52.0" 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" 1472 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1473 | dependencies = [ 1474 | "windows-targets 0.52.5", 1475 | ] 1476 | 1477 | [[package]] 1478 | name = "windows-targets" 1479 | version = "0.48.5" 1480 | source = "registry+https://github.com/rust-lang/crates.io-index" 1481 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1482 | dependencies = [ 1483 | "windows_aarch64_gnullvm 0.48.5", 1484 | "windows_aarch64_msvc 0.48.5", 1485 | "windows_i686_gnu 0.48.5", 1486 | "windows_i686_msvc 0.48.5", 1487 | "windows_x86_64_gnu 0.48.5", 1488 | "windows_x86_64_gnullvm 0.48.5", 1489 | "windows_x86_64_msvc 0.48.5", 1490 | ] 1491 | 1492 | [[package]] 1493 | name = "windows-targets" 1494 | version = "0.52.5" 1495 | source = "registry+https://github.com/rust-lang/crates.io-index" 1496 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 1497 | dependencies = [ 1498 | "windows_aarch64_gnullvm 0.52.5", 1499 | "windows_aarch64_msvc 0.52.5", 1500 | "windows_i686_gnu 0.52.5", 1501 | "windows_i686_gnullvm", 1502 | "windows_i686_msvc 0.52.5", 1503 | "windows_x86_64_gnu 0.52.5", 1504 | "windows_x86_64_gnullvm 0.52.5", 1505 | "windows_x86_64_msvc 0.52.5", 1506 | ] 1507 | 1508 | [[package]] 1509 | name = "windows_aarch64_gnullvm" 1510 | version = "0.48.5" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1513 | 1514 | [[package]] 1515 | name = "windows_aarch64_gnullvm" 1516 | version = "0.52.5" 1517 | source = "registry+https://github.com/rust-lang/crates.io-index" 1518 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 1519 | 1520 | [[package]] 1521 | name = "windows_aarch64_msvc" 1522 | version = "0.48.5" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1525 | 1526 | [[package]] 1527 | name = "windows_aarch64_msvc" 1528 | version = "0.52.5" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 1531 | 1532 | [[package]] 1533 | name = "windows_i686_gnu" 1534 | version = "0.48.5" 1535 | source = "registry+https://github.com/rust-lang/crates.io-index" 1536 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1537 | 1538 | [[package]] 1539 | name = "windows_i686_gnu" 1540 | version = "0.52.5" 1541 | source = "registry+https://github.com/rust-lang/crates.io-index" 1542 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 1543 | 1544 | [[package]] 1545 | name = "windows_i686_gnullvm" 1546 | version = "0.52.5" 1547 | source = "registry+https://github.com/rust-lang/crates.io-index" 1548 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 1549 | 1550 | [[package]] 1551 | name = "windows_i686_msvc" 1552 | version = "0.48.5" 1553 | source = "registry+https://github.com/rust-lang/crates.io-index" 1554 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1555 | 1556 | [[package]] 1557 | name = "windows_i686_msvc" 1558 | version = "0.52.5" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1561 | 1562 | [[package]] 1563 | name = "windows_x86_64_gnu" 1564 | version = "0.48.5" 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" 1566 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1567 | 1568 | [[package]] 1569 | name = "windows_x86_64_gnu" 1570 | version = "0.52.5" 1571 | source = "registry+https://github.com/rust-lang/crates.io-index" 1572 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1573 | 1574 | [[package]] 1575 | name = "windows_x86_64_gnullvm" 1576 | version = "0.48.5" 1577 | source = "registry+https://github.com/rust-lang/crates.io-index" 1578 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1579 | 1580 | [[package]] 1581 | name = "windows_x86_64_gnullvm" 1582 | version = "0.52.5" 1583 | source = "registry+https://github.com/rust-lang/crates.io-index" 1584 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1585 | 1586 | [[package]] 1587 | name = "windows_x86_64_msvc" 1588 | version = "0.48.5" 1589 | source = "registry+https://github.com/rust-lang/crates.io-index" 1590 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1591 | 1592 | [[package]] 1593 | name = "windows_x86_64_msvc" 1594 | version = "0.52.5" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1597 | 1598 | [[package]] 1599 | name = "winreg" 1600 | version = "0.50.0" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 1603 | dependencies = [ 1604 | "cfg-if", 1605 | "windows-sys 0.48.0", 1606 | ] 1607 | 1608 | [[package]] 1609 | name = "yansi" 1610 | version = "0.5.1" 1611 | source = "registry+https://github.com/rust-lang/crates.io-index" 1612 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" 1613 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nixtract" 3 | version = "0.3.0" 4 | authors = [ 5 | "Erin van der Veen ", 6 | "Guillaume Desforges ", 7 | "Dorran Howell ", 8 | "Tweag I/O", 9 | ] 10 | edition = "2021" 11 | description = "A CLI tool and library to extract the graph of derivations from a Nix flake." 12 | homepage = "github.com/tweag/nixtract" 13 | repository = "https://github.com/tweag/nixtract" 14 | license = "MIT OR Apache-2.0" 15 | keywords = ["nix", "flake", "graph", "derivation"] 16 | categories = ["development-tools"] 17 | exclude = [ 18 | "tests/**", 19 | "examples/**", 20 | "benches/**", 21 | "docs/**", 22 | "target/**", 23 | ".github/**", 24 | ] 25 | 26 | [dependencies] 27 | clap = { version = "4.4.18", features = ["derive"] } 28 | clap-verbosity-flag = "2.1.2" 29 | env_logger = "0.11.1" 30 | flume = "0.11.0" 31 | indicatif = "0.17.8" 32 | indicatif-log-bridge = "0.2.2" 33 | log = "0.4.20" 34 | num_cpus = "1.16.0" 35 | rayon = "1.8.1" 36 | reqwest = { version = "0.11.24", features = ["blocking"] } 37 | schemars = "0.8.16" 38 | serde = { version = "1.0.196", features = ["derive"] } 39 | serde_json = "1.0.113" 40 | shell-escape = "0.1.5" 41 | tempfile = "3.9.0" 42 | thiserror = "1.0.56" 43 | 44 | [dev-dependencies] 45 | pretty_assertions = "1.4.0" 46 | 47 | # The profile that 'cargo dist' will build with 48 | [profile.dist] 49 | inherits = "release" 50 | lto = "thin" 51 | 52 | # Config for 'cargo dist' 53 | [workspace.metadata.dist] 54 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 55 | cargo-dist-version = "0.8.2" 56 | # CI backends to support 57 | ci = ["github"] 58 | # The installers to generate for each app 59 | installers = [] 60 | # Target platforms to build apps for (Rust target-triple syntax) 61 | targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu"] 62 | # Publish jobs to run in CI 63 | pr-run-mode = "plan" 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build](https://img.shields.io/github/actions/workflow/status/tweag/nixtract/build_nix.yml 2 | ) [![Discord channel](https://img.shields.io/discord/1174731094726295632)](https://discord.gg/53XwX7Ft) 3 | 4 | # `nixtract` 5 | 6 | A CLI tool to extract the graph of derivations from a Nix flake. 7 | 8 | We [invite you](https://discord.gg/53XwX7Ft) to join our [Discord channel](https://discord.com/channels/1174731094726295632/1183682765212897280)! It's a great place to ask questions, share your ideas, and collaborate with other contributors. 9 | 10 | ## Usage 11 | 12 | ### Requirements 13 | 14 | * Nix >= 2.4 15 | * experimental feature `nix-command` needs to be enabled: [Nix command - NixOS Wiki](https://wiki.nixos.org/wiki/Nix_command) 16 | * experimental feature `flakes` needs to be enabled: [Flakes - NixOS Wiki](https://wiki.nixos.org/wiki/Flakes) 17 | 18 | ### Set up 19 | 20 | Get it using Nix 21 | 22 | ```console 23 | $ nix shell github:tweag/nixtract 24 | ``` 25 | 26 | Or install using `cargo` 27 | 28 | ```console 29 | cargo install --git https://github.com/tweag/nixtract.git 30 | ``` 31 | 32 | ### Usage 33 | 34 | ```console 35 | $ nixtract --help 36 | A CLI tool and library to extract the graph of derivations from a Nix flake. 37 | 38 | Usage: nixtract [OPTIONS] [OUTPUT_PATH] 39 | 40 | Arguments: 41 | [OUTPUT_PATH] 42 | Write the output to a file instead of stdout or explicitly use `-` for stdout 43 | 44 | Options: 45 | -f, --target-flake-ref 46 | The flake URI to extract, e.g. "github:tweag/nixtract" 47 | 48 | [default: nixpkgs] 49 | 50 | -a, --target-attribute-path 51 | The attribute path to extract, e.g. "haskellPackages.hello", defaults to all derivations in the flake 52 | 53 | -s, --target-system 54 | The system to extract, e.g. "x86_64-linux", defaults to the host system 55 | 56 | --offline 57 | Run nix evaluation in offline mode 58 | 59 | --n-workers 60 | Count of workers to spawn to describe derivations 61 | 62 | --pretty 63 | Pretty print the output 64 | 65 | -v, --verbose... 66 | Increase logging verbosity 67 | 68 | -q, --quiet... 69 | Decrease logging verbosity 70 | 71 | --output-schema 72 | Output the json schema 73 | 74 | -h, --help 75 | Print help (see a summary with '-h') 76 | 77 | -V, --version 78 | Print version 79 | ``` 80 | 81 | ### Extract nixpkgs graph of derivations 82 | 83 | To extract the data from the `nixpkgs` of your flake registry and output to stdout, use: 84 | 85 | ```console 86 | $ nixtract 87 | ``` 88 | 89 | you can also specify an output file path directly instead 90 | 91 | ```console 92 | $ nixtract derivations.jsonl 93 | ``` 94 | 95 | in order to extract from a specific flake, use `--target-flake-ref` or `-f`: 96 | 97 | ```console 98 | $ nixtract --target-flake-ref 'github:nixos/nixpkgs/23.05' 99 | ``` 100 | 101 | in order to extract a specific attribute, use `--target-attribute` or `-a`: 102 | 103 | ```console 104 | $ nixtract --target-attribute-path 'haskellPackages.hello' 105 | ``` 106 | 107 | in order to extract for a system different from your own, use `--target-system` or `-s`: 108 | 109 | ```console 110 | $ nixtract --target-system 'x86_64-darwin' 111 | ``` 112 | 113 | in order to only consider runtime dependencies, use `--runtime-only` or `-r`: 114 | 115 | ```console 116 | $ nixtract --runtime-only 117 | ``` 118 | 119 | ### Understanding the output 120 | 121 | `nixtract` evaluates Nix code to recursively find all derivations in a flake. 122 | It first finds the top level derivations, basically all derivations you can find with `nix search`. 123 | Then it recurses into the input derivations of any derivation it has found. 124 | 125 | Each line of the output is a valid JSON that describes a derivation. 126 | As such, the output is a JSONL file. 127 | 128 | The JSON schema of a derivation can be shown like so: 129 | 130 | ```console 131 | $ nixtract --output-schema 132 | ``` 133 | 134 | ## Development 135 | 136 | ### Set up 137 | 138 | #### Using Nix 139 | ```console 140 | $ nix develop 141 | ``` 142 | 143 | #### Manually 144 | If Nix is not available, you can install the Rust toolchain manually. 145 | 146 | ### Under the hood 147 | 148 | The overall architecture inside is described in `src/main.rs`: 149 | 150 | ``` 151 | Calling this tool starts a subprocess that list top-level derivations (outputPath + attribute path) to its stderr pipe, see `src/nix/find_attribute_paths.nix`. 152 | This pipe is consumed in a thread that reads each line and populates a vector. 153 | This vector is consumed by rayon threads that will call the `process` function. 154 | This function will call a subprocess that describes the derivation (name, version, license, dependencies, ...), see `src/nix/describe_derivation.nix`. 155 | When describing a derivation, if dependencies are found and have not been already queued for processing, they are added to the current thread's queue, which makes us explore the entire depth of the graph. 156 | Rayon ensures that a thread without work in its queue will steal work from another thread, so we can explore the graph in parallel. 157 | 158 | The whole system stops once 159 | - all top-level attribute paths have been found 160 | - all derivations from that search have been processed 161 | - all dependencies have been processed 162 | 163 | Glossary: 164 | - output path: full path of the realization of the derivation in the Nix store. 165 | e.g. /nix/store/py9jjqsgsya5b9cpps64gchaj8lq2h5i-python3.10-versioneer-0.28 166 | - attribute path: path from the root attribute set to get the desired value. 167 | e.g. python3Derivations.versioneer 168 | ``` 169 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "naersk": { 4 | "inputs": { 5 | "nixpkgs": "nixpkgs" 6 | }, 7 | "locked": { 8 | "lastModified": 1698420672, 9 | "narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=", 10 | "owner": "nix-community", 11 | "repo": "naersk", 12 | "rev": "aeb58d5e8faead8980a807c840232697982d47b9", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "nix-community", 17 | "ref": "master", 18 | "repo": "naersk", 19 | "type": "github" 20 | } 21 | }, 22 | "nixpkgs": { 23 | "locked": { 24 | "lastModified": 1708247094, 25 | "narHash": "sha256-H2VS7VwesetGDtIaaz4AMsRkPoSLEVzL/Ika8gnbUnE=", 26 | "owner": "NixOS", 27 | "repo": "nixpkgs", 28 | "rev": "045b51a3ae66f673ed44b5bbd1f4a341d96703bf", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "id": "nixpkgs", 33 | "type": "indirect" 34 | } 35 | }, 36 | "nixpkgs_2": { 37 | "locked": { 38 | "lastModified": 1708247094, 39 | "narHash": "sha256-H2VS7VwesetGDtIaaz4AMsRkPoSLEVzL/Ika8gnbUnE=", 40 | "owner": "NixOS", 41 | "repo": "nixpkgs", 42 | "rev": "045b51a3ae66f673ed44b5bbd1f4a341d96703bf", 43 | "type": "github" 44 | }, 45 | "original": { 46 | "owner": "NixOS", 47 | "ref": "nixpkgs-unstable", 48 | "repo": "nixpkgs", 49 | "type": "github" 50 | } 51 | }, 52 | "root": { 53 | "inputs": { 54 | "naersk": "naersk", 55 | "nixpkgs": "nixpkgs_2", 56 | "utils": "utils" 57 | } 58 | }, 59 | "systems": { 60 | "locked": { 61 | "lastModified": 1681028828, 62 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 63 | "owner": "nix-systems", 64 | "repo": "default", 65 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 66 | "type": "github" 67 | }, 68 | "original": { 69 | "owner": "nix-systems", 70 | "repo": "default", 71 | "type": "github" 72 | } 73 | }, 74 | "utils": { 75 | "inputs": { 76 | "systems": "systems" 77 | }, 78 | "locked": { 79 | "lastModified": 1705309234, 80 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 81 | "owner": "numtide", 82 | "repo": "flake-utils", 83 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 84 | "type": "github" 85 | }, 86 | "original": { 87 | "owner": "numtide", 88 | "repo": "flake-utils", 89 | "type": "github" 90 | } 91 | } 92 | }, 93 | "root": "root", 94 | "version": 7 95 | } 96 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | naersk.url = "github:nix-community/naersk/master"; 4 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 5 | utils.url = "github:numtide/flake-utils"; 6 | }; 7 | 8 | outputs = { self, nixpkgs, utils, naersk }: 9 | utils.lib.eachDefaultSystem (system: 10 | let 11 | pkgs = import nixpkgs { inherit system; }; 12 | naersk-lib = pkgs.callPackage naersk { }; 13 | in 14 | { 15 | defaultPackage = naersk-lib.buildPackage { 16 | pname = "nixtract"; 17 | src = ./.; 18 | 19 | # nixtract uses the reqwest crate to query for narinfo on the substituters. 20 | # reqwest depends on openssl. 21 | nativeBuildInputs = with pkgs; [ 22 | pkg-config 23 | ]; 24 | buildInputs = with pkgs; ([ 25 | openssl 26 | ] ++ lib.optionals stdenv.isDarwin (with darwin; [ 27 | apple_sdk.frameworks.SystemConfiguration 28 | libiconv 29 | ])); 30 | }; 31 | devShell = with pkgs; mkShell { 32 | buildInputs = [ 33 | cargo 34 | rustc 35 | rustfmt 36 | pre-commit 37 | rustPackages.clippy 38 | cargo-flamegraph 39 | cargo-dist 40 | 41 | pkg-config 42 | openssl 43 | ] ++ lib.optionals stdenv.isDarwin (with darwin; [ 44 | darwin.apple_sdk.frameworks.SystemConfiguration 45 | libiconv 46 | ]); 47 | 48 | RUST_SRC_PATH = rustPlatform.rustLibSrc; 49 | }; 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | pub type Result = std::result::Result; 2 | 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum Error { 5 | #[error("Could not parse the Nix CLI output for attribute path/flake ref: {0}: {1}")] 6 | SerdeJSON(String, serde_json::Error), 7 | 8 | #[error("Nix exited with a non-zero exit code: {0:#?}: {1}")] 9 | NixCommand(Option, String), 10 | 11 | #[error("IO error when calling Nix: {0}")] 12 | NixIO(#[from] std::io::Error), 13 | 14 | #[error("Erorr when sending data to a mpsc channel: {0}")] 15 | Mpsc(Box>), 16 | 17 | #[error("Error when sending a status message to the caller: {0}")] 18 | MessageMpsc(Box>), 19 | 20 | #[error("The provided value could not be parsed as an integer: {0}")] 21 | NarInfoParseIntError(#[from] std::num::ParseIntError), 22 | 23 | #[error("The provided NarInfo could not be parsed because we encountered a line without a delimiter: {0}")] 24 | NarInfoNoDelimiter(String), 25 | 26 | #[error("The provided NarInfo is missing a required field: {0}")] 27 | NarInfoMissingField(String), 28 | 29 | #[error("The store path is malformed and cannot be used to fetch the narinfo: {0}")] 30 | NarInfoInvalidPath(String), 31 | 32 | #[error("The narinfo file could not be fetched: {0}")] 33 | NarInfoReqwest(#[from] reqwest::Error), 34 | 35 | #[error("The field {0} of the parsed narinfo file was invalid for reason: {1}")] 36 | NarInfoInvalidField(String, String), 37 | } 38 | 39 | // Cannot automatically derive using #[from] because of the Box 40 | impl From> for Error { 41 | fn from(e: std::sync::mpsc::SendError) -> Self { 42 | Error::Mpsc(Box::new(e)) 43 | } 44 | } 45 | 46 | impl From> for Error { 47 | fn from(e: std::sync::mpsc::SendError) -> Self { 48 | Error::MessageMpsc(Box::new(e)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # nixtract 2 | //! nixtract is a library and command line tool to extract information from nix derivations. 3 | //! The main way to use nixtract is to call the `nixtract` function with a flake reference and optionally a system and attribute path. 4 | //! Alternatively, the underlying functions can be used directly to extract information from nix derivations. 5 | //! ## Example 6 | //! ```no_run 7 | //! use nixtract::{nixtract, NixtractConfig}; 8 | 9 | //! use std::error::Error; 10 | //! 11 | //! fn main() -> Result<(), Box> { 12 | //! let flake_ref = "nixpkgs"; 13 | //! let system = Some("x86_64-linux"); 14 | //! let attribute_path = Some("haskellPackages.hello"); 15 | //! let config = NixtractConfig::default(); 16 | //! 17 | //! let derivations = nixtract(flake_ref, system, attribute_path, config)?; 18 | //! 19 | //! for derivation in derivations { 20 | //! println!("{:?}", derivation); 21 | //! } 22 | //! 23 | //! Ok(()) 24 | //! } 25 | //! ``` 26 | //! ## Command Line 27 | //! nixtract can also be used as a command line tool. For example: 28 | //! ```sh 29 | //! nixtract --target-flake-ref nixpkgs --target-system x86_64-linux --target-attribute-path haskellPackages.hello 30 | //! ``` 31 | 32 | use ::std::sync::{Arc, Mutex}; 33 | use rayon::prelude::*; 34 | use std::sync::mpsc; 35 | 36 | use error::Result; 37 | 38 | mod nix; 39 | pub use nix::*; 40 | 41 | pub mod error; 42 | pub mod message; 43 | 44 | #[derive(Debug, Clone)] 45 | pub struct ProcessingArgs<'a> { 46 | pub collected_paths: &'a Arc>>, 47 | pub flake_ref: &'a String, 48 | pub system: &'a Option, 49 | pub attribute_path: String, 50 | pub offline: bool, 51 | pub include_nar_info: bool, 52 | pub runtime_only: bool, 53 | pub binary_caches: &'a Vec, 54 | pub lib: &'a nix::lib::Lib, 55 | pub tx: mpsc::Sender, 56 | /// Used by the worker threads to communicate their status back to the main thread. 57 | /// This can for instance be used to update the UI. 58 | /// main.rs uses this channel to update the indicatif status bard. 59 | pub message_tx: Option>, 60 | } 61 | 62 | fn send_message( 63 | message_tx: &Option>, 64 | message: message::Message, 65 | ) -> Result<()> { 66 | if let Some(tx) = message_tx { 67 | Ok(tx.send(message)?) 68 | } else { 69 | Ok(()) 70 | } 71 | } 72 | 73 | fn process(args: ProcessingArgs) -> Result<()> { 74 | log::debug!("Processing derivation: {:?}", args.attribute_path); 75 | 76 | // Inform the calling thread that we are starting to process the derivation 77 | send_message( 78 | &args.message_tx, 79 | message::Message { 80 | status: message::Status::Started, 81 | id: rayon::current_thread_index().unwrap(), 82 | path: args.attribute_path.clone(), 83 | }, 84 | )?; 85 | 86 | let description = nix::describe_derivation(&nix::DescribeDerivationArgs::from(args.clone()))?; 87 | 88 | // Abort if we have reached to bootstrap stage 89 | if description.name == "bootstrap-tools" || description.name.starts_with("bootstrap-stage") { 90 | return Ok(()); 91 | } 92 | 93 | // Inform the calling thread that we have described the derivation 94 | send_message( 95 | &args.message_tx, 96 | message::Message { 97 | status: message::Status::Completed, 98 | id: rayon::current_thread_index().unwrap(), 99 | path: description.attribute_path.clone(), 100 | }, 101 | )?; 102 | 103 | // Send the DerivationDescription to the main thread 104 | args.tx.send(description.clone())?; 105 | 106 | // use par_iter to call process on all children of this derivation 107 | description 108 | .build_inputs 109 | .into_par_iter() 110 | .map(|build_input| -> Result<()> { 111 | // check if the build_input has already be processed 112 | let done = { 113 | let mut collected_paths = args.collected_paths.lock().unwrap(); 114 | match &build_input.output_path { 115 | None => { 116 | log::warn!( 117 | "Found a derivation without an output_path: {:?}", 118 | build_input 119 | ); 120 | false 121 | } 122 | Some(output_path) => !collected_paths.insert(output_path.clone()), 123 | } 124 | }; 125 | 126 | if done { 127 | log::debug!( 128 | "Skipping already processed derivation: {}", 129 | build_input.attribute_path.to_string() 130 | ); 131 | 132 | // Inform calling thread that the derivation was skipped if 133 | // requested. 134 | send_message( 135 | &args.message_tx, 136 | message::Message { 137 | status: message::Status::Skipped, 138 | id: rayon::current_thread_index().unwrap(), 139 | path: build_input.attribute_path.clone(), 140 | }, 141 | )?; 142 | 143 | return Ok(()); 144 | } 145 | 146 | // Call process with the build_input 147 | process(ProcessingArgs { 148 | attribute_path: build_input.attribute_path, 149 | tx: args.tx.clone(), 150 | message_tx: args.message_tx.clone(), 151 | ..args 152 | }) 153 | }) 154 | .collect::>>()?; 155 | 156 | Ok(()) 157 | } 158 | 159 | #[derive(Debug, Default, Clone)] 160 | pub struct NixtractConfig { 161 | pub offline: bool, 162 | pub include_nar_info: bool, 163 | pub runtime_only: bool, 164 | pub binary_caches: Option>, 165 | pub message_tx: Option>, 166 | } 167 | 168 | pub fn nixtract( 169 | flake_ref: impl Into, 170 | system: Option>, 171 | attribute_path: Option>, 172 | config: NixtractConfig, 173 | ) -> Result> { 174 | // Convert the arguments to the expected types 175 | let flake_ref = flake_ref.into(); 176 | let system = system.map(Into::into); 177 | let attribute_path = attribute_path.map(Into::into); 178 | 179 | let binary_caches = match config.binary_caches { 180 | None => nix::substituters::get_substituters(flake_ref.clone())?, 181 | Some(caches) => caches, 182 | }; 183 | 184 | // Writes the `lib.nix` file to the tempdir and stores its path 185 | let lib = nix::lib::Lib::new()?; 186 | 187 | // Create a channel to communicate DerivationDescription to the main thread 188 | let (tx, rx) = mpsc::channel(); 189 | 190 | log::info!( 191 | "Starting nixtract with flake_ref: {}, system: {}, attribute_path: {:?}", 192 | flake_ref, 193 | system 194 | .clone() 195 | .unwrap_or("builtins.currentSystem".to_owned()), 196 | attribute_path.clone().unwrap_or_default() 197 | ); 198 | 199 | let collected_paths: Arc>> = 200 | Arc::new(Mutex::new(std::collections::HashSet::new())); 201 | 202 | // call find_attribute_paths to get the initial set of derivations 203 | let attribute_paths = 204 | nix::find_attribute_paths(&flake_ref, &system, &attribute_path, &config.offline, &lib)?; 205 | 206 | // Combine all AttributePaths into a single Vec 207 | let mut derivations: Vec = Vec::new(); 208 | for attribute_path in attribute_paths { 209 | derivations.extend(attribute_path.found_drvs); 210 | } 211 | 212 | for found_drv in derivations.clone() { 213 | match found_drv.output_path { 214 | None => log::warn!("Found a derivation without an output_path: {:?}", found_drv), 215 | Some(output_path) => { 216 | let mut collected_paths = collected_paths.lock().unwrap(); 217 | collected_paths.insert(output_path); 218 | } 219 | } 220 | } 221 | 222 | // Spawn a new rayon thread to call process on every foundDrv 223 | rayon::spawn(move || { 224 | derivations.into_par_iter().for_each(|found_drv| { 225 | let processing_args = ProcessingArgs { 226 | collected_paths: &collected_paths, 227 | flake_ref: &flake_ref, 228 | system: &system, 229 | attribute_path: found_drv.attribute_path, 230 | offline: config.offline, 231 | runtime_only: config.runtime_only, 232 | include_nar_info: config.include_nar_info, 233 | binary_caches: &binary_caches, 234 | lib: &lib, 235 | tx: tx.clone(), 236 | message_tx: config.message_tx.clone(), 237 | }; 238 | match process(processing_args) { 239 | Ok(_) => {} 240 | Err(e) => log::warn!("Error processing derivation: {}", e), 241 | } 242 | }); 243 | }); 244 | 245 | Ok(rx.into_iter()) 246 | } 247 | 248 | #[cfg(test)] 249 | mod tests { 250 | use super::*; 251 | use core::panic; 252 | use std::fs; 253 | 254 | fn init() { 255 | let _ = env_logger::builder().is_test(true).try_init(); 256 | } 257 | 258 | #[test] 259 | fn test_main_fixtures() -> Result<()> { 260 | init(); 261 | 262 | // For every subdirectory in the tests/fixtures directory 263 | for entry in fs::read_dir("tests/fixtures").unwrap() { 264 | let entry = entry.unwrap(); 265 | let path = entry.path().canonicalize().unwrap(); 266 | if path.is_dir() { 267 | let config = NixtractConfig { 268 | runtime_only: false, 269 | binary_caches: None, 270 | offline: false, 271 | include_nar_info: false, 272 | message_tx: None, 273 | }; 274 | 275 | log::info!("Running test for {:?}", path); 276 | 277 | let test_name = path 278 | .components() 279 | .last() 280 | .unwrap() 281 | .as_os_str() 282 | .to_str() 283 | .unwrap(); 284 | let flake_ref = path.to_str().unwrap(); 285 | let system: Option = None; 286 | let attribute_path: Option = None; 287 | 288 | let mut descriptions = nixtract(flake_ref, system, attribute_path, config).unwrap(); 289 | 290 | match test_name { 291 | "flake-direct-buildInput" => {} 292 | "flake-direct-nativeBuildInput" => {} 293 | "flake-three-levels" => {} 294 | "flake-trivial-rust" => { 295 | assert!(descriptions.any(|d| { 296 | d.src.is_some_and(|s| { 297 | s.git_repo_url == "https://github.com/hello-lang/Rust.git" 298 | }) 299 | })); 300 | } 301 | "flake-trivial" => {} 302 | s => panic!("Unknown test: {}", s), 303 | } 304 | } 305 | } 306 | Ok(()) 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! The main entry point for the nixtract command line tool. 2 | //! 3 | //! Calling this tool starts a subprocess that list top-level derivations (outputPath + attribute path) to its stderr pipe, see `src/nix/find_attribute_paths.nix`. 4 | //! This pipe is consumed in a thread that reads each line and populates a vector. 5 | //! This vector is consumed by rayon threads that will call the `process` function. 6 | //! This function will call a subprocess that describes the derivation (name, version, license, dependencies, ...), see `src/nix/describe_derivation.nix`. 7 | //! When describing a derivation, if dependencies are found and have not been already queued for processing, they are added to the current thread's queue, which makes us explore the entire depth of the graph. 8 | //! Rayon ensures that a thread without work in its queue will steal work from another thread, so we can explore the graph in parallel. 9 | //! 10 | //! The whole system stops once 11 | //! - all top-level attribute paths have been found 12 | //! - all derivations from that search have been processed 13 | //! - all dependencies have been processed 14 | //! 15 | //! Glossary: 16 | //! - output path: full path of the realization of the derivation in the Nix store. 17 | //! e.g. /nix/store/py9jjqsgsya5b9cpps64gchaj8lq2h5i-python3.10-versioneer-0.28 18 | //! - attribute path: path from the root attribute set to get the desired value. 19 | //! e.g. python3Derivations.versioneer 20 | use std::{error::Error, io::Write}; 21 | 22 | use clap::Parser; 23 | use nixtract::{message::Message, nixtract, NixtractConfig}; 24 | 25 | #[derive(Parser, Debug)] 26 | #[command(author, version, about, long_about = None)] 27 | struct Args { 28 | #[arg( 29 | short, 30 | long = "target-flake-ref", 31 | default_value = "nixpkgs", 32 | help = "The flake URI to extract", 33 | long_help = "The flake URI to extract, e.g. \"github:tweag/nixtract\"" 34 | )] 35 | flake_ref: String, 36 | 37 | #[arg( 38 | short, 39 | long = "target-attribute-path", 40 | help = "The attribute path to extract", 41 | long_help = "The attribute path to extract, e.g. \"haskellPackages.hello\", defaults to all derivations in the flake" 42 | )] 43 | attribute_path: Option, 44 | 45 | #[arg( 46 | short, 47 | long = "target-system", 48 | help = "The system to extract", 49 | long_help = "The system to extract, e.g. \"x86_64-linux\", defaults to the host system" 50 | )] 51 | system: Option, 52 | 53 | /// Run nix evaluation in offline mode 54 | #[arg(long, default_value_t = false)] 55 | offline: bool, 56 | 57 | /// Attempt to fetch nar info from the binary cache 58 | #[arg(short = 'n', long, default_value_t = false)] 59 | include_nar_info: bool, 60 | 61 | /// List of caches to attempt to fetch narinfo from, defaults to the substituters from nix.conf and the `extra-substituters` from provided flake. 62 | #[arg(short, long)] 63 | binary_caches: Option>, 64 | 65 | /// Count of workers to spawn to describe derivations 66 | #[arg(long)] 67 | n_workers: Option, 68 | 69 | /// Pretty print the output 70 | #[arg(long, default_value_t = false)] 71 | pretty: bool, 72 | 73 | #[command(flatten)] 74 | verbose: clap_verbosity_flag::Verbosity, 75 | 76 | /// Output the json schema 77 | #[arg(long, default_value_t = false)] 78 | output_schema: bool, 79 | 80 | /// Ouput only runtime dependencies 81 | #[arg(long, short, default_value_t = false)] 82 | runtime_only: bool, 83 | 84 | /// Write the output to a file instead of stdout or explicitly use `-` for stdout 85 | #[arg()] 86 | output_path: Option, 87 | } 88 | 89 | impl From<&Args> for NixtractConfig { 90 | fn from(args: &Args) -> Self { 91 | NixtractConfig { 92 | offline: args.offline, 93 | include_nar_info: args.include_nar_info, 94 | runtime_only: args.runtime_only, 95 | binary_caches: args.binary_caches.clone(), 96 | message_tx: None, 97 | } 98 | } 99 | } 100 | 101 | fn main() -> Result<(), Box> { 102 | let opts: Args = Args::parse(); 103 | 104 | // Create the out writer 105 | let (mut out_writer, to_file) = match opts.output_path.as_deref() { 106 | None | Some("-") => ( 107 | Box::new(std::io::stdout()) as Box, 108 | false, 109 | ), 110 | Some(path) => { 111 | let file = std::fs::File::create(path)?; 112 | (Box::new(file) as Box, true) 113 | } 114 | }; 115 | 116 | // If schema is requested, print the schema and return 117 | if opts.output_schema { 118 | let schema = schemars::schema_for!(nixtract::DerivationDescription); 119 | let schema_string = serde_json::to_string_pretty(&schema)?; 120 | out_writer.write_all(schema_string.as_bytes())?; 121 | out_writer.write_all(b"\n")?; 122 | Ok(()) 123 | } else { 124 | main_with_args(opts, out_writer, to_file) 125 | } 126 | } 127 | 128 | fn main_with_args( 129 | opts: Args, 130 | mut out_writer: impl Write, 131 | to_file: bool, 132 | ) -> Result<(), Box> { 133 | // Initialize the rayon thread pool with the provided number of workers 134 | // or use the default number of workers if none is provided 135 | if let Some(n_workers) = opts.n_workers { 136 | rayon::ThreadPoolBuilder::new() 137 | .num_threads(n_workers) 138 | .build_global()?; 139 | } 140 | 141 | let (status_tx, status_rx): ( 142 | std::sync::mpsc::Sender, 143 | std::sync::mpsc::Receiver, 144 | ) = std::sync::mpsc::channel(); 145 | 146 | // Initialize the logger if not writing to a file, otherwise we defer it to after we created the MultiProcess 147 | let mut log_builder = env_logger::Builder::new(); 148 | log_builder.filter_level(opts.verbose.log_level_filter()); 149 | if !to_file { 150 | // Initialize the logger with the provided verbosity 151 | let _ = log_builder.try_init(); 152 | } 153 | 154 | // If we are outputing to a file and not stdout, start a gui thread that uses indicatif to display progress 155 | // If we would always start a MultiProgress progress bar, the output would be mangled by the output we write to stdout ourselves. 156 | let handle = if to_file { 157 | let spinner_style = 158 | indicatif::ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}")? 159 | .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "); 160 | 161 | let multi = indicatif::MultiProgress::new(); 162 | let logger = log_builder.build(); 163 | let _ = indicatif_log_bridge::LogWrapper::new(multi.clone(), logger).try_init(); 164 | 165 | Some(std::thread::spawn(move || { 166 | // Create a progress bar for rayon thread in the global thread pool 167 | let mut progress_bars = Vec::new(); 168 | for _ in 0..rayon::current_num_threads() { 169 | let pb = multi.add(indicatif::ProgressBar::new(0)); 170 | pb.set_style(spinner_style.clone()); 171 | progress_bars.push(pb); 172 | } 173 | 174 | for message in status_rx { 175 | match message.status { 176 | nixtract::message::Status::Started => { 177 | progress_bars[message.id] 178 | .set_message(format!("Processing {}", message.path)); 179 | } 180 | nixtract::message::Status::Completed => { 181 | progress_bars[message.id] 182 | .set_message(format!("Processed {}", message.path)); 183 | progress_bars[message.id].inc(1); 184 | } 185 | nixtract::message::Status::Skipped => { 186 | progress_bars[message.id].set_message(format!("Skipped {}", message.path)); 187 | } 188 | } 189 | } 190 | 191 | for pb in progress_bars { 192 | pb.finish(); 193 | } 194 | 195 | multi.clear().expect("Failed to clear the progress bar"); 196 | })) 197 | } else { 198 | None 199 | }; 200 | 201 | let config = NixtractConfig { 202 | message_tx: Some(status_tx), 203 | ..(&opts).into() 204 | }; 205 | 206 | let results = nixtract(opts.flake_ref, opts.system, opts.attribute_path, config)?; 207 | 208 | // Print the results to the provided output, and pretty print if specified 209 | for result in results { 210 | let output = if opts.pretty { 211 | serde_json::to_string_pretty(&result)? 212 | } else { 213 | serde_json::to_string(&result)? 214 | }; 215 | 216 | out_writer.write_all(output.as_bytes())?; 217 | out_writer.write_all(b"\n")?; 218 | } 219 | 220 | if let Some(handle) = handle { 221 | handle.join().expect("Failed to join the gui thread"); 222 | } 223 | 224 | Ok(()) 225 | } 226 | 227 | #[cfg(test)] 228 | mod tests { 229 | use super::*; 230 | use std::fs; 231 | 232 | fn init() { 233 | let _ = env_logger::builder().is_test(true).try_init(); 234 | } 235 | 236 | #[test] 237 | fn test_main_fixtures() -> Result<(), Box> { 238 | init(); 239 | 240 | // For every subdirectory in the tests/fixtures directory 241 | for entry in fs::read_dir("tests/fixtures").unwrap() { 242 | let entry = entry.unwrap(); 243 | let path = entry.path().canonicalize().unwrap(); 244 | if path.is_dir() { 245 | // Create the Opts for the main_with_args function 246 | let opts = Args { 247 | flake_ref: path.to_str().unwrap().to_string(), 248 | attribute_path: Option::default(), 249 | system: Option::default(), 250 | offline: bool::default(), 251 | n_workers: Option::default(), 252 | pretty: bool::default(), 253 | verbose: clap_verbosity_flag::Verbosity::default(), 254 | output_schema: bool::default(), 255 | // Write output to /dev/null to avoid cluttering the test output 256 | output_path: Some("/dev/null".to_string()), 257 | include_nar_info: false, 258 | runtime_only: false, 259 | binary_caches: None, 260 | }; 261 | 262 | log::info!("Running test for {:?}", path); 263 | 264 | // Set out_writer to /dev/null to avoid cluttering the test output 265 | let out_writer = std::fs::File::create("/dev/null").unwrap(); 266 | 267 | let res = main_with_args(opts, out_writer, true); 268 | 269 | if res.is_ok() { 270 | log::info!("Test for {:?} passed", path); 271 | } else { 272 | log::error!("Test for {:?} failed", path); 273 | return res; 274 | } 275 | } 276 | } 277 | Ok(()) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/message.rs: -------------------------------------------------------------------------------- 1 | //! This module provides the messages use to send status updates back to the 2 | //! caller of nixtract. This is, for instance, usefull when writing a UI for 3 | //! nixtract. 4 | 5 | use std::fmt; 6 | 7 | /// Define an enum for the status 8 | #[derive(serde::Serialize)] 9 | pub enum Status { 10 | Started, 11 | Completed, 12 | Skipped, 13 | } 14 | 15 | /// Combine the struct and enum into a new Message struct 16 | #[derive(serde::Serialize)] 17 | pub struct Message { 18 | pub status: Status, 19 | pub id: usize, 20 | pub path: String, 21 | } 22 | 23 | // Implement Display for the Status enum 24 | impl fmt::Display for Status { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | match self { 27 | Status::Started => write!(f, "started"), 28 | Status::Completed => write!(f, "completed"), 29 | Status::Skipped => write!(f, "skipped"), 30 | } 31 | } 32 | } 33 | 34 | // Implement Display for the Message enum 35 | impl fmt::Display for Message { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | write!(f, "{} {}", self.status, self.path) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/nix/describe_derivation.nix: -------------------------------------------------------------------------------- 1 | # Describe a derivation 2 | # 3 | # Args (as environment variables): 4 | # TARGET_FLAKE_REF: flake reference to evaluate 5 | # TARGET_SYSTEM: system to evaluate 6 | # TARGET_ATTRIBUTE_PATH: attribute path to the derivation to evaluate 7 | # RUNTIME_ONLY: 1 if you only want to include "buildInputs" (only runtime dependencies), 0 if you want all dependencies 8 | # 9 | # Example: 10 | # TARGET_FLAKE_REF="nixpkgs" TARGET_SYSTEM="x86_64-linux" TARGET_ATTRIBUTE_PATH="python3" nix eval --json --file describe-derivation.nix 11 | 12 | let 13 | nixpkgs = builtins.getFlake "nixpkgs"; 14 | lib = import { inherit nixpkgs; }; 15 | 16 | # Arguments have to be taken from environment when using `nix` command 17 | targetFlakeRef = builtins.getEnv "TARGET_FLAKE_REF"; 18 | targetAttributePath = builtins.getEnv "TARGET_ATTRIBUTE_PATH"; 19 | targetSystem = let env = builtins.getEnv "TARGET_SYSTEM"; in if env == "" then builtins.currentSystem else env; 20 | # 0 is false, everything else is true 21 | runtimeOnly = if builtins.getEnv "RUNTIME_ONLY" == "0" then false else true; 22 | 23 | # Get pkgs 24 | targetFlake = builtins.getFlake targetFlakeRef; 25 | targetFlakePkgs = lib.getFlakePkgs targetFlake targetSystem; 26 | 27 | # Get target value 28 | targetValue = lib.getValueAtPath targetFlakePkgs targetAttributePath; 29 | in 30 | { 31 | name = targetValue.name; 32 | parsed_name = (builtins.parseDrvName targetValue.name); 33 | attribute_path = targetAttributePath; 34 | 35 | src = 36 | if targetValue ? src.gitRepoUrl && targetValue ? src.rev 37 | then 38 | { 39 | git_repo_url = targetValue.src.gitRepoUrl; 40 | rev = targetValue.src.rev; 41 | } 42 | else 43 | null; 44 | 45 | nixpkgs_metadata = 46 | { 47 | description = (builtins.tryEval (targetValue.meta.description or "")).value; 48 | pname = (builtins.tryEval (targetValue.pname or "")).value or null; 49 | version = (builtins.tryEval (targetValue.version or "")).value; 50 | broken = (builtins.tryEval (targetValue.meta.broken or false)).value; 51 | homepage = (builtins.tryEval (targetValue.meta.homepage or "")).value; 52 | licenses = (builtins.tryEval ( 53 | if builtins.isAttrs (targetValue.meta.license or null) 54 | # In case the license attribute is not a list, we produce a singleton list to be consistent 55 | then [{ 56 | spdx_id = targetValue.meta.license.spdxId or null; 57 | full_name = targetValue.meta.license.fullName or null; 58 | }] 59 | # In case the license attribute is a list 60 | else if builtins.isList (targetValue.meta.license or null) 61 | then 62 | builtins.map 63 | (l: { 64 | spdx_id = l.spdxId or null; 65 | full_name = l.fullName or null; 66 | }) 67 | targetValue.meta.license 68 | else null 69 | )).value; 70 | }; 71 | 72 | # path to the evaluated derivation file 73 | derivation_Path = lib.safePlatformDrvEval targetSystem (drv: drv.drvPath) targetValue; 74 | 75 | # path to the realized (=built) derivation 76 | # note: we can't name it `outPath` because serialization would only output it instead of dict, see Nix `toString` docs 77 | output_path = 78 | # TODO meaningfully represent when it's not the right platform (instead of null) 79 | lib.safePlatformDrvEval 80 | targetSystem 81 | (drv: drv.outPath) 82 | targetValue; 83 | outputs = map (name: { inherit name; output_path = lib.safePlatformDrvEval targetSystem (drv: drv.outPath) targetValue.${name}; }) (targetValue.outputs or [ ]); 84 | build_inputs = 85 | if targetValue ? outputHash then [ ] else 86 | nixpkgs.lib.concatMap 87 | ({ name, value }: 88 | if nixpkgs.lib.isDerivation value then 89 | [{ 90 | build_input_type = name; 91 | attribute_path = "${targetAttributePath}.drvAttrs.${name}"; 92 | output_path = lib.safePlatformDrvEval targetSystem (drv: drv.outPath) value; 93 | }] 94 | else if nixpkgs.lib.isList value then 95 | nixpkgs.lib.concatMap 96 | ({ index, value }: 97 | if nixpkgs.lib.isDerivation value then 98 | [{ 99 | build_input_type = name; 100 | attribute_path = "${targetAttributePath}.drvAttrs.${name}.${builtins.toString index}"; 101 | output_path = lib.safePlatformDrvEval targetSystem (drv: drv.outPath) value; 102 | }] 103 | else [ ] 104 | ) 105 | (lib.enumerate value) 106 | else [ ] 107 | ) 108 | (if runtimeOnly 109 | then 110 | ( 111 | nixpkgs.lib.optional (targetValue ? buildInputs) targetValue.buildInputs 112 | ++ nixpkgs.lib.optional (targetValue ? propagatedBuildInputs) targetValue.propagatedBuildInputs 113 | ) 114 | else 115 | nixpkgs.lib.attrsToList targetValue.drvAttrs 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /src/nix/describe_derivation.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, process::Command}; 2 | 3 | use schemars::JsonSchema; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use super::lib::Lib; 7 | use crate::error::{Error, Result}; 8 | 9 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, JsonSchema)] 10 | /// All information of a derivation that is extracted directly from nix 11 | pub struct DerivationDescription { 12 | pub attribute_path: String, 13 | pub derivation_path: Option, 14 | pub output_path: Option, 15 | pub outputs: Vec, 16 | pub name: String, 17 | pub parsed_name: ParsedName, 18 | pub nixpkgs_metadata: NixpkgsMetadata, 19 | pub src: Option, 20 | pub build_inputs: Vec, 21 | 22 | #[serde(skip_deserializing)] 23 | pub nar_info: Option, 24 | } 25 | 26 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, JsonSchema)] 27 | pub struct Output { 28 | pub name: String, 29 | pub output_path: Option, 30 | } 31 | 32 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, JsonSchema)] 33 | pub struct ParsedName { 34 | pub name: String, 35 | pub version: String, 36 | } 37 | 38 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, JsonSchema)] 39 | pub struct NixpkgsMetadata { 40 | pub description: String, 41 | pub pname: String, 42 | pub version: String, 43 | pub broken: bool, 44 | pub homepage: String, 45 | pub licenses: Option>, 46 | } 47 | 48 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, JsonSchema)] 49 | pub struct Source { 50 | pub git_repo_url: String, 51 | // Revision or tag of the git repo 52 | pub rev: String, 53 | } 54 | 55 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, JsonSchema)] 56 | pub struct License { 57 | // Not all licenses in nixpkgs have an associated spdx id 58 | pub spdx_id: Option, 59 | pub full_name: String, 60 | } 61 | 62 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, JsonSchema)] 63 | pub struct BuiltInput { 64 | pub attribute_path: String, 65 | pub build_input_type: String, 66 | pub output_path: Option, 67 | } 68 | 69 | #[derive(Clone)] 70 | pub struct DescribeDerivationArgs<'a> { 71 | pub flake_ref: &'a String, 72 | pub system: &'a Option, 73 | pub attribute_path: String, 74 | pub offline: bool, 75 | pub runtime_only: bool, 76 | pub include_nar_info: bool, 77 | pub binary_caches: &'a [String], 78 | pub lib: &'a Lib, 79 | } 80 | 81 | impl<'a> From> for DescribeDerivationArgs<'a> { 82 | fn from(args: crate::ProcessingArgs<'a>) -> Self { 83 | DescribeDerivationArgs { 84 | flake_ref: args.flake_ref, 85 | system: args.system, 86 | attribute_path: args.attribute_path, 87 | offline: args.offline, 88 | runtime_only: args.runtime_only, 89 | include_nar_info: args.include_nar_info, 90 | binary_caches: args.binary_caches, 91 | lib: args.lib, 92 | } 93 | } 94 | } 95 | 96 | pub fn describe_derivation(args: &DescribeDerivationArgs) -> Result { 97 | let expr = include_str!("describe_derivation.nix"); 98 | 99 | // Create a scope so env_vars isn't needlessly mutable 100 | let env_vars: HashMap = { 101 | let mut res = HashMap::from([ 102 | ("TARGET_FLAKE_REF".to_owned(), args.flake_ref.to_owned()), 103 | ( 104 | "TARGET_ATTRIBUTE_PATH".to_owned(), 105 | args.attribute_path.to_owned(), 106 | ), 107 | ("NIXPKGS_ALLOW_UNFREE".to_owned(), "1".to_owned()), 108 | ("NIXPKGS_ALLOW_INSECURE".to_owned(), "1".to_owned()), 109 | ("NIXPKGS_ALLOW_BROKEN".to_owned(), "1".to_owned()), 110 | ( 111 | "RUNTIME_ONLY".to_owned(), 112 | if args.runtime_only { "1" } else { "0" }.to_owned(), 113 | ), 114 | ]); 115 | if let Some(system) = args.system { 116 | res.insert("TARGET_SYSTEM".to_owned(), system.to_owned()); 117 | } 118 | res 119 | }; 120 | 121 | // Run the nix command, with the provided environment variables and expression 122 | let mut command: Command = Command::new("nix"); 123 | command 124 | .arg("eval") 125 | .arg("-I") 126 | .arg(format!("lib={}", args.lib.path().to_string_lossy())) 127 | .args(["--json", "--expr", expr]) 128 | .arg("--impure") 129 | .args(["--extra-experimental-features", "flakes nix-command"]) 130 | .envs(env_vars); 131 | 132 | // Add --offline if offline is set 133 | if args.offline { 134 | command.arg("--offline"); 135 | } 136 | 137 | let output = command.output()?; 138 | 139 | // Get stdout, stderr as a String 140 | let stdout = String::from_utf8_lossy(&output.stdout); 141 | let stderr = String::from_utf8_lossy(&output.stderr); 142 | 143 | log::debug!("stdout: {}", stdout); 144 | 145 | // Check if the nix command was successful 146 | if !output.status.success() { 147 | return Err(Error::NixCommand(output.status.code(), stderr.to_string())); 148 | } 149 | 150 | // Parse the stdout as JSON 151 | let mut description: DerivationDescription = match serde_json::from_str(stdout.trim()) { 152 | Ok(description) => description, 153 | Err(e) => return Err(Error::SerdeJSON(args.attribute_path.to_owned(), e)), 154 | }; 155 | 156 | if args.include_nar_info && description.output_path.is_some() { 157 | let output_path = description.output_path.clone().unwrap(); 158 | let narinfo = super::narinfo::NarInfo::fetch(&output_path, args.binary_caches)?; 159 | 160 | description.nar_info = narinfo; 161 | }; 162 | 163 | Ok(description) 164 | } 165 | -------------------------------------------------------------------------------- /src/nix/find_attribute_paths.nix: -------------------------------------------------------------------------------- 1 | /* Find the attribute paths to derivations directly available in a flake. 2 | 3 | Results are written to stderr, prefixed by `trace: `, as a JSON dict with key "foundDrvs". 4 | 5 | Args (as environment variables): 6 | TARGET_FLAKE_REF: flake reference to evaluate 7 | TARGET_ATTRIBUTE_PATH: attribute to evaluate 8 | TARGET_SYSTEM: system to evaluate 9 | 10 | Example: 11 | TARGET_FLAKE_REF="nixpkgs" TARGET_ATTRIBUTE_PATH="haskellPackages.hello" TARGET_SYSTEM="x86_64-linux" nix eval --json --file ./find-attribute-paths.nix 12 | */ 13 | 14 | let 15 | nixpkgs = builtins.getFlake "nixpkgs"; 16 | lib = import { inherit nixpkgs; }; 17 | 18 | # Arguments have to be taken from environment when using `nix` command 19 | targetFlakeRef = builtins.getEnv "TARGET_FLAKE_REF"; 20 | targetAttributePath = builtins.getEnv "TARGET_ATTRIBUTE_PATH"; 21 | targetSystem = let env = builtins.getEnv "TARGET_SYSTEM"; in if env == "" then builtins.currentSystem else env; 22 | 23 | # Get pkgs 24 | targetFlake = builtins.getFlake targetFlakeRef; 25 | targetFlakePkgs = lib.getFlakePkgs targetFlake targetSystem; 26 | targetRootValue = 27 | if isNull targetAttributePath || targetAttributePath == "" 28 | then targetFlakePkgs 29 | else lib.getValueAtPath targetFlakePkgs targetAttributePath; 30 | 31 | # Describe briefly found derivation 32 | describeDrv = drv: { 33 | derivationPath = lib.safePlatformDrvEval targetSystem (drv: drv.drvPath) drv; 34 | outputPath = lib.safePlatformDrvEval targetSystem (drv: drv.outPath) drv; 35 | }; 36 | 37 | # Helper function to find derivations in a deeply nested attribute set. 38 | # To be used on key-value pairs in an attribute set. 39 | # While recursing, it builds the attribute path to the currently evaluated key-value pair. 40 | # It returns either the current attribute path to the value if it is a derivation, or a deeply nested attribute set of attribute paths to derivations. 41 | # It should be used with `builtins.mapAttrs` and `lib.collect` to build a list of attribute paths to all derivations. 42 | # Args: 43 | # parentPath: attribute path to the parent attribute set 44 | # name: key in the currently evaluated key-value pair 45 | # value: value in the currently evaluated key-value pair 46 | # Usage: 47 | # builtins.mapAttrs (findRecursively "") (builtins.getFlake "nixpkgs") 48 | findRecursively = 49 | parentPath: key: value': 50 | let 51 | # compute attribute path to current attribute set from root attribute set 52 | attributePath = if key == null then null else (if parentPath == "" then "" else parentPath + ".") + key; 53 | value = lib.safeEval value'; 54 | in 55 | if nixpkgs.lib.isDerivation value then 56 | # yield found derivation 57 | # if it has multiple output derivations, yield them instead 58 | let 59 | foundDrvs = 60 | if value ? outputs 61 | then (map (name: describeDrv value.${name} // { attributePath = attributePath + ".${name}"; }) value.outputs) 62 | else [ ((describeDrv value) // { inherit attributePath; }) ]; 63 | in 64 | builtins.trace (builtins.toJSON { inherit foundDrvs; }) foundDrvs 65 | else 66 | # recurse when the current value is an attribute set, otherwise stop 67 | if (nixpkgs.lib.isAttrs value) && (value.recurseForDerivations or false) 68 | then 69 | builtins.mapAttrs (findRecursively attributePath) value 70 | else 71 | null 72 | ; 73 | in 74 | # to prevent accumlutation in memory 75 | lib.collect (x: false) (builtins.mapAttrs (findRecursively targetAttributePath) targetRootValue) 76 | -------------------------------------------------------------------------------- /src/nix/find_attribute_paths.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, process::Command}; 2 | 3 | use serde::Deserialize; 4 | 5 | use super::lib::Lib; 6 | use crate::error::{Error, Result}; 7 | 8 | #[derive(Default, Debug, Clone, Deserialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct AttributePaths { 11 | pub found_drvs: Vec, 12 | } 13 | 14 | #[derive(Default, Debug, Clone, Deserialize, PartialEq, Hash, Eq)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct FoundDrv { 17 | pub attribute_path: String, 18 | /// drv path of the derivation 19 | /// We discard any values that are not null or String, which occasionally occur (namely false) 20 | pub derivation_path: Option, 21 | /// The output path of the derivation 22 | /// We discard any values that are not null or String, which occasionally occur (namely false) 23 | pub output_path: Option, 24 | } 25 | 26 | pub fn find_attribute_paths( 27 | flake_ref: &String, 28 | system: &Option, 29 | attribute_path: &Option, 30 | offline: &bool, 31 | lib: &Lib, 32 | ) -> Result> { 33 | let expr = include_str!("find_attribute_paths.nix"); 34 | 35 | // Create a scope so env_vars isn't needlessly mutable 36 | let env_vars: HashMap = { 37 | let mut res = HashMap::from([ 38 | ("TARGET_FLAKE_REF".to_owned(), flake_ref.to_owned()), 39 | ("NIXPKGS_ALLOW_UNFREE".to_owned(), "1".to_owned()), 40 | ("NIXPKGS_ALLOW_INSECURE".to_owned(), "1".to_owned()), 41 | ("NIXPKGS_ALLOW_BROKEN".to_owned(), "1".to_owned()), 42 | ]); 43 | if let Some(attribute_path) = attribute_path { 44 | res.insert("TARGET_ATTRIBUTE_PATH".to_owned(), attribute_path.clone()); 45 | } 46 | if let Some(system) = system { 47 | res.insert("TARGET_SYSTEM".to_owned(), system.to_owned()); 48 | } 49 | res 50 | }; 51 | 52 | // Run the nix command, with the provided environment variables and expression 53 | let mut command: Command = Command::new("nix"); 54 | command 55 | .arg("eval") 56 | .arg("-I") 57 | .arg(format!("lib={}", lib.path().to_string_lossy())) 58 | .args(["--json", "--expr", expr]) 59 | .arg("--impure") 60 | .args(["--extra-experimental-features", "flakes nix-command"]) 61 | .envs(env_vars); 62 | 63 | if *offline { 64 | command.arg("--offline"); 65 | } 66 | 67 | let output = command.output()?; 68 | 69 | let stderr = String::from_utf8_lossy(&output.stderr); 70 | 71 | // Check if the nix command was successful 72 | if !output.status.success() { 73 | return Err(Error::NixCommand(output.status.code(), stderr.to_string())); 74 | } 75 | 76 | let mut res: Vec = Vec::new(); 77 | 78 | for line in stderr.lines() { 79 | log::info!("find_attribute_paths line: {}", line); 80 | 81 | if !line.starts_with("trace: ") { 82 | log::warn!( 83 | "Unexpected output from nix command, attempting to continue: {}", 84 | line 85 | ); 86 | } else { 87 | match serde_json::from_str(line.trim_start_matches("trace: ")) { 88 | Ok(attribute_paths) => res.push(attribute_paths), 89 | Err(e) => { 90 | log::warn!( 91 | "Error parsing found_derivation output: {} {}. Attempting to continue...", 92 | attribute_path.clone().unwrap_or_default(), 93 | e 94 | ); 95 | } 96 | }; 97 | } 98 | } 99 | 100 | Ok(res) 101 | } 102 | -------------------------------------------------------------------------------- /src/nix/lib.nix: -------------------------------------------------------------------------------- 1 | { nixpkgs ? builtins.getFlake "nixpkgs" }: 2 | 3 | rec { 4 | /* Utility function to enumerate a list 5 | Type: [a] -> [(int, a)] 6 | 7 | Example: 8 | enumerate [ "a" "b" ] 9 | => [ { index = 0; value = "a"; } { index = 1; value = "b"; } ] 10 | */ 11 | enumerate = lst: map (zippedElem: { index = zippedElem.fst; value = zippedElem.snd; }) (nixpkgs.lib.zipLists (nixpkgs.lib.range 0 (builtins.length lst)) lst); 12 | 13 | /* To camel case to snake case 14 | Type: string -> string 15 | */ 16 | toSnakeCase = string: nixpkgs.lib.concatStrings (map (s: if builtins.isString s then s else "_" + nixpkgs.lib.toLower (builtins.elemAt s 0)) (builtins.split "([A-Z])" string)); 17 | 18 | /* A modified version of nixpkgs lib.attrsets.collect to go inside lists as well 19 | */ 20 | collect = 21 | pred: 22 | v: 23 | if pred v then 24 | [ v ] 25 | else if nixpkgs.lib.isAttrs v then 26 | nixpkgs.lib.concatMap (collect pred) (nixpkgs.lib.attrValues v) 27 | else if nixpkgs.lib.isList v then 28 | nixpkgs.lib.concatMap (collect pred) v 29 | else 30 | [ ]; 31 | 32 | /* Packages in a flake are usually a flat attribute set in outputs, but legacy systems use `legacyPackages` 33 | */ 34 | getFlakePkgs = flake: targetSystem: flake.outputs.packages.${targetSystem} or flake.outputs.defaultPackage.${targetSystem} or flake.outputs.legacyPackages.${targetSystem} or { }; 35 | 36 | 37 | /* Follow "attribute path" (split by dot) to access value in tree of nested attribute sets and lists 38 | Type: (attrs | list) -> str -> any 39 | 40 | Examples: 41 | getValueAtPath {a = { b = 1; c = [ 2 ]; }; } "a.b" 42 | => 1 43 | 44 | getValueAtPath {a = { b = 1; c = [ 2 ]; }; } "a.c.0" 45 | => 2 46 | */ 47 | getValueAtPath = 48 | collection: attributePath: 49 | let 50 | recurse = 51 | collection: pathList: 52 | let 53 | x = builtins.head pathList; 54 | value = 55 | if nixpkgs.lib.isAttrs collection 56 | then collection.${x} 57 | else 58 | if nixpkgs.lib.isList collection 59 | then 60 | let index = nixpkgs.lib.toIntBase10 x; 61 | in builtins.elemAt collection index 62 | else builtins.throw "Trying to follow path in neither an attribute set nor a list"; 63 | in 64 | if builtins.length pathList > 1 then 65 | let 66 | # need to skip an item, see `builtins.split` doc 67 | xs = builtins.tail (builtins.tail pathList); 68 | in 69 | recurse value xs 70 | else value; 71 | in 72 | recurse collection (builtins.split "\\." attributePath); 73 | 74 | /* Utility function for safe evaluation of any value, null if evaluation fails 75 | */ 76 | safeEval = v: let 77 | r = builtins.tryEval v; 78 | in 79 | if r.success 80 | then r.value 81 | else null; 82 | 83 | /* Utility specific to nixpkgs, as nixpkgs prevents computing some fields if meta.platforms does not contain the target system 84 | Args: 85 | targetSystem: (string) target system 86 | f: (derivation -> a) function to apply to derivation 87 | drv: derivation 88 | */ 89 | safePlatformDrvEval = 90 | targetSystem: f: drv: 91 | if !(builtins.elem targetSystem (drv.meta.platforms or [ targetSystem ])) 92 | then null 93 | else safeEval (f drv); 94 | } 95 | -------------------------------------------------------------------------------- /src/nix/lib.rs: -------------------------------------------------------------------------------- 1 | //! Nix only accepts a file as included files, so we need to create a temporary file to pass to it 2 | 3 | use crate::error::Result; 4 | use std::io::Write; 5 | use tempfile::NamedTempFile; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct Lib { 9 | inner: std::path::PathBuf, 10 | } 11 | 12 | impl Lib { 13 | pub fn new() -> Result { 14 | let mut file = NamedTempFile::new()?; 15 | 16 | let lib = include_str!("lib.nix"); 17 | 18 | write!(file, "{}", lib)?; 19 | 20 | let inner = file.into_temp_path().keep().unwrap(); 21 | 22 | Ok(Lib { inner }) 23 | } 24 | 25 | pub fn path(&self) -> &std::path::Path { 26 | &self.inner 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/nix/mod.rs: -------------------------------------------------------------------------------- 1 | mod describe_derivation; 2 | mod find_attribute_paths; 3 | pub(crate) mod lib; 4 | pub mod narinfo; 5 | pub mod substituters; 6 | 7 | pub use describe_derivation::*; 8 | pub use find_attribute_paths::*; 9 | -------------------------------------------------------------------------------- /src/nix/narinfo.rs: -------------------------------------------------------------------------------- 1 | #[derive( 2 | Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema, 3 | )] 4 | 5 | /// `NarInfo` is a struct that represents the information about a Nix Archive (nar). 6 | pub struct NarInfo { 7 | /// The path to the store where the nar is located. 8 | pub store_path: String, 9 | /// The URL where the nar can be downloaded. 10 | pub url: String, 11 | /// The hash of the nar. 12 | pub nar_hash: String, 13 | /// The size of the nar in bytes. 14 | pub nar_size: usize, 15 | /// The compression method used on the nar. 16 | pub compression: String, 17 | /// The hash of the file. 18 | pub file_hash: Option, 19 | /// The size of the file in bytes. 20 | pub file_size: Option, 21 | /// The deriver of the nar, if any. 22 | pub deriver: Option, 23 | /// The system for which the nar is intended, if specified. 24 | pub system: Option, 25 | /// The references of the nar. 26 | pub references: Option>, 27 | /// The signature of the nar. 28 | pub sig: Option, 29 | /// The content addressable storage identifier of the nar, if any. 30 | pub ca: Option, 31 | } 32 | 33 | impl NarInfo { 34 | /// Fetches the narinfo file for a given output path from a list of servers. 35 | /// 36 | /// # Arguments 37 | /// 38 | /// * `output_path` - The output path of the narinfo file. 39 | /// * `servers` - A slice of server URLs to fetch the narinfo file from. 40 | /// 41 | /// # Returns 42 | /// 43 | /// Returns a `Result` containing an `Option` of `NarInfo` if the narinfo file is found, or `None` if it is not found. 44 | /// 45 | /// # Errors 46 | /// 47 | /// Returns an error if the output path is invalid or if there is an error fetching or parsing the narinfo file. 48 | /// 49 | /// # Example 50 | /// 51 | /// ``` 52 | /// use nixtract::narinfo::NarInfo; 53 | /// 54 | /// let output_path = "/nix/store/abc123"; 55 | /// let servers = vec!["server1.example.com".to_string(), "server2.example.com".to_string()]; 56 | /// 57 | /// match NarInfo::fetch(output_path, &servers) { 58 | /// Ok(Some(narinfo)) => { 59 | /// // Narinfo file found, do something with it 60 | /// }, 61 | /// Ok(None) => { 62 | /// // Narinfo file not found 63 | /// }, 64 | /// Err(err) => { 65 | /// // Error fetching or parsing narinfo file 66 | /// } 67 | /// } 68 | /// ``` 69 | pub fn fetch(output_path: &str, servers: &[String]) -> crate::error::Result> { 70 | // Strip the /nix/store prefix, and everything after the first - 71 | let hash = output_path 72 | .strip_prefix("/nix/store/") 73 | .ok_or_else(|| crate::error::Error::NarInfoInvalidPath(output_path.to_string()))? 74 | .split('-') 75 | .next() 76 | .ok_or_else(|| crate::error::Error::NarInfoInvalidPath(output_path.to_string()))?; 77 | 78 | for server in servers { 79 | let url = format!( 80 | "{}{}{}.narinfo", 81 | server, 82 | if server.ends_with('/') { "" } else { "/" }, 83 | hash 84 | ); 85 | 86 | log::info!("Fetching narinfo from {}", url); 87 | match reqwest::blocking::get(&url) { 88 | Ok(response) => { 89 | if response.status().is_success() { 90 | let narinfo = response.text()?; 91 | let narinfo = Self::parse(&narinfo)?; 92 | return Ok(Some(narinfo)); 93 | } else { 94 | log::warn!("Cache responded with error code: {}", response.status()); 95 | } 96 | } 97 | Err(err) => log::warn!("Could not fetch narinfo: {}", err), 98 | } 99 | } 100 | 101 | Ok(None) 102 | } 103 | 104 | /// Parses a `narinfo` string into a `NarInfo` struct. 105 | /// 106 | /// This function takes a `narinfo` string and parses it into a `NarInfo` struct. 107 | /// It iterates over each line in the `narinfo` string, splitting it into a key-value pair. 108 | /// The key is then matched to a corresponding field in the `NarInfo` struct. 109 | /// If a key does not match any field, an error is returned. 110 | /// If a required field is missing from the `narinfo` string, an error is returned. 111 | /// 112 | /// # Arguments 113 | /// 114 | /// * `narinfo` - A string slice that holds the `narinfo` data. 115 | /// 116 | /// # Returns 117 | /// 118 | /// * `Result` - Returns a `NarInfo` struct if parsing is successful, otherwise returns an `Error`. 119 | /// 120 | /// # Errors 121 | /// 122 | /// This function will return an error if: 123 | /// * A line in the `narinfo` string does not contain a ':' delimiter. 124 | /// * The `narinfo` string contains an unknown key. 125 | /// * A required field is missing from the `narinfo` string. 126 | pub fn parse(narinfo: &str) -> crate::error::Result { 127 | let mut store_path = None; 128 | let mut url = None; 129 | let mut compression = None; 130 | let mut file_hash = None; 131 | let mut file_size = None; 132 | let mut nar_hash = None; 133 | let mut nar_size = None; 134 | let mut deriver = None; 135 | let mut system = None; 136 | let mut references = Vec::new(); 137 | let mut sig = None; 138 | let mut ca = None; 139 | 140 | for line in narinfo.lines() { 141 | let (key, value) = line 142 | .split_once(':') 143 | .ok_or_else(|| crate::error::Error::NarInfoNoDelimiter(line.to_string())) 144 | .map(|(key, value)| (key.trim(), value.trim()))?; 145 | 146 | match key { 147 | "StorePath" => store_path = Some(value.to_string()), 148 | "URL" => url = Some(value.to_string()), 149 | "Compression" => compression = Some(value.to_string()), 150 | "FileHash" => file_hash = Some(value.to_string()), 151 | "FileSize" => file_size = Some(value.parse()?), 152 | "NarHash" => nar_hash = Some(value.to_string()), 153 | "NarSize" => nar_size = Some(value.parse()?), 154 | "Deriver" => deriver = Some(value.to_string()), 155 | "System" => system = Some(value.to_string()), 156 | "References" => references = value.split(' ').map(|s| s.to_string()).collect(), 157 | "Sig" => sig = Some(value.to_string()), 158 | "CA" => ca = Some(value.to_string()), 159 | _ => { 160 | log::warn!( 161 | "Found an unknown key while parsing a .narinfo file ({}). Please report this issue to github.com/tweag/nixtract", 162 | key 163 | ); 164 | } 165 | } 166 | } 167 | 168 | // Ensure narSize > 0: https://github.com/nixos/nix/blob/0d26358bda1637e54ebacac18e7b5af7381cf5f3/src/libstore/nar-info.cc#L98 169 | if nar_size.unwrap_or(0) == 0 { 170 | return Err(crate::error::Error::NarInfoInvalidField( 171 | "NarSize".to_string(), 172 | "NarSize must be greater than 0".to_string(), 173 | )); 174 | } 175 | 176 | // If Compression was not present, assume bzip: https://github.com/nixos/nix/blob/0d26358bda1637e54ebacac18e7b5af7381cf5f3/src/libstore/nar-info.cc#L96 177 | let compression = compression.unwrap_or_else(|| "bzip".to_string()); 178 | 179 | // If References was empty, parse it as None instead of the empty vector 180 | let references = if references.is_empty() { 181 | None 182 | } else { 183 | Some(references) 184 | }; 185 | 186 | Ok(NarInfo { 187 | store_path: store_path 188 | .ok_or_else(|| crate::error::Error::NarInfoMissingField("StorePath".to_string()))?, 189 | url: url.ok_or_else(|| crate::error::Error::NarInfoMissingField("URL".to_string()))?, 190 | nar_hash: nar_hash 191 | .ok_or_else(|| crate::error::Error::NarInfoMissingField("NarHash".to_string()))?, 192 | nar_size: nar_size 193 | .ok_or_else(|| crate::error::Error::NarInfoMissingField("NarSize".to_string()))?, 194 | file_hash, 195 | file_size, 196 | compression, 197 | deriver, 198 | system, 199 | references, 200 | sig, 201 | ca, 202 | }) 203 | } 204 | } 205 | 206 | #[cfg(test)] 207 | mod tests { 208 | use super::*; 209 | 210 | #[test] 211 | fn test_fetch() { 212 | let result = NarInfo::fetch( 213 | "/nix/store/cg8a576pz2yfc1wbhxm1zy4x7lrk8pix-hello-2.12.1", 214 | &["https://cache.nixos.org".to_owned()], 215 | ) 216 | .unwrap(); 217 | 218 | let expected = NarInfo { 219 | store_path: "/nix/store/cg8a576pz2yfc1wbhxm1zy4x7lrk8pix-hello-2.12.1".to_string(), 220 | url: "nar/1wjh5hhqfi30fx8pqi0901c9n035qbwsv1rmizvmpydva2lpri2g.nar.xz".to_string(), 221 | compression: "xz".to_string(), 222 | file_hash: Some("sha256:1wjh5hhqfi30fx8pqi0901c9n035qbwsv1rmizvmpydva2lpri2g".to_string()), 223 | file_size: Some(50184), 224 | nar_hash: "sha256:0scilhfg9qij3wiz1irrln5nb5nk3nxfkns6yqfh2kvbaixywv26".to_string(), 225 | nar_size: 226552, 226 | deriver: Some("57677sld6ja212hkv1gh8bdm0amnk1hz-hello-2.12.1.drv".to_string()), 227 | system: None, 228 | references: Some(vec![ 229 | "cg8a576pz2yfc1wbhxm1zy4x7lrk8pix-hello-2.12.1".to_string(), 230 | "gqghjch4p1s69sv4mcjksb2kb65rwqjy-glibc-2.38-23".to_string(), 231 | ]), 232 | sig: Some("cache.nixos.org-1:WzRvexDdRP62D8j/4rAk73vAc4gUtAN7qpZesuRc74+My03WcvWxg/LUztmWikOaMqJQJMvB1ria6AIX30yrDw==".to_string()), 233 | ca: None, 234 | }; 235 | 236 | pretty_assertions::assert_eq!(result, Some(expected)); 237 | } 238 | 239 | #[test] 240 | fn test_parse() { 241 | let narinfo = "StorePath: /nix/store/cg8a576pz2yfc1wbhxm1zy4x7lrk8pix-hello-2.12.1 242 | URL: nar/1wjh5hhqfi30fx8pqi0901c9n035qbwsv1rmizvmpydva2lpri2g.nar.xz 243 | Compression: xz 244 | FileHash: sha256:1wjh5hhqfi30fx8pqi0901c9n035qbwsv1rmizvmpydva2lpri2g 245 | FileSize: 50184 246 | NarHash: sha256:0scilhfg9qij3wiz1irrln5nb5nk3nxfkns6yqfh2kvbaixywv26 247 | NarSize: 226552 248 | References: cg8a576pz2yfc1wbhxm1zy4x7lrk8pix-hello-2.12.1 gqghjch4p1s69sv4mcjksb2kb65rwqjy-glibc-2.38-23 249 | Deriver: 57677sld6ja212hkv1gh8bdm0amnk1hz-hello-2.12.1.drv 250 | Sig: cache.nixos.org-1:WzRvexDdRP62D8j/4rAk73vAc4gUtAN7qpZesuRc74+My03WcvWxg/LUztmWikOaMqJQJMvB1ria6AIX30yrDw== 251 | "; 252 | 253 | let expected = NarInfo { 254 | store_path: "/nix/store/cg8a576pz2yfc1wbhxm1zy4x7lrk8pix-hello-2.12.1".to_string(), 255 | url: "nar/1wjh5hhqfi30fx8pqi0901c9n035qbwsv1rmizvmpydva2lpri2g.nar.xz".to_string(), 256 | compression: "xz".to_string(), 257 | file_hash: Some("sha256:1wjh5hhqfi30fx8pqi0901c9n035qbwsv1rmizvmpydva2lpri2g".to_string()), 258 | file_size: Some(50184), 259 | nar_hash: "sha256:0scilhfg9qij3wiz1irrln5nb5nk3nxfkns6yqfh2kvbaixywv26".to_string(), 260 | nar_size: 226552, 261 | deriver: Some("57677sld6ja212hkv1gh8bdm0amnk1hz-hello-2.12.1.drv".to_string()), 262 | system: None, 263 | references: Some(vec![ 264 | "cg8a576pz2yfc1wbhxm1zy4x7lrk8pix-hello-2.12.1".to_string(), 265 | "gqghjch4p1s69sv4mcjksb2kb65rwqjy-glibc-2.38-23".to_string(), 266 | ]), 267 | sig: Some("cache.nixos.org-1:WzRvexDdRP62D8j/4rAk73vAc4gUtAN7qpZesuRc74+My03WcvWxg/LUztmWikOaMqJQJMvB1ria6AIX30yrDw==".to_string()), 268 | ca: None, 269 | }; 270 | 271 | let result = NarInfo::parse(narinfo).unwrap(); 272 | pretty_assertions::assert_eq!(result, expected); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/nix/substituters.rs: -------------------------------------------------------------------------------- 1 | //! This modules parses the get the substituters from the local nix install and the flake ref and combines them into a single list 2 | 3 | use crate::error::{Error, Result}; 4 | 5 | pub type Substituters = Vec; 6 | 7 | fn from_flake_ref(flake_ref: &str) -> Result { 8 | let expr = format!( 9 | "(import ((builtins.getFlake \"{}\").outPath + \"/flake.nix\")).nixConfig.extra-substituters or []", 10 | flake_ref 11 | ); 12 | 13 | // Call nix eval on the flake to get the json representation of the nixConfig 14 | let output = std::process::Command::new("nix") 15 | .args(["eval", "--json", "--impure"]) 16 | .args(["--expr", &expr]) 17 | .args(["--extra-experimental-features", "flakes nix-command"]) 18 | .output()?; 19 | 20 | let stdout = String::from_utf8_lossy(&output.stdout); 21 | let stderr = String::from_utf8_lossy(&output.stderr); 22 | 23 | // Check if the nix command was successful 24 | if !output.status.success() { 25 | return Err(Error::NixCommand(output.status.code(), stderr.to_string())); 26 | } 27 | 28 | let extra_substituters = match serde_json::from_str(stdout.trim()) { 29 | Ok(extra_substituters) => extra_substituters, 30 | Err(e) => return Err(Error::SerdeJSON(flake_ref.to_owned(), e)), 31 | }; 32 | 33 | Ok(extra_substituters) 34 | } 35 | 36 | fn from_nix_conf() -> Result { 37 | let output = std::process::Command::new("nix") 38 | .args(["show-config", "--json"]) 39 | .args(["--extra-experimental-features", "flakes nix-command"]) 40 | .output()?; 41 | 42 | let stdout = String::from_utf8_lossy(&output.stdout); 43 | let stderr = String::from_utf8_lossy(&output.stderr); 44 | 45 | // Check if the nix command was successful 46 | if !output.status.success() { 47 | return Err(Error::NixCommand(output.status.code(), stderr.to_string())); 48 | } 49 | 50 | let config: serde_json::Value = match serde_json::from_str(stdout.trim()) { 51 | Ok(config) => config, 52 | Err(e) => return Err(Error::SerdeJSON("nix.conf".to_owned(), e)), 53 | }; 54 | 55 | let substituters = config.get("substituters").and_then(|v| v.get("value")); 56 | 57 | // Parse the substituters array to a Vec 58 | let substituters = serde_json::from_value( 59 | substituters 60 | .unwrap_or(&serde_json::Value::Array(Vec::new())) 61 | .clone(), 62 | ) 63 | .map_err(|e| Error::SerdeJSON("nix.conf".to_owned(), e))?; 64 | 65 | Ok(substituters) 66 | } 67 | 68 | pub fn get_substituters(flake_ref: String) -> Result { 69 | let mut substituters = from_nix_conf()?; 70 | substituters.extend(from_flake_ref(&flake_ref)?); 71 | Ok(substituters) 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use super::*; 77 | 78 | #[test] 79 | fn test_from_flake_ref() { 80 | let flake_ref = "github:tweag/nixtract"; 81 | let substituters = from_flake_ref(flake_ref); 82 | 83 | assert!(substituters.is_ok()); 84 | } 85 | 86 | #[test] 87 | fn test_from_nix_conf() { 88 | let substituters = from_nix_conf(); 89 | 90 | assert!(substituters.is_ok()); 91 | } 92 | 93 | /// This test gets the substituters and passes them along to the 94 | /// NarInfo::fetch function to ensure they are correct. 95 | #[test] 96 | fn test_get_substituters() { 97 | let store_path = "/nix/store/1gxz5nfzfnhyxjdyzi04r86sh61y4i00-hello-2.12.1"; 98 | let substituters = get_substituters("nixpkgs".to_owned()).unwrap(); 99 | let nar_info = crate::narinfo::NarInfo::fetch(store_path, &substituters); 100 | 101 | assert!(nar_info.is_ok_and(|n| n.is_some_and(|n| n.store_path == store_path))) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/fixtures/flake-direct-buildInput/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1692799911, 9 | "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1688392541, 24 | "narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-22.11", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /tests/fixtures/flake-direct-buildInput/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11"; 3 | inputs.flake-utils.url = "github:numtide/flake-utils"; 4 | 5 | outputs = { flake-utils, nixpkgs, ... }: 6 | flake-utils.lib.eachDefaultSystem (system: 7 | let 8 | pkgs = nixpkgs.legacyPackages.${system}; 9 | 10 | pkg1 = pkgs.stdenv.mkDerivation { 11 | name = "pkg1"; 12 | src = ./pkg1; 13 | buildInputs = [ pkgs.bash ]; 14 | installPhase = '' 15 | mkdir -p $out/bin 16 | cp $src/* $out/bin 17 | ''; 18 | meta.license = pkgs.lib.licenses.gpl2Plus; 19 | }; 20 | 21 | pkg2 = pkgs.stdenv.mkDerivation { 22 | name = "pkg2"; 23 | src = ./pkg2; 24 | buildInputs = [ pkgs.bash pkg1 ]; 25 | buildPhase = '' 26 | pkg1 27 | ''; 28 | installPhase = '' 29 | mkdir -p $out/bin 30 | cp $src/* $out/bin 31 | ''; 32 | meta.license = with pkgs.lib; [ licenses.mit licenses.asl20 ]; 33 | }; 34 | 35 | in 36 | { 37 | packages.default = pkg2; 38 | } 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /tests/fixtures/flake-direct-buildInput/pkg1/pkg1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "pkg1" -------------------------------------------------------------------------------- /tests/fixtures/flake-direct-buildInput/pkg2/pkg2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "pkg2" -------------------------------------------------------------------------------- /tests/fixtures/flake-direct-nativeBuildInput/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1692799911, 9 | "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1688392541, 24 | "narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-22.11", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /tests/fixtures/flake-direct-nativeBuildInput/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11"; 3 | inputs.flake-utils.url = "github:numtide/flake-utils"; 4 | 5 | outputs = { flake-utils, nixpkgs, ... }: 6 | flake-utils.lib.eachDefaultSystem (system: 7 | let 8 | pkgs = nixpkgs.legacyPackages.${system}; 9 | 10 | pkg1 = pkgs.stdenv.mkDerivation { 11 | name = "pkg1"; 12 | src = ./pkg1; 13 | buildInputs = [ pkgs.bash ]; 14 | installPhase = '' 15 | mkdir -p $out/bin 16 | cp $src/* $out/bin 17 | ''; 18 | }; 19 | 20 | pkg2 = pkgs.stdenv.mkDerivation { 21 | name = "pkg2"; 22 | src = ./pkg2; 23 | nativeBuildInputs = [ pkgs.bash pkg1 ]; 24 | buildPhase = '' 25 | pkg1 26 | ''; 27 | installPhase = '' 28 | mkdir -p $out/bin 29 | cp $src/* $out/bin 30 | ''; 31 | }; 32 | 33 | in 34 | { 35 | packages.default = pkg2; 36 | } 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /tests/fixtures/flake-direct-nativeBuildInput/pkg1/pkg1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "pkg1" -------------------------------------------------------------------------------- /tests/fixtures/flake-direct-nativeBuildInput/pkg2/pkg2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "pkg2" -------------------------------------------------------------------------------- /tests/fixtures/flake-three-levels/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1692799911, 9 | "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1688392541, 24 | "narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-22.11", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /tests/fixtures/flake-three-levels/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11"; 3 | inputs.flake-utils.url = "github:numtide/flake-utils"; 4 | 5 | outputs = { flake-utils, nixpkgs, ... }: 6 | flake-utils.lib.eachDefaultSystem (system: 7 | let 8 | pkgs = nixpkgs.legacyPackages.${system}; 9 | 10 | pkg1 = pkgs.stdenv.mkDerivation { 11 | name = "pkg1"; 12 | src = ./pkg1; 13 | buildInputs = [ pkgs.bash ]; 14 | installPhase = '' 15 | mkdir -p $out/bin 16 | cp $src/* $out/bin 17 | ''; 18 | }; 19 | 20 | pkg2 = pkgs.stdenv.mkDerivation { 21 | name = "pkg2"; 22 | src = ./pkg2; 23 | buildInputs = [ pkgs.bash pkg1 ]; 24 | buildPhase = '' 25 | pkg1 26 | ''; 27 | installPhase = '' 28 | mkdir -p $out/bin 29 | cp $src/* $out/bin 30 | ''; 31 | }; 32 | 33 | pkg3 = pkgs.stdenv.mkDerivation { 34 | name = "pkg3"; 35 | src = ./pkg3; 36 | buildInputs = [ pkgs.bash pkg2 ]; 37 | buildPhase = '' 38 | pkg2 39 | ''; 40 | installPhase = '' 41 | mkdir -p $out/bin 42 | cp $src/* $out/bin 43 | ''; 44 | }; 45 | in 46 | { 47 | packages.default = pkg3; 48 | } 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /tests/fixtures/flake-three-levels/pkg1/pkg1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "pkg1" -------------------------------------------------------------------------------- /tests/fixtures/flake-three-levels/pkg2/pkg2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "pkg2" -------------------------------------------------------------------------------- /tests/fixtures/flake-three-levels/pkg3/pkg3: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "pkg3" 3 | -------------------------------------------------------------------------------- /tests/fixtures/flake-trivial-rust/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1681202837, 9 | "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "cfacdce06f30d2b68473a46042957675eebb3401", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "hello": { 22 | "flake": false, 23 | "locked": { 24 | "narHash": "sha256-xe8Hie69L308iLb9d7X5Kbvqm4z4X8PT4oW/TOZPwAM=", 25 | "type": "file", 26 | "url": "https://git.savannah.gnu.org/git/hello.git" 27 | }, 28 | "original": { 29 | "type": "file", 30 | "url": "https://git.savannah.gnu.org/git/hello.git" 31 | } 32 | }, 33 | "nixpkgs": { 34 | "locked": { 35 | "lastModified": 1684580438, 36 | "narHash": "sha256-LUPswmDn6fXP3lEBJFA2Id8PkcYDgzUilevWackYVvQ=", 37 | "owner": "nixos", 38 | "repo": "nixpkgs", 39 | "rev": "7dc71aef32e8faf065cb171700792cf8a65c152d", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "nixos", 44 | "ref": "nixos-22.11", 45 | "repo": "nixpkgs", 46 | "type": "github" 47 | } 48 | }, 49 | "root": { 50 | "inputs": { 51 | "flake-utils": "flake-utils", 52 | "hello": "hello", 53 | "nixpkgs": "nixpkgs" 54 | } 55 | }, 56 | "systems": { 57 | "locked": { 58 | "lastModified": 1681028828, 59 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 60 | "owner": "nix-systems", 61 | "repo": "default", 62 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 63 | "type": "github" 64 | }, 65 | "original": { 66 | "owner": "nix-systems", 67 | "repo": "default", 68 | "type": "github" 69 | } 70 | } 71 | }, 72 | "root": "root", 73 | "version": 7 74 | } 75 | -------------------------------------------------------------------------------- /tests/fixtures/flake-trivial-rust/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11"; 3 | inputs.flake-utils.url = "github:numtide/flake-utils"; 4 | inputs.hello = { 5 | url = "https://git.savannah.gnu.org/git/hello.git"; 6 | flake = false; 7 | }; 8 | 9 | outputs = { flake-utils, nixpkgs, hello, ... }: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let pkgs = nixpkgs.legacyPackages.${system}; 12 | in { 13 | packages.default = pkgs.rustPlatform.buildRustPackage { 14 | pname = "hello"; 15 | version = "0.1.0"; 16 | 17 | src = pkgs.fetchFromGitHub { 18 | owner = "hello-lang"; 19 | repo = "Rust"; 20 | rev = "8e8bd39a444f6d6c7b01046a6b0600273911ac58"; 21 | hash = "sha256-w3IRfqPsLFAY7OpLRaJpnIUIMhtJ71xi1PJGNttb9EQ="; 22 | }; 23 | 24 | cargoHash = "sha256-eouoalg2VO6SCG9oaCPqmlfWnKI+uy6ggPX7IKG1hz4="; 25 | 26 | meta = with pkgs.lib; { 27 | description = "Hello World! in Rust"; 28 | homepage = "https://github.com/hello-lang/Rust"; 29 | license = licenses.unlicense; 30 | maintainers = [ ]; 31 | }; 32 | 33 | }; 34 | } 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /tests/fixtures/flake-trivial/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1681202837, 9 | "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "cfacdce06f30d2b68473a46042957675eebb3401", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1684580438, 24 | "narHash": "sha256-LUPswmDn6fXP3lEBJFA2Id8PkcYDgzUilevWackYVvQ=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "7dc71aef32e8faf065cb171700792cf8a65c152d", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-22.11", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /tests/fixtures/flake-trivial/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11"; 3 | inputs.flake-utils.url = "github:numtide/flake-utils"; 4 | 5 | outputs = { flake-utils, nixpkgs, ... }: 6 | flake-utils.lib.eachDefaultSystem (system: 7 | let pkgs = nixpkgs.legacyPackages.${system}; 8 | in { 9 | packages.default = builtins.derivation { 10 | name = "trivial-1.0"; 11 | system = system; 12 | outputs = [ "out" ]; 13 | builder = "/bin/sh"; 14 | args = [ "-c" "echo trivial > $out" ]; 15 | }; 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from io import StringIO 4 | from pathlib import Path 5 | 6 | from click.testing import CliRunner 7 | 8 | 9 | def test_trivial(): 10 | import nixtract.cli 11 | from nixtract.cli import cli 12 | 13 | # need absolute path 14 | flake_path = (Path(__file__).parent / "fixtures" / "flake-trivial").absolute() 15 | # sanity checks 16 | assert flake_path.exists() 17 | assert flake_path.is_dir() 18 | assert (flake_path / "flake.nix").exists() 19 | assert (flake_path / "flake.lock").exists() 20 | 21 | # CLI logger 22 | cli_stderr_handler = StringIO() 23 | logging.getLogger(nixtract.cli.__name__).addHandler( 24 | logging.StreamHandler(stream=cli_stderr_handler) 25 | ) 26 | 27 | # test CLI 28 | runner = CliRunner( 29 | mix_stderr=False, 30 | ) 31 | result = runner.invoke( 32 | cli=cli, 33 | args=[ 34 | "--target-flake-ref", 35 | f"path:{flake_path}", 36 | "--target-system", 37 | "x86_64-linux", 38 | "--verbose", 39 | # print to stdout 40 | "-", 41 | ], 42 | ) 43 | 44 | # test success 45 | assert result.exit_code == 0 46 | 47 | # test output node 48 | # trivial flake should contain a single node without dependencies 49 | assert result.stdout != "", "Empty result" 50 | result_node = json.loads(result.stdout) 51 | assert result_node.get("output_path") is not None 52 | assert result_node.get("name") == "trivial-1.0" 53 | assert result_node.get("parsed_name", {}).get("name") == "trivial" 54 | assert result_node.get("parsed_name", {}).get("version") == "1.0" 55 | assert result_node.get("build_inputs") == [] 56 | 57 | # test behavior through logs 58 | # quite unstable, will do for now 59 | cli_stderr_handler.seek(0) 60 | cli_stderr = cli_stderr_handler.read() 61 | assert "FINDER EXIT" in cli_stderr 62 | assert "QUEUE PROCESSOR CLOSED" in cli_stderr 63 | assert "READER THREAD EXIT" in cli_stderr 64 | assert "PROCESS QUEUE THREAD EXIT" in cli_stderr 65 | assert "POOL EXIT" in cli_stderr 66 | 67 | 68 | def test_direct_buildinput(): 69 | import nixtract.cli 70 | from nixtract.cli import cli 71 | 72 | # need absolute path 73 | flake_path = ( 74 | Path(__file__).parent / "fixtures" / "flake-direct-buildInput" 75 | ).absolute() 76 | # sanity checks 77 | assert flake_path.exists() 78 | assert flake_path.is_dir() 79 | assert (flake_path / "flake.nix").exists() 80 | assert (flake_path / "flake.lock").exists() 81 | 82 | # CLI logger 83 | cli_stderr_handler = StringIO() 84 | logging.getLogger(nixtract.cli.__name__).addHandler( 85 | logging.StreamHandler(stream=cli_stderr_handler) 86 | ) 87 | 88 | # test CLI 89 | runner = CliRunner( 90 | mix_stderr=False, 91 | ) 92 | result = runner.invoke( 93 | cli=cli, 94 | args=[ 95 | "--target-flake-ref", 96 | f"path:{flake_path}", 97 | "--target-system", 98 | "x86_64-linux", 99 | "--verbose", 100 | # print to stdout 101 | "-", 102 | ], 103 | ) 104 | 105 | # test success 106 | assert result.exit_code == 0 107 | 108 | # test output node 109 | # this flake should contain at least 2 nodes, pkg1 and pkg2 110 | # pkg2 should have pkg1 as a build input 111 | assert result.stdout != "", "Empty result" 112 | stdout_lines = result.stdout.splitlines() 113 | assert len(stdout_lines) >= 2 114 | result_nodes = [json.loads(line) for line in stdout_lines] 115 | pkg2_node = next((node for node in result_nodes if node["name"] == "pkg2"), None) 116 | assert pkg2_node is not None 117 | assert pkg2_node.get("output_path") is not None 118 | assert pkg2_node.get("name") == "pkg2" 119 | assert pkg2_node.get("parsed_name", {}).get("name") == "pkg2" 120 | assert ( 121 | pkg2_node.get("nixpkgs_metadata", {}).get("licenses")[0].get("spdx_id") == "MIT" 122 | ) 123 | assert ( 124 | pkg2_node.get("nixpkgs_metadata", {}).get("licenses")[1].get("spdx_id") 125 | == "Apache-2.0" 126 | ) 127 | assert len(pkg2_node.get("build_inputs")) >= 1 128 | pkg2_buildinput_pkg1 = next( 129 | ( 130 | bi 131 | for bi in pkg2_node.get("build_inputs") 132 | if bi["output_path"].endswith("-pkg1") 133 | ), 134 | None, 135 | ) 136 | assert pkg2_buildinput_pkg1 is not None 137 | 138 | # make sure that the build input has been described as well 139 | pkg1_node = next((node for node in result_nodes if node["name"] == "pkg1"), None) 140 | assert pkg1_node is not None 141 | assert pkg1_node.get("output_path") == pkg2_buildinput_pkg1["output_path"] 142 | assert pkg1_node.get("output_path") == pkg2_buildinput_pkg1["output_path"] 143 | assert ( 144 | pkg1_node.get("nixpkgs_metadata", {}).get("licenses")[0].get("spdx_id") 145 | == "GPL-2.0-or-later" 146 | ) 147 | 148 | # test behavior through logs 149 | # quite unstable, will do for now 150 | cli_stderr_handler.seek(0) 151 | cli_stderr = cli_stderr_handler.read() 152 | assert "FINDER EXIT" in cli_stderr 153 | assert "QUEUE PROCESSOR CLOSED" in cli_stderr 154 | assert "READER THREAD EXIT" in cli_stderr 155 | assert "PROCESS QUEUE THREAD EXIT" in cli_stderr 156 | assert "POOL EXIT" in cli_stderr 157 | 158 | 159 | def test_trivial_trivial_rust(): 160 | import nixtract.cli 161 | from nixtract.cli import cli 162 | 163 | # need absolute path 164 | flake_path = (Path(__file__).parent / "fixtures" / "flake-trivial-rust").absolute() 165 | # sanity checks 166 | assert flake_path.exists() 167 | assert flake_path.is_dir() 168 | assert (flake_path / "flake.nix").exists() 169 | assert (flake_path / "flake.lock").exists() 170 | 171 | # CLI logger 172 | cli_stderr_handler = StringIO() 173 | logging.getLogger(nixtract.cli.__name__).addHandler( 174 | logging.StreamHandler(stream=cli_stderr_handler) 175 | ) 176 | 177 | # test CLI 178 | runner = CliRunner( 179 | mix_stderr=False, 180 | ) 181 | result = runner.invoke( 182 | cli=cli, 183 | args=[ 184 | "--target-flake-ref", 185 | f"path:{flake_path}", 186 | "--target-system", 187 | "x86_64-linux", 188 | "--verbose", 189 | # print to stdout 190 | "-", 191 | ], 192 | ) 193 | 194 | # test success 195 | assert result.exit_code == 0 196 | 197 | # test output node 198 | assert result.stdout != "", "Empty result" 199 | stdout_lines = result.stdout.splitlines() 200 | result_nodes = [json.loads(line) for line in stdout_lines] 201 | hello_node = next( 202 | (node for node in result_nodes if node["name"] == "hello-0.1.0"), None 203 | ) 204 | assert hello_node is not None 205 | assert hello_node.get("output_path") is not None 206 | assert hello_node.get("parsed_name", {}).get("name") == "hello" 207 | assert ( 208 | hello_node.get("src", {}).get("git_repo_url", "") 209 | == "https://github.com/hello-lang/Rust.git" 210 | ) 211 | assert ( 212 | hello_node.get("src", {}).get("rev", "") 213 | == "8e8bd39a444f6d6c7b01046a6b0600273911ac58" 214 | ) 215 | 216 | # test behavior through logs 217 | # quite unstable, will do for now 218 | cli_stderr_handler.seek(0) 219 | cli_stderr = cli_stderr_handler.read() 220 | assert "FINDER EXIT" in cli_stderr 221 | assert "QUEUE PROCESSOR CLOSED" in cli_stderr 222 | assert "READER THREAD EXIT" in cli_stderr 223 | assert "PROCESS QUEUE THREAD EXIT" in cli_stderr 224 | assert "POOL EXIT" in cli_stderr 225 | --------------------------------------------------------------------------------