├── .github └── workflows │ ├── general.yml │ ├── release.yml │ └── security.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── dist-workspace.toml └── src ├── dedup.rs ├── lib.rs └── main.rs /.github/workflows/general.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | test: 16 | name: Test 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions-rust-lang/setup-rust-toolchain@v1 21 | - name: Build tests 22 | run: cargo test --no-run 23 | - name: Run tests 24 | run: cargo test 25 | 26 | fmt: 27 | name: Format 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: actions-rust-lang/setup-rust-toolchain@v1 32 | with: 33 | components: rustfmt 34 | - name: Enforce formatting 35 | run: cargo fmt --check 36 | 37 | lint: 38 | name: Lint 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: actions-rust-lang/setup-rust-toolchain@v1 43 | with: 44 | components: clippy 45 | - name: Linting 46 | run: cargo clippy 47 | 48 | coverage: 49 | name: Code coverage 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout repository 53 | uses: actions/checkout@v4 54 | - uses: actions-rust-lang/setup-rust-toolchain@v1 55 | with: 56 | components: llvm-tools-preview 57 | - name: Install cargo-llvm-cov 58 | uses: taiki-e/install-action@cargo-llvm-cov 59 | - name: Generate code coverage 60 | run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info 61 | - name: Generate report 62 | run: cargo llvm-cov report --html --output-dir coverage 63 | - uses: actions/upload-artifact@v4 64 | with: 65 | name: "Coverage report" 66 | path: coverage/ -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/ 2 | # 3 | # Copyright 2022-2024, axodotdev 4 | # SPDX-License-Identifier: MIT or Apache-2.0 5 | # 6 | # CI that: 7 | # 8 | # * checks for a Git Tag that looks like a release 9 | # * builds artifacts with dist (archives, installers, hashes) 10 | # * uploads those artifacts to temporary workflow zip 11 | # * on success, uploads the artifacts to a GitHub Release 12 | # 13 | # Note that the GitHub Release will be created with a generated 14 | # title/body based on your changelogs. 15 | 16 | name: Release 17 | permissions: 18 | "contents": "write" 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 22 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 25 | # 26 | # If PACKAGE_NAME is specified, then the announcement will be for that 27 | # package (erroring out if it doesn't have the given version or isn't dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 30 | # (dist-able) packages in the workspace with that version (this mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent announcement for each one. However, GitHub 36 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 37 | # mistake. 38 | # 39 | # If there's a prerelease-style suffix to the version, then the release(s) 40 | # will be marked as a prerelease. 41 | on: 42 | pull_request: 43 | push: 44 | tags: 45 | - '**[0-9]+.[0-9]+.[0-9]+*' 46 | 47 | jobs: 48 | # Run 'dist plan' (or host) to determine what tasks we need to do 49 | plan: 50 | runs-on: "ubuntu-20.04" 51 | outputs: 52 | val: ${{ steps.plan.outputs.manifest }} 53 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 54 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 55 | publishing: ${{ !github.event.pull_request }} 56 | env: 57 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | submodules: recursive 62 | - name: Install dist 63 | # we specify bash to get pipefail; it guards against the `curl` command 64 | # failing. otherwise `sh` won't catch that `curl` returned non-0 65 | shell: bash 66 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.28.0/cargo-dist-installer.sh | sh" 67 | - name: Cache dist 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: cargo-dist-cache 71 | path: ~/.cargo/bin/dist 72 | # sure would be cool if github gave us proper conditionals... 73 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 74 | # functionality based on whether this is a pull_request, and whether it's from a fork. 75 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 76 | # but also really annoying to build CI around when it needs secrets to work right.) 77 | - id: plan 78 | run: | 79 | dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 80 | echo "dist ran successfully" 81 | cat plan-dist-manifest.json 82 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 83 | - name: "Upload dist-manifest.json" 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: artifacts-plan-dist-manifest 87 | path: plan-dist-manifest.json 88 | 89 | # Build and packages all the platform-specific things 90 | build-local-artifacts: 91 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 92 | # Let the initial task tell us to not run (currently very blunt) 93 | needs: 94 | - plan 95 | 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') }} 96 | strategy: 97 | fail-fast: false 98 | # Target platforms/runners are computed by dist in create-release. 99 | # Each member of the matrix has the following arguments: 100 | # 101 | # - runner: the github runner 102 | # - dist-args: cli flags to pass to dist 103 | # - install-dist: expression to run to install dist on the runner 104 | # 105 | # Typically there will be: 106 | # - 1 "global" task that builds universal installers 107 | # - N "local" tasks that build each platform's binaries and platform-specific installers 108 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 109 | runs-on: ${{ matrix.runner }} 110 | container: ${{ matrix.container && matrix.container.image || null }} 111 | env: 112 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 113 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 114 | steps: 115 | - name: enable windows longpaths 116 | run: | 117 | git config --global core.longpaths true 118 | - uses: actions/checkout@v4 119 | with: 120 | submodules: recursive 121 | - name: Install Rust non-interactively if not already installed 122 | if: ${{ matrix.container }} 123 | run: | 124 | if ! command -v cargo > /dev/null 2>&1; then 125 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 126 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 127 | fi 128 | - name: Install dist 129 | run: ${{ matrix.install_dist.run }} 130 | # Get the dist-manifest 131 | - name: Fetch local artifacts 132 | uses: actions/download-artifact@v4 133 | with: 134 | pattern: artifacts-* 135 | path: target/distrib/ 136 | merge-multiple: true 137 | - name: Install dependencies 138 | run: | 139 | ${{ matrix.packages_install }} 140 | - name: Build artifacts 141 | run: | 142 | # Actually do builds and make zips and whatnot 143 | dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 144 | echo "dist ran successfully" 145 | - id: cargo-dist 146 | name: Post-build 147 | # We force bash here just because github makes it really hard to get values up 148 | # to "real" actions without writing to env-vars, and writing to env-vars has 149 | # inconsistent syntax between shell and powershell. 150 | shell: bash 151 | run: | 152 | # Parse out what we just built and upload it to scratch storage 153 | echo "paths<> "$GITHUB_OUTPUT" 154 | dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" 155 | echo "EOF" >> "$GITHUB_OUTPUT" 156 | 157 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 158 | - name: "Upload artifacts" 159 | uses: actions/upload-artifact@v4 160 | with: 161 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 162 | path: | 163 | ${{ steps.cargo-dist.outputs.paths }} 164 | ${{ env.BUILD_MANIFEST_NAME }} 165 | 166 | # Build and package all the platform-agnostic(ish) things 167 | build-global-artifacts: 168 | needs: 169 | - plan 170 | - build-local-artifacts 171 | runs-on: "ubuntu-20.04" 172 | env: 173 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 174 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 175 | steps: 176 | - uses: actions/checkout@v4 177 | with: 178 | submodules: recursive 179 | - name: Install cached dist 180 | uses: actions/download-artifact@v4 181 | with: 182 | name: cargo-dist-cache 183 | path: ~/.cargo/bin/ 184 | - run: chmod +x ~/.cargo/bin/dist 185 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 186 | - name: Fetch local artifacts 187 | uses: actions/download-artifact@v4 188 | with: 189 | pattern: artifacts-* 190 | path: target/distrib/ 191 | merge-multiple: true 192 | - id: cargo-dist 193 | shell: bash 194 | run: | 195 | dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 196 | echo "dist ran successfully" 197 | 198 | # Parse out what we just built and upload it to scratch storage 199 | echo "paths<> "$GITHUB_OUTPUT" 200 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 201 | echo "EOF" >> "$GITHUB_OUTPUT" 202 | 203 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 204 | - name: "Upload artifacts" 205 | uses: actions/upload-artifact@v4 206 | with: 207 | name: artifacts-build-global 208 | path: | 209 | ${{ steps.cargo-dist.outputs.paths }} 210 | ${{ env.BUILD_MANIFEST_NAME }} 211 | # Determines if we should publish/announce 212 | host: 213 | needs: 214 | - plan 215 | - build-local-artifacts 216 | - build-global-artifacts 217 | # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) 218 | 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') }} 219 | env: 220 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 221 | runs-on: "ubuntu-20.04" 222 | outputs: 223 | val: ${{ steps.host.outputs.manifest }} 224 | steps: 225 | - uses: actions/checkout@v4 226 | with: 227 | submodules: recursive 228 | - name: Install cached dist 229 | uses: actions/download-artifact@v4 230 | with: 231 | name: cargo-dist-cache 232 | path: ~/.cargo/bin/ 233 | - run: chmod +x ~/.cargo/bin/dist 234 | # Fetch artifacts from scratch-storage 235 | - name: Fetch artifacts 236 | uses: actions/download-artifact@v4 237 | with: 238 | pattern: artifacts-* 239 | path: target/distrib/ 240 | merge-multiple: true 241 | - id: host 242 | shell: bash 243 | run: | 244 | dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 245 | echo "artifacts uploaded and released successfully" 246 | cat dist-manifest.json 247 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 248 | - name: "Upload dist-manifest.json" 249 | uses: actions/upload-artifact@v4 250 | with: 251 | # Overwrite the previous copy 252 | name: artifacts-dist-manifest 253 | path: dist-manifest.json 254 | # Create a GitHub Release while uploading all files to it 255 | - name: "Download GitHub Artifacts" 256 | uses: actions/download-artifact@v4 257 | with: 258 | pattern: artifacts-* 259 | path: artifacts 260 | merge-multiple: true 261 | - name: Cleanup 262 | run: | 263 | # Remove the granular manifests 264 | rm -f artifacts/*-dist-manifest.json 265 | - name: Create GitHub Release 266 | env: 267 | PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" 268 | ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" 269 | ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" 270 | RELEASE_COMMIT: "${{ github.sha }}" 271 | run: | 272 | # Write and read notes from a file to avoid quoting breaking things 273 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 274 | 275 | gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* 276 | 277 | announce: 278 | needs: 279 | - plan 280 | - host 281 | # use "always() && ..." to allow us to wait for all publish jobs while 282 | # still allowing individual publish jobs to skip themselves (for prereleases). 283 | # "host" however must run to completion, no skipping allowed! 284 | if: ${{ always() && needs.host.result == 'success' }} 285 | runs-on: "ubuntu-20.04" 286 | env: 287 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 288 | steps: 289 | - uses: actions/checkout@v4 290 | with: 291 | submodules: recursive 292 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Verify dependencies 2 | 3 | on: 4 | schedule: 5 | - cron: "0 8 * * *" 6 | push: 7 | paths: 8 | - "**/Cargo.toml" 9 | - "**/Cargo.lock" 10 | 11 | jobs: 12 | advisories: 13 | name: Advisories 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: EmbarkStudios/cargo-deny-action@v2 18 | with: 19 | # Check if security advisories have been issued 20 | # against any of the crates in our dependency tree 21 | command: check advisories 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 10 | dependencies = [ 11 | "cfg-if", 12 | "getrandom", 13 | "once_cell", 14 | "version_check", 15 | "zerocopy", 16 | ] 17 | 18 | [[package]] 19 | name = "anstream" 20 | version = "0.6.13" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" 23 | dependencies = [ 24 | "anstyle", 25 | "anstyle-parse", 26 | "anstyle-query", 27 | "anstyle-wincon", 28 | "colorchoice", 29 | "utf8parse", 30 | ] 31 | 32 | [[package]] 33 | name = "anstyle" 34 | version = "1.0.6" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 37 | 38 | [[package]] 39 | name = "anstyle-parse" 40 | version = "0.2.3" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 43 | dependencies = [ 44 | "utf8parse", 45 | ] 46 | 47 | [[package]] 48 | name = "anstyle-query" 49 | version = "1.0.2" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 52 | dependencies = [ 53 | "windows-sys", 54 | ] 55 | 56 | [[package]] 57 | name = "anstyle-wincon" 58 | version = "3.0.2" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 61 | dependencies = [ 62 | "anstyle", 63 | "windows-sys", 64 | ] 65 | 66 | [[package]] 67 | name = "anyhow" 68 | version = "1.0.81" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" 71 | 72 | [[package]] 73 | name = "autocfg" 74 | version = "1.1.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 77 | 78 | [[package]] 79 | name = "camino" 80 | version = "1.1.6" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" 83 | dependencies = [ 84 | "serde", 85 | ] 86 | 87 | [[package]] 88 | name = "cargo-autoinherit" 89 | version = "0.1.6" 90 | dependencies = [ 91 | "anyhow", 92 | "cargo-manifest", 93 | "clap", 94 | "fs-err", 95 | "guppy", 96 | "pathdiff", 97 | "semver", 98 | "toml", 99 | "toml_edit", 100 | ] 101 | 102 | [[package]] 103 | name = "cargo-manifest" 104 | version = "0.19.1" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "a1d8af896b707212cd0e99c112a78c9497dd32994192a463ed2f7419d29bd8c6" 107 | dependencies = [ 108 | "serde", 109 | "thiserror 2.0.11", 110 | "toml", 111 | ] 112 | 113 | [[package]] 114 | name = "cargo-platform" 115 | version = "0.1.7" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" 118 | dependencies = [ 119 | "serde", 120 | ] 121 | 122 | [[package]] 123 | name = "cargo_metadata" 124 | version = "0.18.1" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" 127 | dependencies = [ 128 | "camino", 129 | "cargo-platform", 130 | "semver", 131 | "serde", 132 | "serde_json", 133 | "thiserror 1.0.58", 134 | ] 135 | 136 | [[package]] 137 | name = "cfg-expr" 138 | version = "0.15.7" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" 141 | dependencies = [ 142 | "smallvec", 143 | "target-lexicon", 144 | ] 145 | 146 | [[package]] 147 | name = "cfg-if" 148 | version = "1.0.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 151 | 152 | [[package]] 153 | name = "clap" 154 | version = "4.5.4" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" 157 | dependencies = [ 158 | "clap_builder", 159 | "clap_derive", 160 | ] 161 | 162 | [[package]] 163 | name = "clap_builder" 164 | version = "4.5.2" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 167 | dependencies = [ 168 | "anstream", 169 | "anstyle", 170 | "clap_lex", 171 | "strsim", 172 | ] 173 | 174 | [[package]] 175 | name = "clap_derive" 176 | version = "4.5.4" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" 179 | dependencies = [ 180 | "heck", 181 | "proc-macro2", 182 | "quote", 183 | "syn", 184 | ] 185 | 186 | [[package]] 187 | name = "clap_lex" 188 | version = "0.7.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 191 | 192 | [[package]] 193 | name = "colorchoice" 194 | version = "1.0.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 197 | 198 | [[package]] 199 | name = "debug-ignore" 200 | version = "1.0.5" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "ffe7ed1d93f4553003e20b629abe9085e1e81b1429520f897f8f8860bc6dfc21" 203 | 204 | [[package]] 205 | name = "either" 206 | version = "1.10.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 209 | 210 | [[package]] 211 | name = "equivalent" 212 | version = "1.0.1" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 215 | 216 | [[package]] 217 | name = "fixedbitset" 218 | version = "0.4.2" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" 221 | 222 | [[package]] 223 | name = "fs-err" 224 | version = "2.11.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" 227 | dependencies = [ 228 | "autocfg", 229 | ] 230 | 231 | [[package]] 232 | name = "getrandom" 233 | version = "0.2.12" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 236 | dependencies = [ 237 | "cfg-if", 238 | "libc", 239 | "wasi", 240 | ] 241 | 242 | [[package]] 243 | name = "guppy" 244 | version = "0.17.5" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "34e99a7734579b834a076ef11789783c153c6eb5fb3520ed15bc41f483f0f317" 247 | dependencies = [ 248 | "ahash", 249 | "camino", 250 | "cargo_metadata", 251 | "cfg-if", 252 | "debug-ignore", 253 | "fixedbitset", 254 | "guppy-workspace-hack", 255 | "indexmap", 256 | "itertools", 257 | "nested", 258 | "once_cell", 259 | "pathdiff", 260 | "petgraph", 261 | "semver", 262 | "serde", 263 | "serde_json", 264 | "smallvec", 265 | "static_assertions", 266 | "target-spec", 267 | ] 268 | 269 | [[package]] 270 | name = "guppy-workspace-hack" 271 | version = "0.1.0" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "92620684d99f750bae383ecb3be3748142d6095760afd5cbcf2261e9a279d780" 274 | 275 | [[package]] 276 | name = "hashbrown" 277 | version = "0.14.3" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 280 | 281 | [[package]] 282 | name = "heck" 283 | version = "0.5.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 286 | 287 | [[package]] 288 | name = "indexmap" 289 | version = "2.2.5" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" 292 | dependencies = [ 293 | "equivalent", 294 | "hashbrown", 295 | ] 296 | 297 | [[package]] 298 | name = "itertools" 299 | version = "0.12.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 302 | dependencies = [ 303 | "either", 304 | ] 305 | 306 | [[package]] 307 | name = "itoa" 308 | version = "1.0.10" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 311 | 312 | [[package]] 313 | name = "libc" 314 | version = "0.2.153" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 317 | 318 | [[package]] 319 | name = "memchr" 320 | version = "2.7.1" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 323 | 324 | [[package]] 325 | name = "nested" 326 | version = "0.1.1" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "ca2b420f638f07fe83056b55ea190bb815f609ec5a35e7017884a10f78839c9e" 329 | 330 | [[package]] 331 | name = "once_cell" 332 | version = "1.19.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 335 | 336 | [[package]] 337 | name = "pathdiff" 338 | version = "0.2.1" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" 341 | dependencies = [ 342 | "camino", 343 | ] 344 | 345 | [[package]] 346 | name = "petgraph" 347 | version = "0.6.4" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" 350 | dependencies = [ 351 | "fixedbitset", 352 | "indexmap", 353 | ] 354 | 355 | [[package]] 356 | name = "proc-macro2" 357 | version = "1.0.93" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 360 | dependencies = [ 361 | "unicode-ident", 362 | ] 363 | 364 | [[package]] 365 | name = "quote" 366 | version = "1.0.35" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 369 | dependencies = [ 370 | "proc-macro2", 371 | ] 372 | 373 | [[package]] 374 | name = "ryu" 375 | version = "1.0.17" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 378 | 379 | [[package]] 380 | name = "semver" 381 | version = "1.0.22" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" 384 | dependencies = [ 385 | "serde", 386 | ] 387 | 388 | [[package]] 389 | name = "serde" 390 | version = "1.0.197" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 393 | dependencies = [ 394 | "serde_derive", 395 | ] 396 | 397 | [[package]] 398 | name = "serde_derive" 399 | version = "1.0.197" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 402 | dependencies = [ 403 | "proc-macro2", 404 | "quote", 405 | "syn", 406 | ] 407 | 408 | [[package]] 409 | name = "serde_json" 410 | version = "1.0.114" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" 413 | dependencies = [ 414 | "itoa", 415 | "ryu", 416 | "serde", 417 | ] 418 | 419 | [[package]] 420 | name = "serde_spanned" 421 | version = "0.6.5" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" 424 | dependencies = [ 425 | "serde", 426 | ] 427 | 428 | [[package]] 429 | name = "smallvec" 430 | version = "1.13.1" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 433 | 434 | [[package]] 435 | name = "static_assertions" 436 | version = "1.1.0" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 439 | 440 | [[package]] 441 | name = "strsim" 442 | version = "0.11.0" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 445 | 446 | [[package]] 447 | name = "syn" 448 | version = "2.0.98" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 451 | dependencies = [ 452 | "proc-macro2", 453 | "quote", 454 | "unicode-ident", 455 | ] 456 | 457 | [[package]] 458 | name = "target-lexicon" 459 | version = "0.12.14" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" 462 | 463 | [[package]] 464 | name = "target-spec" 465 | version = "3.1.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "36a8e795b1824524d13cdf04f73cf8b4f244ce86c96b4d2a83a6ca1a753d2752" 468 | dependencies = [ 469 | "cfg-expr", 470 | "guppy-workspace-hack", 471 | "target-lexicon", 472 | "unicode-ident", 473 | ] 474 | 475 | [[package]] 476 | name = "thiserror" 477 | version = "1.0.58" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" 480 | dependencies = [ 481 | "thiserror-impl 1.0.58", 482 | ] 483 | 484 | [[package]] 485 | name = "thiserror" 486 | version = "2.0.11" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 489 | dependencies = [ 490 | "thiserror-impl 2.0.11", 491 | ] 492 | 493 | [[package]] 494 | name = "thiserror-impl" 495 | version = "1.0.58" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" 498 | dependencies = [ 499 | "proc-macro2", 500 | "quote", 501 | "syn", 502 | ] 503 | 504 | [[package]] 505 | name = "thiserror-impl" 506 | version = "2.0.11" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 509 | dependencies = [ 510 | "proc-macro2", 511 | "quote", 512 | "syn", 513 | ] 514 | 515 | [[package]] 516 | name = "toml" 517 | version = "0.8.11" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e" 520 | dependencies = [ 521 | "indexmap", 522 | "serde", 523 | "serde_spanned", 524 | "toml_datetime", 525 | "toml_edit", 526 | ] 527 | 528 | [[package]] 529 | name = "toml_datetime" 530 | version = "0.6.5" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 533 | dependencies = [ 534 | "serde", 535 | ] 536 | 537 | [[package]] 538 | name = "toml_edit" 539 | version = "0.22.7" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992" 542 | dependencies = [ 543 | "indexmap", 544 | "serde", 545 | "serde_spanned", 546 | "toml_datetime", 547 | "winnow", 548 | ] 549 | 550 | [[package]] 551 | name = "unicode-ident" 552 | version = "1.0.12" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 555 | 556 | [[package]] 557 | name = "utf8parse" 558 | version = "0.2.1" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 561 | 562 | [[package]] 563 | name = "version_check" 564 | version = "0.9.4" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 567 | 568 | [[package]] 569 | name = "wasi" 570 | version = "0.11.0+wasi-snapshot-preview1" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 573 | 574 | [[package]] 575 | name = "windows-sys" 576 | version = "0.52.0" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 579 | dependencies = [ 580 | "windows-targets", 581 | ] 582 | 583 | [[package]] 584 | name = "windows-targets" 585 | version = "0.52.4" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 588 | dependencies = [ 589 | "windows_aarch64_gnullvm", 590 | "windows_aarch64_msvc", 591 | "windows_i686_gnu", 592 | "windows_i686_msvc", 593 | "windows_x86_64_gnu", 594 | "windows_x86_64_gnullvm", 595 | "windows_x86_64_msvc", 596 | ] 597 | 598 | [[package]] 599 | name = "windows_aarch64_gnullvm" 600 | version = "0.52.4" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 603 | 604 | [[package]] 605 | name = "windows_aarch64_msvc" 606 | version = "0.52.4" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 609 | 610 | [[package]] 611 | name = "windows_i686_gnu" 612 | version = "0.52.4" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 615 | 616 | [[package]] 617 | name = "windows_i686_msvc" 618 | version = "0.52.4" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 621 | 622 | [[package]] 623 | name = "windows_x86_64_gnu" 624 | version = "0.52.4" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 627 | 628 | [[package]] 629 | name = "windows_x86_64_gnullvm" 630 | version = "0.52.4" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 633 | 634 | [[package]] 635 | name = "windows_x86_64_msvc" 636 | version = "0.52.4" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 639 | 640 | [[package]] 641 | name = "winnow" 642 | version = "0.6.5" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" 645 | dependencies = [ 646 | "memchr", 647 | ] 648 | 649 | [[package]] 650 | name = "zerocopy" 651 | version = "0.7.32" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 654 | dependencies = [ 655 | "zerocopy-derive", 656 | ] 657 | 658 | [[package]] 659 | name = "zerocopy-derive" 660 | version = "0.7.32" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 663 | dependencies = [ 664 | "proc-macro2", 665 | "quote", 666 | "syn", 667 | ] 668 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-autoinherit" 3 | version = "0.1.6" 4 | edition = "2021" 5 | authors = ["Luca Palmieri "] 6 | description = "A cargo sub-command to leverage dependency inheritance wherever possible" 7 | keywords = ["cargo", "workspace", "inheritance", "dependencies"] 8 | categories = ["development-tools::cargo-plugins", "command-line-utilities"] 9 | repository = "https://github.com/mainmatter/cargo-autoinherit" 10 | license = "Apache-2.0 OR MIT" 11 | 12 | [dependencies] 13 | anyhow = "1.0.80" 14 | clap = { version = "4", features = ["derive"] } 15 | guppy = "0.17.5" 16 | fs-err = "2.11.0" 17 | cargo-manifest = "0.19.1" 18 | toml = "0.8.10" 19 | semver = "1.0.22" 20 | toml_edit = "0.22.6" 21 | pathdiff = "0.2.1" 22 | 23 | # The profile that 'cargo dist' will build with 24 | [profile.dist] 25 | inherits = "release" 26 | lto = "thin" 27 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mainmatter GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `cargo-autoinherit` 2 | 3 | A Cargo subcommand to automatically DRY up your `Cargo.toml` manifests in a workspace. 4 | 5 | > [!NOTE] 6 | > This project has been created by [Mainmatter](https://mainmatter.com/rust-consulting/). 7 | > Check out our [landing page](https://mainmatter.com/rust-consulting/) if you're looking for Rust consulting or training! 8 | 9 | ## The problem 10 | 11 | When you have multiple packages in a Cargo workspace, you often end up depending on the same packages 12 | in multiple `Cargo.toml` files. 13 | This duplication can become an issue: 14 | 15 | - When you want to update a dependency, you have to update it in multiple places. 16 | - When you need to add a new dependency, you first have to check if it's already used in another package of your workspace 17 | to keep versions in sync. 18 | 19 | This process it's error-prone and tedious. 20 | If you mess it up, you end up with different versions of the same dependency within your workspace. 21 | This can lead to hard-to-debug compilation errors or bloat your artifacts with unnecessary copies of the same package. 22 | 23 | ## The solution 24 | 25 | `cargo-autoinherit` is a Cargo subcommand that helps you to keep your dependencies DRY. 26 | 27 | It takes advantage of [dependency inheritance](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace), 28 | a recent feature of Cargo: you can specify dependencies in the root `Cargo.toml` of your workspace, 29 | and all the members of the workspace will inherit them (`dependency_name = { workspace = true}`). 30 | 31 | Converting an existing workspace to use dependency inheritance can be a tedious process—a non-trivial project 32 | can have tens of dependencies, and you have to move them all manually from the `Cargo.toml` 33 | of each member to the root `Cargo.toml`. 34 | 35 | `cargo-autoinherit` automates this process for you. 36 | 37 | ```bash 38 | # From the root of your workspace 39 | cargo autoinherit 40 | ``` 41 | 42 | It collects all the dependencies in your workspace, determines which ones can be DRYed and moves them to 43 | the `[workspace.dependencies]` section of the root `Cargo.toml`. It also takes care of updating the members' 44 | `Cargo.toml` files, setting the correct `features` field for each package. 45 | 46 | To exclude workspace members from the autoinherit process, you can either pass their packgage names as an 47 | option like so: 48 | 49 | ```bash 50 | cargo autoinherit -e cargo-inherit-test-web 51 | ``` 52 | 53 | or you can define the exclusion in the workspace metadata: 54 | 55 | ```toml 56 | # Cargo.toml 57 | [workspace] 58 | members = [ 59 | "cli", 60 | "config", 61 | "db", 62 | "web", 63 | "macros" 64 | ] 65 | 66 | [workspace.metadata.cargo-autoinherit] 67 | # Skip cargo-autoinherit for these packages 68 | exclude-members = [ 69 | "cargo-autoinherit-test-web" # <= This member will be excluded 70 | ] 71 | ``` 72 | 73 | ## Installation 74 | 75 | You can find prebuilt binaries on the [Releases page](https://github.com/mainmatter/cargo-autoinherit/releases). 76 | Alternatively, you can build from source: 77 | 78 | ```bash 79 | cargo install --locked cargo-autoinherit 80 | ``` 81 | 82 | ## Usage 83 | 84 | ```bash 85 | # From the root of your workspace 86 | cargo autoinherit 87 | ``` 88 | 89 | ## Limitations 90 | 91 | - `cargo-autoinherit` won't auto-inherit dependencies from private registries. 92 | - `cargo-autoinherit` will only merge version requirements that are obviously compatible (e.g. 93 | `^1.0.0` and `^1.1.5` will be merged to `^1.1.5`, but `^1.0.0` and `>=1,<2` won't be merged). 94 | 95 | # License 96 | 97 | Copyright © 2025- Mainmatter GmbH (https://mainmatter.com), released under the 98 | [MIT](./LICENSE-MIT) and [Apache](./LICENSE-APACHE) licenses. 99 | -------------------------------------------------------------------------------- /dist-workspace.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo:."] 3 | 4 | # Config for 'dist' 5 | [dist] 6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 7 | cargo-dist-version = "0.28.0" 8 | # CI backends to support 9 | ci = "github" 10 | # The installers to generate for each app 11 | installers = ["shell", "powershell"] 12 | # Target platforms to build apps for (Rust target-triple syntax) 13 | targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] 14 | # Which actions to run on pull requests 15 | pr-run-mode = "plan" 16 | # Path that installers should place binaries in 17 | install-path = "CARGO_HOME" 18 | # Whether to install an updater program 19 | install-updater = false 20 | -------------------------------------------------------------------------------- /src/dedup.rs: -------------------------------------------------------------------------------- 1 | use crate::{DependencySource, SharedDependency}; 2 | use semver::{Comparator, Op, Prerelease, VersionReq}; 3 | use std::cmp::Ordering; 4 | use std::collections::HashMap; 5 | 6 | /// For a given package, this struct keeps track of the versions that have been seen. 7 | /// It actively tries to minimize the number of versions that are kept. 8 | /// 9 | /// In particular: 10 | /// 11 | /// - If the same version requirement appears more than once, only one instance is kept. 12 | /// - If different version requirements appear, all instances are kept. 13 | /// - If the same version requirement appears more than once, with default features enabled in one 14 | /// case and disabled in another, only the disabled instance is kept. 15 | #[derive(Default)] 16 | pub(crate) struct MinimalVersionSet { 17 | seen: HashMap, 18 | } 19 | 20 | impl MinimalVersionSet { 21 | pub(crate) fn insert(&mut self, dep: SharedDependency) { 22 | if let Some(default_features) = self.seen.get_mut(&dep.source) { 23 | *default_features &= dep.default_features; 24 | return; 25 | } 26 | 27 | if let DependencySource::Version(version_req) = &dep.source { 28 | let mut swap = None; 29 | for (source, default_features) in self.seen.iter() { 30 | let DependencySource::Version(other_version_req) = source else { 31 | continue; 32 | }; 33 | if let Some(merged) = try_merge(version_req, other_version_req) { 34 | swap = Some(( 35 | source.clone(), 36 | merged, 37 | *default_features && dep.default_features, 38 | )); 39 | break; 40 | } 41 | } 42 | if let Some((source, merged, default_features)) = swap { 43 | self.seen.remove(&source); 44 | self.seen 45 | .insert(DependencySource::Version(merged), default_features); 46 | return; 47 | } 48 | } 49 | 50 | self.seen.insert(dep.source, dep.default_features); 51 | } 52 | 53 | pub(crate) fn into_iter(self) -> impl Iterator { 54 | self.seen 55 | .into_iter() 56 | .map(|(source, default_features)| SharedDependency { 57 | default_features, 58 | source, 59 | }) 60 | } 61 | 62 | pub(crate) fn len(&self) -> usize { 63 | self.seen.len() 64 | } 65 | } 66 | 67 | /// Tries to merge two version requirements into a single version requirement. 68 | /// 69 | /// We handle: 70 | /// 71 | /// - The case where both version requirements are the same. 72 | /// - The case where one version requirement is a wildcard and the other isn't. 73 | /// - The case where both version requirements are simple carets—e.g. `^1.2` and `^1.3.1`. 74 | /// In this case, we can merge them into `^1.3.1`. 75 | fn try_merge(first: &VersionReq, second: &VersionReq) -> Option { 76 | if first == second { 77 | return Some(first.clone()); 78 | } 79 | 80 | if first == &VersionReq::STAR && second != &VersionReq::STAR { 81 | // First is wildcard, second isn't 82 | return Some(second.clone()); 83 | } 84 | 85 | if first != &VersionReq::STAR && second == &VersionReq::STAR { 86 | // Second is wildcard, first isn't 87 | return Some(first.clone()); 88 | } 89 | 90 | let first = as_simple_caret(first)?; 91 | let second = as_simple_caret(second)?; 92 | if first.major != second.major { 93 | return None; 94 | } 95 | if first.major == 0 { 96 | if first.minor != second.minor { 97 | return None; 98 | } 99 | if first.minor == Some(0) { 100 | return None; 101 | } 102 | let comparator = Comparator { 103 | op: Op::Caret, 104 | major: second.major, 105 | minor: second.minor, 106 | patch: first.patch.max(second.patch), 107 | pre: Prerelease::EMPTY, 108 | }; 109 | return Some(VersionReq { 110 | comparators: vec![comparator], 111 | }); 112 | } 113 | let comparator = match first.minor.cmp(&second.minor) { 114 | Ordering::Less => Comparator { 115 | op: Op::Caret, 116 | major: second.major, 117 | minor: second.minor, 118 | patch: second.patch, 119 | pre: Prerelease::EMPTY, 120 | }, 121 | Ordering::Greater => Comparator { 122 | op: Op::Caret, 123 | major: first.major, 124 | minor: first.minor, 125 | patch: first.patch, 126 | pre: Prerelease::EMPTY, 127 | }, 128 | Ordering::Equal => Comparator { 129 | op: Op::Caret, 130 | major: first.major, 131 | minor: first.minor, 132 | patch: first.patch.max(second.patch), 133 | pre: Prerelease::EMPTY, 134 | }, 135 | }; 136 | Some(VersionReq { 137 | comparators: vec![comparator], 138 | }) 139 | } 140 | 141 | /// A `VersionReq` is "a simple caret" if it contains a single comparator with a `^` prefix 142 | /// and there are no pre-release or build identifiers. 143 | fn as_simple_caret(req: &VersionReq) -> Option<&Comparator> { 144 | if req.comparators.len() != 1 { 145 | return None; 146 | } 147 | let comp = &req.comparators[0]; 148 | if comp.op != Op::Caret { 149 | return None; 150 | } 151 | if comp.pre != Prerelease::EMPTY { 152 | return None; 153 | } 154 | Some(comp) 155 | } 156 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::dedup::MinimalVersionSet; 2 | use anyhow::{anyhow, Context}; 3 | use cargo_manifest::{Dependency, DependencyDetail, DepsSet, Manifest, Workspace}; 4 | use guppy::VersionReq; 5 | use std::collections::{BTreeMap, BTreeSet}; 6 | use std::fmt::Formatter; 7 | use toml_edit::{Array, Key}; 8 | 9 | mod dedup; 10 | 11 | #[derive(Debug, Default, Clone, clap::Args)] 12 | pub struct AutoInheritConf { 13 | #[arg( 14 | long, 15 | help = "Represents inherited dependencies as `package.workspace = true` if possible." 16 | )] 17 | pub prefer_simple_dotted: bool, 18 | /// Package name(s) of workspace member(s) to exclude. 19 | #[arg(short, long)] 20 | exclude_members: Vec, 21 | } 22 | 23 | #[derive(Debug, Default)] 24 | struct AutoInheritMetadata { 25 | exclude_members: Vec, 26 | } 27 | 28 | impl AutoInheritMetadata { 29 | fn from_workspace(workspace: &Workspace) -> Result { 30 | fn error() -> anyhow::Error { 31 | anyhow!("Excpected value of `exclude` in `workspace.metadata.cargo-autoinherit` to be an array of strings") 32 | } 33 | 34 | let Some(exclude) = workspace 35 | .metadata 36 | .as_ref() 37 | .and_then(|m| m.get("cargo-autoinherit")) 38 | .and_then(|v| v.as_table()) 39 | .and_then(|t| t.get("exclude-members").or(t.get("exclude_members"))) 40 | else { 41 | return Ok(Self::default()); 42 | }; 43 | 44 | let exclude: Vec = match exclude { 45 | toml::Value::Array(excluded) => excluded 46 | .iter() 47 | .map(|v| v.as_str().ok_or_else(error).map(|s| s.to_string())) 48 | .try_fold(Vec::with_capacity(excluded.len()), |mut res, item| { 49 | res.push(item?); 50 | Ok::<_, anyhow::Error>(res) 51 | })?, 52 | _ => return Err(error()), 53 | }; 54 | Ok(Self { 55 | exclude_members: exclude, 56 | }) 57 | } 58 | } 59 | 60 | /// Rewrites a `path` dependency as being absolute, based on a given path 61 | fn rewrite_dep_paths_as_absolute<'a, P: AsRef>( 62 | deps: impl Iterator, 63 | parent: P, 64 | ) { 65 | deps.for_each(|dep| { 66 | if let Dependency::Detailed(detail) = dep { 67 | detail.path = detail.path.as_mut().map(|path| { 68 | parent 69 | .as_ref() 70 | .join(path) 71 | .canonicalize() 72 | .unwrap() 73 | .to_str() 74 | .expect("Canonicalized absolute path contained non-UTF-8 segments.") 75 | .to_string() 76 | }) 77 | } 78 | }); 79 | } 80 | 81 | /// Rewrites a `path` dependency as being relative, based on a given path 82 | fn rewrite_dep_path_as_relative>(dep: &mut Dependency, parent: P) { 83 | if let Dependency::Detailed(detail) = dep { 84 | detail.path = detail.path.as_mut().map(|path| { 85 | pathdiff::diff_paths(path, parent.as_ref().canonicalize().unwrap()) 86 | .expect( 87 | "Error rewriting dependency path as relative: unable to determine path diff.", 88 | ) 89 | .to_str() 90 | .expect("Error rewriting dependency path as relative: path diff is not UTF-8.") 91 | .to_string() 92 | }) 93 | } 94 | } 95 | 96 | // Gets the first entry out of the document as a table if it exists, 97 | // or gets the second one if it doesn't. If that doesn't exist 98 | // either, then it returns an error. 99 | // Borrowing rules make it hard to do this in a function, 100 | // so here we are. 101 | macro_rules! get_either_table_mut { 102 | ($first:literal, $second:literal, $manifest_toml:expr) => { 103 | if let Some(i) = $manifest_toml 104 | .get_mut($first) 105 | .and_then(|d| d.as_table_mut()) 106 | { 107 | Ok(i) 108 | } else if let Some(i) = $manifest_toml 109 | .get_mut($second) 110 | .and_then(|d| d.as_table_mut()) 111 | { 112 | Ok(i) 113 | } else { 114 | Err(anyhow::anyhow!(concat!( 115 | "Failed to find `[", 116 | $first, 117 | "]` table in root manifest." 118 | ))) 119 | } 120 | }; 121 | } 122 | 123 | pub fn auto_inherit(conf: AutoInheritConf) -> Result<(), anyhow::Error> { 124 | let metadata = guppy::MetadataCommand::new().exec().context( 125 | "Failed to execute `cargo metadata`. Was the command invoked inside a Rust project?", 126 | )?; 127 | let graph = metadata 128 | .build_graph() 129 | .context("Failed to build package graph")?; 130 | let workspace_root = graph.workspace().root(); 131 | let mut root_manifest: Manifest = { 132 | let contents = fs_err::read_to_string(workspace_root.join("Cargo.toml").as_std_path()) 133 | .context("Failed to read root manifest")?; 134 | toml::from_str(&contents).context("Failed to parse root manifest")? 135 | }; 136 | let Some(workspace) = &mut root_manifest.workspace else { 137 | anyhow::bail!( 138 | "`cargo autoinherit` can only be run in a workspace. \ 139 | The root manifest ({}) does not have a `workspace` field.", 140 | workspace_root 141 | ) 142 | }; 143 | 144 | let autoinherit_metadata = AutoInheritMetadata::from_workspace(workspace)?; 145 | let excluded_members = BTreeSet::from_iter( 146 | conf.exclude_members 147 | .into_iter() 148 | .chain(autoinherit_metadata.exclude_members), 149 | ); 150 | 151 | let mut package_name2specs: BTreeMap = BTreeMap::new(); 152 | if let Some(deps) = &mut workspace.dependencies { 153 | rewrite_dep_paths_as_absolute(deps.values_mut(), workspace_root); 154 | process_deps(deps, &mut package_name2specs); 155 | } 156 | 157 | for member_id in graph.workspace().member_ids() { 158 | let package = graph.metadata(member_id)?; 159 | assert!(package.in_workspace()); 160 | 161 | let mut manifest: Manifest = { 162 | if excluded_members.contains(package.name()) { 163 | println!("Excluded workspace member `{}`", package.name()); 164 | continue; 165 | } 166 | let contents = fs_err::read_to_string(package.manifest_path().as_std_path()) 167 | .context("Failed to read root manifest")?; 168 | toml::from_str(&contents).context("Failed to parse root manifest")? 169 | }; 170 | if let Some(deps) = &mut manifest.dependencies { 171 | rewrite_dep_paths_as_absolute( 172 | deps.values_mut(), 173 | package.manifest_path().parent().unwrap(), 174 | ); 175 | process_deps(deps, &mut package_name2specs); 176 | } 177 | if let Some(deps) = &mut manifest.dev_dependencies { 178 | rewrite_dep_paths_as_absolute( 179 | deps.values_mut(), 180 | package.manifest_path().parent().unwrap(), 181 | ); 182 | process_deps(deps, &mut package_name2specs); 183 | } 184 | if let Some(deps) = &mut manifest.build_dependencies { 185 | rewrite_dep_paths_as_absolute( 186 | deps.values_mut(), 187 | package.manifest_path().parent().unwrap(), 188 | ); 189 | process_deps(deps, &mut package_name2specs); 190 | } 191 | } 192 | 193 | let mut package_name2inherited_source: BTreeMap = BTreeMap::new(); 194 | 'outer: for (package_name, action) in package_name2specs { 195 | let Action::TryInherit(specs) = action else { 196 | eprintln!("`{package_name}` won't be auto-inherited because it appears at least once from a source type \ 197 | that we currently don't support (e.g. private registry, path dependency)."); 198 | continue; 199 | }; 200 | if specs.len() > 1 { 201 | eprintln!("`{package_name}` won't be auto-inherited because there are multiple sources for it:"); 202 | for spec in specs.into_iter() { 203 | eprintln!(" - {}", spec.source); 204 | } 205 | continue 'outer; 206 | } 207 | 208 | let spec = specs.into_iter().next().unwrap(); 209 | package_name2inherited_source.insert(package_name, spec); 210 | } 211 | 212 | // Add new "shared" dependencies to `[workspace.dependencies]` 213 | let mut workspace_toml: toml_edit::DocumentMut = { 214 | let contents = fs_err::read_to_string(workspace_root.join("Cargo.toml").as_std_path()) 215 | .context("Failed to read root manifest")?; 216 | contents.parse().context("Failed to parse root manifest")? 217 | }; 218 | let workspace_table = workspace_toml.as_table_mut()["workspace"] 219 | .as_table_mut() 220 | .expect( 221 | "Failed to find `[workspace]` table in root manifest. \ 222 | This is a bug in `cargo_autoinherit`.", 223 | ); 224 | let workspace_deps = workspace_table 225 | .entry("dependencies") 226 | .or_insert(toml_edit::Item::Table(toml_edit::Table::new())) 227 | .as_table_mut() 228 | .expect("Failed to find `[workspace.dependencies]` table in root manifest."); 229 | let mut was_modified = false; 230 | for (package_name, source) in &package_name2inherited_source { 231 | if workspace_deps.get(package_name).is_some() { 232 | continue; 233 | } else { 234 | let mut dep = shared2dep(source); 235 | rewrite_dep_path_as_relative(&mut dep, workspace_root); 236 | 237 | insert_preserving_decor(workspace_deps, package_name, dep2toml_item(&dep)); 238 | was_modified = true; 239 | } 240 | } 241 | if was_modified { 242 | fs_err::write( 243 | workspace_root.join("Cargo.toml").as_std_path(), 244 | workspace_toml.to_string(), 245 | ) 246 | .context("Failed to write manifest")?; 247 | } 248 | 249 | // Inherit new "shared" dependencies in each member's manifest 250 | for member_id in graph.workspace().member_ids() { 251 | let package = graph.metadata(member_id)?; 252 | if excluded_members.contains(package.name()) { 253 | continue; 254 | } 255 | 256 | let manifest_contents = fs_err::read_to_string(package.manifest_path().as_std_path()) 257 | .context("Failed to read root manifest")?; 258 | let manifest: Manifest = 259 | toml::from_str(&manifest_contents).context("Failed to parse root manifest")?; 260 | let mut manifest_toml: toml_edit::DocumentMut = manifest_contents 261 | .parse() 262 | .context("Failed to parse root manifest")?; 263 | let mut was_modified = false; 264 | if let Some(deps) = &manifest.dependencies { 265 | let deps_toml = manifest_toml["dependencies"] 266 | .as_table_mut() 267 | .expect("Failed to find `[dependencies]` table in root manifest."); 268 | inherit_deps( 269 | deps, 270 | deps_toml, 271 | &package_name2inherited_source, 272 | &mut was_modified, 273 | conf.prefer_simple_dotted, 274 | ); 275 | } 276 | if let Some(deps) = &manifest.dev_dependencies { 277 | let deps_toml = 278 | get_either_table_mut!("dev-dependencies", "dev_dependencies", manifest_toml)?; 279 | 280 | inherit_deps( 281 | deps, 282 | deps_toml, 283 | &package_name2inherited_source, 284 | &mut was_modified, 285 | conf.prefer_simple_dotted, 286 | ); 287 | } 288 | if let Some(deps) = &manifest.build_dependencies { 289 | let deps_toml = 290 | get_either_table_mut!("build-dependencies", "build_dependencies", manifest_toml)?; 291 | 292 | inherit_deps( 293 | deps, 294 | deps_toml, 295 | &package_name2inherited_source, 296 | &mut was_modified, 297 | conf.prefer_simple_dotted, 298 | ); 299 | } 300 | if was_modified { 301 | fs_err::write( 302 | package.manifest_path().as_std_path(), 303 | manifest_toml.to_string(), 304 | ) 305 | .context("Failed to write manifest")?; 306 | } 307 | } 308 | 309 | Ok(()) 310 | } 311 | 312 | enum Action { 313 | TryInherit(MinimalVersionSet), 314 | Skip, 315 | } 316 | 317 | impl Default for Action { 318 | fn default() -> Self { 319 | Action::TryInherit(MinimalVersionSet::default()) 320 | } 321 | } 322 | 323 | fn inherit_deps( 324 | deps: &DepsSet, 325 | toml_deps: &mut toml_edit::Table, 326 | package_name2spec: &BTreeMap, 327 | was_modified: &mut bool, 328 | prefer_simple_dotted: bool, 329 | ) { 330 | for (name, dep) in deps { 331 | let package_name = dep.package().unwrap_or(name.as_str()); 332 | if !package_name2spec.contains_key(package_name) { 333 | continue; 334 | } 335 | match dep { 336 | Dependency::Simple(_) => { 337 | let mut inherited = toml_edit::InlineTable::new(); 338 | inherited.insert("workspace", toml_edit::value(true).into_value().unwrap()); 339 | inherited.set_dotted(prefer_simple_dotted); 340 | 341 | insert_preserving_decor(toml_deps, name, toml_edit::Item::Value(inherited.into())); 342 | *was_modified = true; 343 | } 344 | Dependency::Inherited(_) => { 345 | // Nothing to do. 346 | } 347 | Dependency::Detailed(details) => { 348 | let mut inherited = toml_edit::InlineTable::new(); 349 | inherited.insert("workspace", toml_edit::value(true).into_value().unwrap()); 350 | if let Some(features) = &details.features { 351 | inherited.insert( 352 | "features", 353 | toml_edit::Value::Array(Array::from_iter(features.iter())), 354 | ); 355 | } 356 | if let Some(optional) = details.optional { 357 | inherited.insert("optional", toml_edit::value(optional).into_value().unwrap()); 358 | } 359 | 360 | if inherited.len() == 1 { 361 | inherited.set_dotted(prefer_simple_dotted); 362 | } 363 | 364 | insert_preserving_decor(toml_deps, name, toml_edit::Item::Value(inherited.into())); 365 | *was_modified = true; 366 | } 367 | } 368 | } 369 | } 370 | 371 | fn insert_preserving_decor(table: &mut toml_edit::Table, key: &str, mut value: toml_edit::Item) { 372 | fn get_decor(item: &toml_edit::Item) -> Option { 373 | match item { 374 | toml_edit::Item::Value(v) => Some(v.decor().clone()), 375 | toml_edit::Item::Table(t) => Some(t.decor().clone()), 376 | _ => None, 377 | } 378 | } 379 | 380 | fn set_decor(item: &mut toml_edit::Item, decor: toml_edit::Decor) { 381 | match item { 382 | toml_edit::Item::Value(v) => { 383 | *v.decor_mut() = decor; 384 | } 385 | toml_edit::Item::Table(t) => { 386 | *t.decor_mut() = decor; 387 | } 388 | _ => unreachable!(), 389 | } 390 | } 391 | 392 | let mut new_key = Key::new(key); 393 | if let Some((existing_key, existing_value)) = table.get_key_value(key) { 394 | new_key = new_key.with_leaf_decor(existing_key.leaf_decor().to_owned()); 395 | 396 | if let Some(mut decor) = get_decor(existing_value) { 397 | // Tables tend to have newline whitespacing that doesn't agree with other types 398 | if existing_value.is_table() && !value.is_table() { 399 | decor.set_prefix(" "); 400 | } 401 | set_decor(&mut value, decor); 402 | } 403 | } 404 | table.insert_formatted(&new_key, value); 405 | } 406 | 407 | fn process_deps(deps: &DepsSet, package_name2specs: &mut BTreeMap) { 408 | for (name, details) in deps { 409 | match dep2shared_dep(details) { 410 | SourceType::Shareable(source) => { 411 | let action = package_name2specs.entry(name.clone()).or_default(); 412 | if let Action::TryInherit(set) = action { 413 | set.insert(source); 414 | } 415 | } 416 | SourceType::Inherited => {} 417 | SourceType::MustBeSkipped => { 418 | package_name2specs.insert(name.clone(), Action::Skip); 419 | } 420 | } 421 | } 422 | } 423 | 424 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] 425 | struct SharedDependency { 426 | default_features: bool, 427 | source: DependencySource, 428 | } 429 | 430 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] 431 | enum DependencySource { 432 | Version(VersionReq), 433 | Git { 434 | git: String, 435 | branch: Option, 436 | tag: Option, 437 | rev: Option, 438 | version: Option, 439 | }, 440 | Path { 441 | path: String, 442 | version: Option, 443 | }, 444 | } 445 | 446 | impl std::fmt::Display for DependencySource { 447 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 448 | match self { 449 | DependencySource::Version(version) => write!(f, "version: {}", version), 450 | DependencySource::Git { 451 | git, 452 | branch, 453 | tag, 454 | rev, 455 | version, 456 | } => { 457 | write!(f, "git: {}", git)?; 458 | if let Some(branch) = branch { 459 | write!(f, ", branch: {}", branch)?; 460 | } 461 | if let Some(tag) = tag { 462 | write!(f, ", tag: {}", tag)?; 463 | } 464 | if let Some(rev) = rev { 465 | write!(f, ", rev: {}", rev)?; 466 | } 467 | if let Some(version) = version { 468 | write!(f, ", version: {}", version)?; 469 | } 470 | Ok(()) 471 | } 472 | DependencySource::Path { path, version } => { 473 | write!(f, "path: {}", path)?; 474 | if let Some(version) = version { 475 | write!(f, ", version: {}", version)?; 476 | } 477 | Ok(()) 478 | } 479 | } 480 | } 481 | } 482 | 483 | enum SourceType { 484 | Shareable(SharedDependency), 485 | Inherited, 486 | MustBeSkipped, 487 | } 488 | 489 | fn dep2shared_dep(dep: &Dependency) -> SourceType { 490 | match dep { 491 | Dependency::Simple(version) => { 492 | let version_req = 493 | VersionReq::parse(version).expect("Failed to parse version requirement"); 494 | SourceType::Shareable(SharedDependency { 495 | default_features: true, 496 | source: DependencySource::Version(version_req), 497 | }) 498 | } 499 | Dependency::Inherited(_) => SourceType::Inherited, 500 | Dependency::Detailed(d) => { 501 | let mut source = None; 502 | // We ignore custom registries for now. 503 | if d.registry.is_some() || d.registry_index.is_some() { 504 | return SourceType::MustBeSkipped; 505 | } 506 | if d.path.is_some() { 507 | source = Some(DependencySource::Path { 508 | path: d.path.as_ref().unwrap().to_owned(), 509 | version: d.version.as_ref().map(|v| { 510 | VersionReq::parse(v).expect("Failed to parse version requirement") 511 | }), 512 | }); 513 | } else if let Some(git) = &d.git { 514 | source = Some(DependencySource::Git { 515 | git: git.to_owned(), 516 | branch: d.branch.to_owned(), 517 | tag: d.tag.to_owned(), 518 | rev: d.rev.to_owned(), 519 | version: d.version.as_ref().map(|v| { 520 | VersionReq::parse(v).expect("Failed to parse version requirement") 521 | }), 522 | }); 523 | } else if let Some(version) = &d.version { 524 | let version_req = 525 | VersionReq::parse(version).expect("Failed to parse version requirement"); 526 | source = Some(DependencySource::Version(version_req)); 527 | } 528 | match source { 529 | None => SourceType::MustBeSkipped, 530 | Some(source) => SourceType::Shareable(SharedDependency { 531 | default_features: d.default_features.unwrap_or(true), 532 | source, 533 | }), 534 | } 535 | } 536 | } 537 | } 538 | 539 | fn shared2dep(shared_dependency: &SharedDependency) -> Dependency { 540 | let SharedDependency { 541 | default_features, 542 | source, 543 | } = shared_dependency; 544 | match source { 545 | DependencySource::Version(version) => { 546 | if *default_features { 547 | Dependency::Simple(version.to_string()) 548 | } else { 549 | Dependency::Detailed(DependencyDetail { 550 | version: Some(version.to_string()), 551 | default_features: Some(false), 552 | ..DependencyDetail::default() 553 | }) 554 | } 555 | } 556 | DependencySource::Git { 557 | git, 558 | branch, 559 | tag, 560 | rev, 561 | version, 562 | } => Dependency::Detailed(DependencyDetail { 563 | package: None, 564 | version: version.as_ref().map(|v| v.to_string()), 565 | registry: None, 566 | registry_index: None, 567 | path: None, 568 | git: Some(git.clone()), 569 | branch: branch.clone(), 570 | tag: tag.clone(), 571 | rev: rev.clone(), 572 | features: None, 573 | optional: None, 574 | default_features: if *default_features { None } else { Some(false) }, 575 | }), 576 | DependencySource::Path { path, version } => Dependency::Detailed(DependencyDetail { 577 | package: None, 578 | version: version.as_ref().map(|v| v.to_string()), 579 | registry: None, 580 | registry_index: None, 581 | path: Some(path.clone()), 582 | git: None, 583 | branch: None, 584 | tag: None, 585 | rev: None, 586 | features: None, 587 | optional: None, 588 | default_features: if *default_features { None } else { Some(false) }, 589 | }), 590 | } 591 | } 592 | 593 | fn dep2toml_item(dependency: &Dependency) -> toml_edit::Item { 594 | match dependency { 595 | Dependency::Simple(version) => toml_edit::value(version.trim_start_matches('^').to_owned()), 596 | Dependency::Inherited(inherited) => { 597 | let mut table = toml_edit::InlineTable::new(); 598 | table.get_or_insert("workspace", true); 599 | if let Some(features) = &inherited.features { 600 | table.get_or_insert("features", Array::from_iter(features.iter())); 601 | } 602 | if let Some(optional) = inherited.optional { 603 | table.get_or_insert("optional", optional); 604 | } 605 | toml_edit::Item::Value(toml_edit::Value::InlineTable(table)) 606 | } 607 | Dependency::Detailed(details) => { 608 | let mut table = toml_edit::InlineTable::new(); 609 | let DependencyDetail { 610 | version, 611 | registry, 612 | registry_index, 613 | path, 614 | git, 615 | branch, 616 | tag, 617 | rev, 618 | features, 619 | optional, 620 | default_features, 621 | package, 622 | } = details; 623 | 624 | if let Some(version) = version { 625 | table.get_or_insert("version", version.trim_start_matches('^')); 626 | } 627 | if let Some(registry) = registry { 628 | table.get_or_insert("registry", registry); 629 | } 630 | if let Some(registry_index) = registry_index { 631 | table.get_or_insert("registry-index", registry_index); 632 | } 633 | if let Some(path) = path { 634 | table.get_or_insert("path", path); 635 | } 636 | if let Some(git) = git { 637 | table.get_or_insert("git", git); 638 | } 639 | if let Some(branch) = branch { 640 | table.get_or_insert("branch", branch); 641 | } 642 | if let Some(tag) = tag { 643 | table.get_or_insert("tag", tag); 644 | } 645 | if let Some(rev) = rev { 646 | table.get_or_insert("rev", rev); 647 | } 648 | if let Some(features) = features { 649 | table.get_or_insert("features", Array::from_iter(features.iter())); 650 | } 651 | if let Some(optional) = optional { 652 | table.get_or_insert( 653 | "optional", 654 | toml_edit::value(*optional).into_value().unwrap(), 655 | ); 656 | } 657 | if let Some(default_features) = default_features { 658 | table.get_or_insert( 659 | "default-features", 660 | toml_edit::value(*default_features).into_value().unwrap(), 661 | ); 662 | } 663 | if let Some(package) = package { 664 | table.get_or_insert("package", package); 665 | } 666 | 667 | toml_edit::Item::Value(toml_edit::Value::InlineTable(table)) 668 | } 669 | } 670 | } 671 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use cargo_autoinherit::{auto_inherit, AutoInheritConf}; 2 | 3 | use clap::Parser; 4 | 5 | #[derive(Parser)] 6 | #[command(bin_name = "cargo")] 7 | struct CliWrapper { 8 | #[command(subcommand)] 9 | command: CargoInvocation, 10 | } 11 | 12 | #[derive(Parser)] 13 | pub enum CargoInvocation { 14 | /// Automatically centralize all dependencies as workspace dependencies. 15 | #[command(name = "autoinherit")] 16 | AutoInherit(AutoInheritConf), 17 | } 18 | 19 | fn main() -> Result<(), anyhow::Error> { 20 | let cli = CliWrapper::parse(); 21 | let CargoInvocation::AutoInherit(conf) = cli.command; 22 | auto_inherit(conf) 23 | } 24 | --------------------------------------------------------------------------------